{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Errors\n", "\n", "Many errors will arise when you develop your Python package. \n", "At a mature stage of development the code should be error-free and robust. This means that \n", "anyone should expect to be able to use it without encountering errors.\n", "To ensure that as you continue developing your package you are not breaking some parts, leading to some errors without you \n", "noticing, the best way is to write a test suite.\n", "\n", "\n", "The test suite is a set of tests that should be run automatically to check every functionality of your package every time you update its distribution.\n", "\n", "\n", "Before going into the test part, let us recap on the different types of errors you will generally encounter.\n", "\n", "\n", "## Types of errors in Python\n", "\n", "In Python, there are several common **built-in exceptions** that you'll frequently encounter and might want to test against. Here are some of the main ones:\n", "\n", "1. `ZeroDivisionError`: Raised when attempting to divide by zero." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [10], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;241;43m10\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m \u001b[38;5;66;03m# Raises ZeroDivisionError\u001b[39;00m\n", "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" ] } ], "source": [ "result = 10 / 0 # Raises ZeroDivisionError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "2. `TypeError`: Raised when an operation or function is applied to an object of inappropriate type. For example, trying to add a string to an integer or passing a non-iterable to a function that expects an iterable." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "can only concatenate str (not \"int\") to str", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [9], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mtext\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m10\u001b[39;49m \u001b[38;5;66;03m# Raises TypeError\u001b[39;00m\n", "\u001b[0;31mTypeError\u001b[0m: can only concatenate str (not \"int\") to str" ] } ], "source": [ "result = 'text' + 10 # Raises TypeError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "3. `ValueError`: Raised when a function receives an argument of the correct type but inappropriate value. This could happen, for instance, when trying to convert a non-numeric string to an integer." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "invalid literal for int() with base 10: 'abc'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [8], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m number \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mint\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mabc\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# Raises ValueError\u001b[39;00m\n", "\u001b[0;31mValueError\u001b[0m: invalid literal for int() with base 10: 'abc'" ] } ], "source": [ "number = int(\"abc\") # Raises ValueError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "4. `IndexError`: Raised when an index is out of the range of a list, tuple, or other indexable collections. " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "ename": "IndexError", "evalue": "list index out of range", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [7], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m lst \u001b[38;5;241m=\u001b[39m [\u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m, \u001b[38;5;241m3\u001b[39m]\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mlst\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;241;43m5\u001b[39;49m\u001b[43m]\u001b[49m) \u001b[38;5;66;03m# Raises IndexError\u001b[39;00m\n", "\u001b[0;31mIndexError\u001b[0m: list index out of range" ] } ], "source": [ "lst = [1, 2, 3]\n", "print(lst[5]) # Raises IndexError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "5. `KeyError`: Raised when trying to access a dictionary with a key that doesn’t exist. This is useful for handling cases where a function requires specific dictionary keys." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "ename": "KeyError", "evalue": "'b'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [6], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m my_dict \u001b[38;5;241m=\u001b[39m {\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124ma\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;241m1\u001b[39m}\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43mmy_dict\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mb\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m) \u001b[38;5;66;03m# Raises KeyError\u001b[39;00m\n", "\u001b[0;31mKeyError\u001b[0m: 'b'" ] } ], "source": [ "my_dict = {\"a\": 1}\n", "print(my_dict[\"b\"]) # Raises KeyError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "6. `AttributeError`: Raised when an invalid attribute is referenced, typically due to accessing an attribute or method that doesn’t exist in an object." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "ename": "AttributeError", "evalue": "'MyClass' object has no attribute 'some_method'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [5], line 5\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n\u001b[1;32m 4\u001b[0m obj \u001b[38;5;241m=\u001b[39m MyClass()\n\u001b[0;32m----> 5\u001b[0m \u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msome_method\u001b[49m() \u001b[38;5;66;03m# Raises AttributeError\u001b[39;00m\n", "\u001b[0;31mAttributeError\u001b[0m: 'MyClass' object has no attribute 'some_method'" ] } ], "source": [ "class MyClass:\n", " pass\n", "\n", "obj = MyClass()\n", "obj.some_method() # Raises AttributeError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "7. `FileNotFoundError`: Raised when trying to open a file that does not exist. It’s often used in data science to handle cases where file paths are incorrect or files are missing.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "ename": "FileNotFoundError", "evalue": "[Errno 2] No such file or directory: 'non_existent_file.txt'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mnon_existent_file.txt\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 2\u001b[0m content \u001b[38;5;241m=\u001b[39m f\u001b[38;5;241m.\u001b[39mread() \u001b[38;5;66;03m# Raises FileNotFoundError\u001b[39;00m\n", "File \u001b[0;32m~/opt/miniconda3/lib/python3.9/site-packages/IPython/core/interactiveshell.py:282\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 275\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 276\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 277\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 278\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 279\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 280\u001b[0m )\n\u001b[0;32m--> 282\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: 'non_existent_file.txt'" ] } ], "source": [ "with open(\"non_existent_file.txt\") as f:\n", " content = f.read() # Raises FileNotFoundError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "8. `OverflowError`: Raised when a numerical calculation exceeds the maximum limit for a numeric type. This is common in scientific computations where very large numbers are generated." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "ename": "OverflowError", "evalue": "math range error", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mOverflowError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [2], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mmath\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mmath\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mexp\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1000\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# Raises OverflowError on some systems\u001b[39;00m\n", "\u001b[0;31mOverflowError\u001b[0m: math range error" ] } ], "source": [ "import math\n", "result = math.exp(1000) # Raises OverflowError on some systems\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "9. `AssertionError`: Raised when an `assert` statement fails. Useful in testing when specific conditions should be met." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "ename": "AssertionError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn [1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;241m2\u001b[39m \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m2\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m5\u001b[39m \u001b[38;5;66;03m# Raises AssertionError\u001b[39;00m\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "source": [ "assert 2 + 2 == 5 # Raises AssertionError" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "10. `RuntimeError`: A generic error raised when an error occurs that doesn’t fall into other categories. It’s often used in more complex scenarios where exceptions need custom handling.\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exception handling\n", "\n", "These exception allow us to use a very useful feature of Python which is called **exception handling**.\n", "\n", "An example is more useful than words:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Division successful!\n", "Execution complete.\n", "5.0\n", "Error: Cannot divide by zero!\n", "Execution complete.\n", "None\n" ] } ], "source": [ "def divide(a, b):\n", " try:\n", " result = a / b\n", " except ZeroDivisionError:\n", " print(\"Error: Cannot divide by zero!\")\n", " return None\n", " else:\n", " print(\"Division successful!\")\n", " return result\n", " finally:\n", " print(\"Execution complete.\")\n", "\n", "# Example usage\n", "print(divide(10, 2)) # Should print \"Division successful!\" and the result 5.0\n", "print(divide(10, 0)) # Should print \"Error: Cannot divide by zero!\" and return None\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Without exception handling, the program would crash. This feature allows you to handle errors gracefully and continue the execution of the program, which can mean simply exiting it but in a smooth manner, and providing a message to the user on what is going wrong." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Tests \n", "\n", "The goal of the test suite is to test every functionality and part of your package.\n", "\n", "As soos as you have finnished implementing a new part of your code, good practice wants you to write a test for it.\n", "\n", "The test suite is stored in the `tests` folder of your package root directory.\n", "\n", "## Test suite with pytest\n", "\n", "All the files in the `tests` folder are called `test_.py` where `` should be replaced by the name of the functionality you are testing.\n", "\n", "For instance, in our `company` package, we can create the following test files:\n", "\n", "```bash\n", "tests/\n", "├── test_base_company.py\n", "├── test_cli.py\n", "└── test_medical.py\n", "```\n", "\n", "The first one tests the `base_company.py` file (i.e. the `Company` class and its methods) and the second one tests the `medical.py` file (i.e. the `MedicalCompany` class and its methods).\n", "\n", "\n", "A test file looks contains a set of functions that look like this: \n", "\n", "```python\n", "def test_medical_init():\n", " med_company = MedicalCompany(name=\"MediCorp\", specialty=\"Cardiology\", drug_manufacturer=True)\n", " assert med_company.name == \"MediCorp\"\n", " assert med_company.specialty == \"Cardiology\"\n", " assert med_company.drug_manufacturer is True\n", "```\n", "\n", "These functions are all based on the `assert` statement.\n", "\n", "The `assert` statement is used to check if a condition is true. If the condition is false, an `AssertionError` is raised.\n", "\n", "\n", "Of course, you can be as creative as you want with the tests, and as data scientists, you will want loads of quantitative tests. \n", "\n", "\n", "For example, consider the `stock_price_difference` function in the `cli.py` file.\n", "\n", "We can write a test for this function as follows (in `tests/test_cli.py`):\n", "\n", "```python\n", "def test_get_stock_price_difference(capsys, monkeypatch):\n", " # Mock command-line arguments with a known ticker and date range\n", " monkeypatch.setattr(\"sys.argv\", [\n", " \"cli.py\", \"get_stock_price_difference\", \n", " \"--ticker\", \"AAPL\", \n", " \"--interval\", \"1y\", \n", " \"--stop_date\", \"2023-12-31\"\n", " ])\n", " \n", " # Run the CLI main function\n", " main()\n", " \n", " # Capture output\n", " captured = capsys.readouterr()\n", "\n", " # Test the numeric value directly by extracting it from the output\n", " # price_diff = float(captured.out.split(\": \")[1].strip())\n", " assert abs(price_diff - 18.717864990234) < 1e-4\n", "```\n", "\n", "Here we know that the value of the stock price difference is 18.717864990234 (at this precision), and we test that the value we get from the function is close enough to this value.\n", "\n", "`pytest` contains a nice feature allowing you to compare floating point numbers with a certain precision, which is `pytest.approx`.\n", "You could replace the last line above by: \n", "\n", "```python\n", " assert price_diff == pytest.approx(18.717864990234, rel=1e-4)\n", "```\n", "\n", "To test all entries in an array you can also use the following assertion in a test function: \n", "\n", "```python \n", "def test_():\n", "\n", " ... \n", "\n", " expected_values = np.array([0. , 3663.04149234, 5618.94079371, 6811.03765429, 7625.75439281,\n", " 8226.01526502, 8691.41376217, 9065.71293446, 9375.23339903, 9636.58188782])\n", " \n", " result = \n", " \n", " np.testing.assert_allclose(result, expected_values, rtol=1e-5)\n", "```\n", "\n", "\n", "## Additional features\n", "\n", "`pytest` has a lot of additional features that you can use to make your life easier.\n", "\n", "For instance, you can use the `monkeypatch` fixture to mock objects or functions, or the `capsys` fixture to capture the output (i.e., what is stored in `stdout`) of the print statements of your functions.\n", "\n", "We have created an example for this in the [test_medical.py file](https://github.com/borisbolliet/company_package/blob/main/tests/test_medical.py).\n", "\n", "\n", "## Running the test suite\n", "\n", "\n", "To run the test suite, go to the root directory of your package and run:\n", "\n", "```bash\n", "pytest -s tests/*\n", "```\n", "to run all the tests in the `tests` folder.\n", "\n", "Here the `-s` option is used to show the output of the print statements in your test files on the terminal. Without this option the print statements are automatically suppressed.\n", "\n", "If you want to run a single test, you can use the following command:\n", "\n", "```bash\n", "pytest tests/test_.py\n", "```\n", "\n", "When a test runs well you would see something like this:\n", "\n", "```bash\n", "================================================== test session starts ==================================================\n", "platform darwin -- Python 3.9.13, pytest-7.2.0, pluggy-1.0.0\n", "rootdir: /Users/boris/MPhil/company_package\n", "plugins: cov-4.1.0, anyio-3.6.2\n", "collecting ... Company package version: 0.0.0b1.dev8+g5c0d18a.d20241030\n", "collected 8 items \n", "\n", "tests/test_base_company.py ....\n", "tests/test_cli.py ..\n", "tests/test_medical.py ..\n", "\n", "=================================================== 8 passed in 0.85s ===================================================\n", "```\n", "\n", "When a test fails you would see something like this (here we artificially made a test fail by changing the expected value of stock price difference):\n", "\n", "```bash\n", "================================================== test session starts ==================================================\n", "platform darwin -- Python 3.9.13, pytest-7.2.0, pluggy-1.0.0\n", "rootdir: /Users/boris/MPhil/company_package\n", "plugins: cov-4.1.0, anyio-3.6.2\n", "collecting ... Company package version: 0.0.0b1.dev8+g5c0d18a.d20241030\n", "collected 8 items \n", "\n", "tests/test_base_company.py ....\n", "tests/test_cli.py .F\n", "tests/test_medical.py ..\n", "\n", "======================================================= FAILURES ========================================================\n", "____________________________________________ test_get_stock_price_difference ____________________________________________\n", "\n", "capsys = <_pytest.capture.CaptureFixture object at 0x134d294c0>\n", "monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x134d297c0>\n", "\n", " def test_get_stock_price_difference(capsys, monkeypatch):\n", " # Mock command-line arguments with a known ticker and date range\n", " monkeypatch.setattr(\"sys.argv\", [\n", " \"cli.py\", \"get_stock_price_difference\",\n", " \"--ticker\", \"AAPL\",\n", " \"--interval\", \"1y\",\n", " \"--stop_date\", \"2023-12-31\"\n", " ])\n", " \n", " # Run the CLI main function\n", " main()\n", " \n", " # Capture output\n", " captured = capsys.readouterr()\n", " \n", " # # Test the numeric value directly by extracting it from the output\n", " price_diff = float(captured.out.split(\": \")[1].strip())\n", " # assert abs(price_diff - 18.717864990234) < 1e-4\n", " \n", " \n", " # Test using pytest.approx for better floating point comparison\n", "> assert price_diff == pytest.approx(19.717864990234, rel=1e-4)\n", "E assert 18.717864990234375 == 19.717864990234 ± 2.0e-03\n", "E comparison failed\n", "E Obtained: 18.717864990234375\n", "E Expected: 19.717864990234 ± 2.0e-03\n", "\n", "tests/test_cli.py:42: AssertionError\n", "================================================ short test summary info ================================================\n", "FAILED tests/test_cli.py::test_get_stock_price_difference - assert 18.717864990234375 == 19.717864990234 ± 2.0e-03\n", "============================================== 1 failed, 7 passed in 0.89s ==============================================\n", "```\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.9" } }, "nbformat": 4, "nbformat_minor": 4 }