{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ee7da2b6-b3d2-4bad-928f-9c416b1eaec7",
   "metadata": {},
   "source": [
    "# OOP Design & Methods"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "0f6e77dd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.723133Z",
     "iopub.status.busy": "2026-04-27T03:21:51.723049Z",
     "iopub.status.idle": "2026-04-27T03:21:51.910032Z",
     "shell.execute_reply": "2026-04-27T03:21:51.909770Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "current = Path.cwd()\n",
    "for parent in [current, *current.parents]:\n",
    "    if (parent / '_config.yml').exists():\n",
    "        project_root = parent\n",
    "        break\n",
    "else:\n",
    "    project_root = Path.cwd().parent.parent\n",
    "\n",
    "sys.path.insert(0, str(project_root))\n",
    "\n",
    "from shared import thinkpython, diagram, jupyturtle, download\n",
    "\n",
    "sys.modules['thinkpython'] = thinkpython\n",
    "sys.modules['diagram'] = diagram\n",
    "sys.modules['jupyturtle'] = jupyturtle\n",
    "sys.modules['download'] = download\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6da7aee2",
   "metadata": {},
   "source": [
    "## Why Object-Oriented Programming?\n",
    "\n",
    "Procedural programs often pass the same data through many separate functions. For example, the `deposit()` and `withdraw()` functions of a banking account management program would have the same parameter (data intake) of `owner_id`, `balance`, and `amount`. \n",
    "\n",
    "```python \n",
    "def deposit(owner, balance, amount):\n",
    "    return owner, balance + amount\n",
    "\n",
    "def withdraw(owner, balance, amount):\n",
    "    if amount > balance:\n",
    "        raise ValueError(\"Insufficient funds\")\n",
    "    return owner, balance - amount\n",
    "```\n",
    "...\n",
    "\n",
    "You may create a file called `banking.py` and throw in the functions. In Python shell, you may do `import banking`, and then you can actually use the functions. \n",
    "\n",
    "```python\n",
    ">>> import banking\n",
    ">>> banking.deposit('Tom', 1000, 100)\n",
    "('Tom', 1100)\n",
    ">>> \n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b7f27bd",
   "metadata": {},
   "source": [
    "This procedural style can work well for small scripts, but once the banking program starts to grow in the number of functions (which will eventually become non-trivial), it becomes harder to keep track of which data (the parameters) belongs together and which functions are allowed to change it.\n",
    "\n",
    "The same problem will occur when we model our programs and systems based on real world things like library management information, student information system, or even game characters.\n",
    "\n",
    "**Object-Oriented Programming (OOP)** helps by organizing related data and behavior (functions/methods) into a **class**, which you then use to create **objects**. \n",
    "\n",
    "Key Benefits of Using OOP include:\n",
    "\n",
    "1. Modularity and Organization: OOP allows you to structure your code by breaking complex systems into smaller, manageable pieces (classes). This makes the code easier to navigate and maintain as projects grow.\n",
    "2. Code Reusability: Through inheritance, you can create a base class and reuse its logic in multiple child classes, adhering to the \"Don't Repeat Yourself\" (DRY) principle.\n",
    "3. Easier Debugging: Because data and the functions that manipulate it are bundled together in objects, it is often easier to isolate and fix errors without affecting the rest of the program.\n",
    "4. Real-World Modeling: OOP makes it intuitive to model real-world entities like \"Users,\" \"BankAccount\" objects, or \"GameCharacters\" by giving them specific properties (attributes) and actions (methods). \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "465f66ad",
   "metadata": {},
   "source": [
    "Python is object-oriented at its core. While Python is multi-paradigm and allows for functional or procedural styles, OOP is its core foundation. In fact, everything in Python is an object, including integers and strings. \n",
    "\n",
    "This can be evidenced by seeing that Python values are already objects because they have methods:\n",
    "\n",
    "| Value | Type (Class) | Example method |\n",
    "|---|---|---|\n",
    "| `7` | `int` | `bit_length()` |\n",
    "| `\"hello\"` | `str` | `upper()`, `split()` |\n",
    "| `[1, 2, 3]` | `list` | `append()`, `sort()` |\n",
    "| `{'a': 1}` | `dict` | `keys()`, `get()` |\n",
    "\n",
    "When you call `[1, 2, 3].sort()`, you are calling a method on a `list` object. In this chapter, we apply the same idea to custom classes.\n",
    "\n",
    "Note the column header **Type (Class)**: in Python 3, the two words mean the same thing. Every class you define *is* a type, and every built-in type like `str` or `list` *is* a class. The terms are used interchangeably throughout this chapter."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "796bfff2",
   "metadata": {},
   "source": [
    "## Classes and Instances\n",
    "\n",
    "A **class** is a blueprint that defines data and behavior. An **instance** is one concrete **object** created from that blueprint.\n",
    "\n",
    "Think of a class as a cookie cutter and each instance as an individual cookie. The cutter defines the shape; every cookie produced from it is a separate object with its own toppings (attributes).\n",
    "\n",
    "| Term | Meaning | Cookie analogy |\n",
    "|---|---|---|\n",
    "| Class | Blueprint / template | The cookie cutter |\n",
    "| Instance | One object made from the class | A specific cookie |\n",
    "| Attribute | Data stored on the instance | Icing color, sprinkles |\n",
    "| Method | Function that belongs to the class | `decorate()`, `eat()` |\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb77d3be",
   "metadata": {},
   "source": [
    "The basic syntax for defining a class in Python looks like this:\n",
    "\n",
    "```python\n",
    "class ClassName:\n",
    "    \"\"\"Optional docstring describing the class.\"\"\"\n",
    "\n",
    "    def __init__(self, attribute1, attribute2):   # constructor\n",
    "        self.attribute1 = attribute1              # store data on the instance\n",
    "        self.attribute2 = attribute2\n",
    "\n",
    "    def method_name(self, other_param):           # instance method\n",
    "        # self always refers to the current instance\n",
    "        return self.attribute1 + other_param\n",
    "```\n",
    "\n",
    "\n",
    "- `class ClassName:` declares a new class (use PascalCase by convention)\n",
    "- `__init__`: the **constructor** that runs automatically when you create a new instance\n",
    "- `self`: the instance itself; Python passes it automatically\n",
    "- `def method_name(self, ...)`: any method that operates on the instance"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d27e2fd3",
   "metadata": {},
   "source": [
    "Here is a simple `Point` class. As you can see, it creates 2D points and can calculate the distance between two points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8808b80b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.911502Z",
     "iopub.status.busy": "2026-04-27T03:21:51.911405Z",
     "iopub.status.idle": "2026-04-27T03:21:51.913081Z",
     "shell.execute_reply": "2026-04-27T03:21:51.912912Z"
    }
   },
   "outputs": [],
   "source": [
    "class Point:\n",
    "    \"\"\"Represent a 2D point with x and y coordinates.\"\"\"\n",
    "\n",
    "    def __init__(self, x, y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "\n",
    "    def distance_to(self, other):\n",
    "        \"\"\"Return Euclidean distance from this point to another Point.\"\"\"\n",
    "        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5\n",
    "\n",
    "### run the code below to see the default string representation of Point objects, \n",
    "### which is not very informative. Then uncomment the __str__ method to provide a \n",
    "### more useful string representation.\n",
    "   \n",
    "    # def __str__(self):\n",
    "    #     return f\"Point({self.x}, {self.y})\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e189b75e",
   "metadata": {},
   "source": [
    "\n",
    "You create instances by calling the class like a function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5320f1dd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.914041Z",
     "iopub.status.busy": "2026-04-27T03:21:51.913980Z",
     "iopub.status.idle": "2026-04-27T03:21:51.915474Z",
     "shell.execute_reply": "2026-04-27T03:21:51.915286Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<__main__.Point object at 0x10eb81be0>\n",
      "<__main__.Point object at 0x10ec08690>\n",
      "5.0\n"
     ]
    }
   ],
   "source": [
    "p = Point(3, 4)   # one point object\n",
    "q = Point(0, 0)   # another point object\n",
    "\n",
    "print(p)\n",
    "print(q)\n",
    "\n",
    "print(p.distance_to(q))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c81a474",
   "metadata": {},
   "source": [
    "Even though the points come from the same class, they are independent objects. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "300eee75",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.916383Z",
     "iopub.status.busy": "2026-04-27T03:21:51.916330Z",
     "iopub.status.idle": "2026-04-27T03:21:51.917720Z",
     "shell.execute_reply": "2026-04-27T03:21:51.917552Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "False\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "r = Point(3, 4)   # a third point object with the same coordinates as p\n",
    "s = Point(3, 4)   # a fourth point object with the same coordinates as p and r\n",
    "print(r is s)\n",
    "print(r == s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "411e9bea",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Classes vs Instances\n",
    "#   1. Define a class `Book` with `title` and `author` in `__init__`.\n",
    "#   2. Create two instances with different titles.\n",
    "#   3. Print each instance's title and author on separate lines.\n",
    "#   4. Print `book1 is book2` to verify they are different objects.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c83d985d",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "class Book:\n",
    "    def __init__(self, title, author):\n",
    "        self.title = title\n",
    "        self.author = author\n",
    "\n",
    "book1 = Book(\"Python Design\", \"A. Chen\")\n",
    "book2 = Book(\"Data Workflows\", \"B. Diaz\")\n",
    "\n",
    "print(book1.title, \"-\", book1.author)\n",
    "print(book2.title, \"-\", book2.author)\n",
    "print(book1 is book2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9d9f3d1",
   "metadata": {},
   "source": [
    "## A First Class\n",
    "\n",
    "Here is a simple sample class: `CalcTwo`, a calculator that performs operations on **two input numbers**. Methods still use **`self`** because they belong to an instance. Now you have a good idea that a class can contain multiple methods."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "63e1d465",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.918661Z",
     "iopub.status.busy": "2026-04-27T03:21:51.918586Z",
     "iopub.status.idle": "2026-04-27T03:21:51.920342Z",
     "shell.execute_reply": "2026-04-27T03:21:51.920199Z"
    }
   },
   "outputs": [],
   "source": [
    "class CalcTwo:\n",
    "    \"\"\"Represents a calculator for operations on two numbers.\"\"\"\n",
    "\n",
    "    def __init__(self):\n",
    "        self.last_result = None  # Keep track of the most recent calculation\n",
    "\n",
    "    def add(self, a, b):\n",
    "        \"\"\"Return a + b.\"\"\"\n",
    "        self.last_result = a + b  # Save result on this instance\n",
    "        return self.last_result\n",
    "\n",
    "    def subtract(self, a, b):\n",
    "        \"\"\"Return a - b.\"\"\"\n",
    "        self.last_result = a - b\n",
    "        return self.last_result\n",
    "\n",
    "    def multiply(self, a, b):\n",
    "        \"\"\"Return a * b.\"\"\"\n",
    "        self.last_result = a * b\n",
    "        return self.last_result\n",
    "\n",
    "    def divide(self, a, b):\n",
    "        \"\"\"Return a / b.\"\"\"\n",
    "        if b == 0:\n",
    "            raise ValueError(\"Cannot divide by zero\")  # Avoid undefined operation\n",
    "        self.last_result = a / b\n",
    "        return self.last_result\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"CalcTwo(last_result={self.last_result})\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "53a320ac",
   "metadata": {},
   "source": [
    "Key parts of the code above:\n",
    "\n",
    "| Part | What it does |\n",
    "|---|---|\n",
    "| `class CalcTwo:` | Declares the class |\n",
    "| `def __init__(self):` | Constructor method; runs automatically when you call `CalcTwo()` |\n",
    "| `def __str__(self):` | Controls what `print()` shows; Python calls it automatically |\n",
    "| **`self`** | <span style=\"color:darkred\">The instance being created or used; Python passes it automatically</span> |\n",
    "| `self.last_result` | Stores the most recent result on this instance |\n",
    "| `def add(self, a, b):` | Returns the sum of two numbers |\n",
    "| `def subtract(self, a, b):` | Returns the difference of two numbers |\n",
    "| `def multiply(self, a, b):` | Returns the product of two numbers |\n",
    "| `def divide(self, a, b):` | Returns the quotient of two numbers |\n",
    "\n",
    "- `__init__()`: The **constructor**, called automatically when a new `CalcTwo` is created.\n",
    "- `__str__()`: Returns a readable string representation of the calculator state.\n",
    "- Arithmetic methods: each method takes two numbers and returns a computed result.\n",
    "- `last_result`: lets each instance remember its own most recent calculation."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39d319da",
   "metadata": {},
   "source": [
    "Now create two instances of `CalcTwo`: `c1` and `c2`. After instance creation, we can:\n",
    "\n",
    "1. access data in the object (e.g., `c1.last_result`)\n",
    "2. make **method calls** with two numbers (e.g., `c1.add(5, 2)`, `c1.subtract(9, 4)`)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2eb9374d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.921337Z",
     "iopub.status.busy": "2026-04-27T03:21:51.921283Z",
     "iopub.status.idle": "2026-04-27T03:21:51.922852Z",
     "shell.execute_reply": "2026-04-27T03:21:51.922688Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "15\n",
      "7\n",
      "24\n",
      "7.0\n",
      "24\n",
      "7.0\n",
      "CalcTwo(last_result=24)\n"
     ]
    }
   ],
   "source": [
    "c1 = CalcTwo()\n",
    "c2 = CalcTwo()\n",
    "\n",
    "print(c1.add(10, 5))           # 15\n",
    "print(c1.subtract(10, 3))      # 7\n",
    "print(c1.multiply(4, 6))       # 24\n",
    "print(c2.divide(21, 3))        # 7.0\n",
    "print(c1.last_result)          # 24 (independent from c2)\n",
    "print(c2.last_result)          # 7.0\n",
    "\n",
    "print(c1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78dc84c4",
   "metadata": {},
   "source": [
    ":::{admonition} The `__init__` Method\n",
    "`__init__` is called a **dunder method** (short for \"double underscore\") because its name starts and ends with two underscores. Python uses dunder methods to hook into built-in behaviors - you define them in your class, and Python calls them automatically at certain times.\n",
    "\n",
    "`__init__` is the **constructor**: Python calls it automatically whenever you create a new instance of a class. Its job is to **initialize** the object's attributes. For example:\n",
    "\n",
    "```python\n",
    "c = CalcTwo()\n",
    "```\n",
    "\n",
    "This creates a new `CalcTwo` object and immediately calls `__init__(self)`, which sets `self.last_result = None`.\n",
    "\n",
    "The first parameter of every method is `self`, which refers to the instance being created or operated on.\n",
    ":::\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d50292f5",
   "metadata": {},
   "source": [
    "### Custom and Built-in Types"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "243a25b6",
   "metadata": {},
   "source": [
    "Let's use two built-in tools that let you inspect objects:\n",
    "\n",
    "- `type(obj)` — returns the class of the object\n",
    "- `isinstance(obj, SomeClass)` — returns `True` if `obj` is an instance of that class (or a subclass)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c8ef72d",
   "metadata": {},
   "source": [
    "Now let's examine the characteristics of `CalcTwo` and compare with Python built-in data types.\n",
    "\n",
    "**Takeaway:** user-defined class instances behave like built-in objects under `type()` and `isinstance()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8ad2da99",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.923802Z",
     "iopub.status.busy": "2026-04-27T03:21:51.923750Z",
     "iopub.status.idle": "2026-04-27T03:21:51.925311Z",
     "shell.execute_reply": "2026-04-27T03:21:51.925157Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class '__main__.CalcTwo'>\n",
      "True\n",
      "True\n",
      "False\n",
      "<class 'str'>\n",
      "True\n",
      "True\n",
      "False\n"
     ]
    }
   ],
   "source": [
    "c = CalcTwo()\n",
    "\n",
    "print(type(c))                      ### <class '__main__.CalcTwo'>\n",
    "print(type(c) is CalcTwo)          ### True\n",
    "print(isinstance(c, CalcTwo))      ### True\n",
    "print(isinstance(c, str))           ### False\n",
    "\n",
    "### Custom types/classes and built-in types work the same way\n",
    "print(type(\"hello\"))                ### <class 'str'>\n",
    "print(type(\"hello\") is str)         ### True\n",
    "print(isinstance(42, int))          ### True\n",
    "print(isinstance(42, float))        ### False"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7d6ef776",
   "metadata": {},
   "source": [
    "### Classes and Methods\n",
    "\n",
    "Python is an **object-oriented language** -- that is, it provides features that support object-oriented programming, which has these defining characteristics:\n",
    "\n",
    "-   Most of the computation is expressed in terms of operations on objects.\n",
    "\n",
    "-   Objects often represent things in the real world, and methods often correspond to the ways things in the real world interact.\n",
    "\n",
    "-   Programs include class and method definitions.\n",
    "\n",
    "For example, consider a `CalcTwo` class and an operation like addition.\n",
    "There is no explicit connection between a standalone function and the class unless we make one.\n",
    "We can make that connection explicit by rewriting a function as a **method**, which is defined inside a class definition."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d28be810",
   "metadata": {},
   "source": [
    "### Defining Methods\n",
    "\n",
    "To start, here is a standalone function that adds two numbers and stores the result on a calculator object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "557f34cd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.926289Z",
     "iopub.status.busy": "2026-04-27T03:21:51.926241Z",
     "iopub.status.idle": "2026-04-27T03:21:51.927448Z",
     "shell.execute_reply": "2026-04-27T03:21:51.927286Z"
    }
   },
   "outputs": [],
   "source": [
    "def add_two(calc, a, b):\n",
    "    calc.last_result = a + b\n",
    "    return calc.last_result"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc151163",
   "metadata": {},
   "source": [
    "To make this behavior a method, we move the function logic inside the class.\n",
    "\n",
    "At the same time, we change the first parameter from `calc` to `self`.\n",
    "This change is not required by syntax, but it is the standard convention in Python classes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "14398fe0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.928326Z",
     "iopub.status.busy": "2026-04-27T03:21:51.928277Z",
     "iopub.status.idle": "2026-04-27T03:21:51.929409Z",
     "shell.execute_reply": "2026-04-27T03:21:51.929268Z"
    }
   },
   "outputs": [],
   "source": [
    "# Method version inside the class (already defined above):\n",
    "#\n",
    "# class CalcTwo:\n",
    "#     def add(self, a, b):\n",
    "#         self.last_result = a + b\n",
    "#         return self.last_result"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0832ef86",
   "metadata": {},
   "source": [
    "To call either version, we first create a `CalcTwo` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "4e39be0d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.930384Z",
     "iopub.status.busy": "2026-04-27T03:21:51.930336Z",
     "iopub.status.idle": "2026-04-27T03:21:51.931529Z",
     "shell.execute_reply": "2026-04-27T03:21:51.931373Z"
    }
   },
   "outputs": [],
   "source": [
    "calc = CalcTwo()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6231a828",
   "metadata": {},
   "source": [
    "Now there are two ways to call the method logic. The first (and less common) way is function-style syntax."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "474799ba",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.932358Z",
     "iopub.status.busy": "2026-04-27T03:21:51.932315Z",
     "iopub.status.idle": "2026-04-27T03:21:51.934305Z",
     "shell.execute_reply": "2026-04-27T03:21:51.934174Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "13"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "CalcTwo.add(calc, 9, 4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e04351f1",
   "metadata": {},
   "source": [
    "In this version, `CalcTwo` is the class name, `add` is the method name, and `calc` is passed explicitly as the first argument.\n",
    "The second (and more idiomatic) way is method syntax:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "0e3bfcab",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.935226Z",
     "iopub.status.busy": "2026-04-27T03:21:51.935168Z",
     "iopub.status.idle": "2026-04-27T03:21:51.936626Z",
     "shell.execute_reply": "2026-04-27T03:21:51.936462Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "13"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "calc.add(9, 4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "342456f0",
   "metadata": {},
   "source": [
    "Both calls return the same result.\n",
    "The receiver object is assigned to the first parameter, so inside the method, `self` refers to the same object as `calc`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "56eec25f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.937458Z",
     "iopub.status.busy": "2026-04-27T03:21:51.937413Z",
     "iopub.status.idle": "2026-04-27T03:21:51.938623Z",
     "shell.execute_reply": "2026-04-27T03:21:51.938478Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "13\n"
     ]
    }
   ],
   "source": [
    "print(calc.last_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "592a372c",
   "metadata": {},
   "source": [
    "In method syntax, `calc` is the object the method is invoked on (the **receiver**).\n",
    "\n",
    "Regardless of syntax, behavior is the same:\n",
    "`CalcTwo.add(calc, 9, 4)` and `calc.add(9, 4)` both use the same method implementation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "8f6b6b79",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.939476Z",
     "iopub.status.busy": "2026-04-27T03:21:51.939426Z",
     "iopub.status.idle": "2026-04-27T03:21:51.940720Z",
     "shell.execute_reply": "2026-04-27T03:21:51.940505Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: A First Class\n",
    "#   Define a class Rectangle with:\n",
    "#   1. __init__(self, width, height) — store both as instance attributes.\n",
    "#   2. __str__ — returns a string like 'Rectangle(3 x 5)'.\n",
    "#   3. area(self) — returns width * height.\n",
    "#   Then create Rectangle(3, 5), print it, and call .area().\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "4c14e492",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.941638Z",
     "iopub.status.busy": "2026-04-27T03:21:51.941588Z",
     "iopub.status.idle": "2026-04-27T03:21:51.943165Z",
     "shell.execute_reply": "2026-04-27T03:21:51.943007Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Rectangle(3 x 5)\n",
      "15\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "class Rectangle:\n",
    "    def __init__(self, width, height):\n",
    "        self.width = width\n",
    "        self.height = height\n",
    "\n",
    "    def __str__(self):\n",
    "        return f'Rectangle({self.width} x {self.height})'\n",
    "\n",
    "    def area(self):\n",
    "        return self.width * self.height\n",
    "\n",
    "r = Rectangle(3, 5)\n",
    "print(r)          # Rectangle(3 x 5)\n",
    "print(r.area())   # 15"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a42bfd4",
   "metadata": {},
   "source": [
    "## More Methods\n",
    "\n",
    "In this section, we build a `BankAccount` class and explore the four\n",
    "special methods Python programmers write most often:\n",
    "\n",
    "| Method | Called by | Purpose |\n",
    "|---|---|---|\n",
    "| `__init__` | `BankAccount(...)` | Initializes attributes when a new object is created |\n",
    "| `__str__` | `str()`, `print()`, f-strings | Human-readable string |\n",
    "| `__repr__` | `repr()`, interactive shell | Unambiguous, ideally reconstructable string |\n",
    "| `@property` | attribute access (`acct.balance`) | Read-only computed attribute |\n",
    "\n",
    "Here is the complete `BankAccount` class. We define it all at once and\n",
    "then walk through each method in detail below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c529c7ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.944029Z",
     "iopub.status.busy": "2026-04-27T03:21:51.943986Z",
     "iopub.status.idle": "2026-04-27T03:21:51.945617Z",
     "shell.execute_reply": "2026-04-27T03:21:51.945449Z"
    }
   },
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"A simple bank account with owner name and a dollar balance.\"\"\"\n",
    "\n",
    "    def __init__(self, owner=\"Unknown\", balance=0.0):\n",
    "        self.owner = owner\n",
    "        self._balance = balance\n",
    "\n",
    "    @property\n",
    "    def balance(self):\n",
    "        \"\"\"Current balance in dollars (read-only).\"\"\"\n",
    "        return self._balance\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        \"\"\"Add amount to the balance and return self.\"\"\"\n",
    "        self._balance += amount\n",
    "        return self\n",
    "\n",
    "    def withdraw(self, amount):\n",
    "        \"\"\"Subtract amount from the balance and return self.\"\"\"\n",
    "        if amount > self._balance:\n",
    "            raise ValueError(\"Insufficient funds\")\n",
    "        self._balance -= amount\n",
    "        return self\n",
    "\n",
    "    def __str__(self):\n",
    "        return f\"BankAccount(owner={self.owner}, balance=${self._balance:.2f})\"\n",
    "\n",
    "    def __repr__(self):\n",
    "        return f\"BankAccount('{self.owner}', {self._balance})\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "110502f3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.946418Z",
     "iopub.status.busy": "2026-04-27T03:21:51.946376Z",
     "iopub.status.idle": "2026-04-27T03:21:51.947515Z",
     "shell.execute_reply": "2026-04-27T03:21:51.947370Z"
    }
   },
   "outputs": [],
   "source": [
    "acct1 = BankAccount(\"Ava\", 945.30)\n",
    "acct2 = BankAccount(\"Max\", 500.00)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2fbe6bba",
   "metadata": {},
   "source": [
    "### The `__init__` Method\n",
    "\n",
    "The most special of the special methods is `__init__`, so-called because it\n",
    "initializes the attributes of a new object. Here is the `__init__` method\n",
    "from our `BankAccount` class:\n",
    "\n",
    "```python\n",
    "    def __init__(self, owner=\"Unknown\", balance=0.0):\n",
    "        self.owner = owner\n",
    "        self._balance = balance\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78515e85",
   "metadata": {},
   "source": [
    "When we instantiate a `BankAccount` object, Python invokes `__init__` automatically\n",
    "and passes along the arguments. So we can create an object and set its initial balance\n",
    "at the same time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "599bb4b4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.959323Z",
     "iopub.status.busy": "2026-04-27T03:21:51.959281Z",
     "iopub.status.idle": "2026-04-27T03:21:51.960485Z",
     "shell.execute_reply": "2026-04-27T03:21:51.960326Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Ava, balance=$94.00)\n"
     ]
    }
   ],
   "source": [
    "account = BankAccount(\"Ava\", 94.00)\n",
    "print(account)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f22ccfa7",
   "metadata": {},
   "source": [
    "Both parameters are optional. If you call `BankAccount` with no arguments,\n",
    "you get the default values (`\"Unknown\"` and `0.0`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c0b4a924",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.961400Z",
     "iopub.status.busy": "2026-04-27T03:21:51.961347Z",
     "iopub.status.idle": "2026-04-27T03:21:51.962581Z",
     "shell.execute_reply": "2026-04-27T03:21:51.962429Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Unknown, balance=$0.00)\n"
     ]
    }
   ],
   "source": [
    "account = BankAccount()\n",
    "print(account)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "944ba44c",
   "metadata": {},
   "source": [
    "If you provide one argument, it overrides `owner`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "40b10f78",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.963406Z",
     "iopub.status.busy": "2026-04-27T03:21:51.963362Z",
     "iopub.status.idle": "2026-04-27T03:21:51.964554Z",
     "shell.execute_reply": "2026-04-27T03:21:51.964395Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankAccount(owner=Sam, balance=$0.00)\n"
     ]
    }
   ],
   "source": [
    "account = BankAccount(\"Sam\")\n",
    "print(account)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4cc47eda",
   "metadata": {},
   "source": [
    "If you provide two arguments, they override `owner` and `balance`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e759bbb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.965455Z",
     "iopub.status.busy": "2026-04-27T03:21:51.965411Z",
     "iopub.status.idle": "2026-04-27T03:21:51.966645Z",
     "shell.execute_reply": "2026-04-27T03:21:51.966466Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankingAccount(owner=Sam, balance=$45.00)\n"
     ]
    }
   ],
   "source": [
    "account = BankAccount(\"Sam\", 45.00)\n",
    "print(account)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89af1327",
   "metadata": {},
   "source": [
    "When I write a new class, I almost always start by writing `__init__`, which\n",
    "makes it easier to create objects, and `__str__`, which is useful for debugging."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4df6ae6",
   "metadata": {},
   "source": [
    "### Properties\n",
    "\n",
    "The `@property` decorator lets you define a method that behaves like an attribute.\n",
    "This is useful when you want controlled read-only (or validated) access to a\n",
    "value stored on the object, *without* changing how callers access it.\n",
    "\n",
    "Here is the `balance` property from our `BankAccount` class:\n",
    "\n",
    "```python\n",
    "    @property\n",
    "    def balance(self):\n",
    "        \"\"\"Current balance in dollars (read-only).\"\"\"\n",
    "        return self._balance\n",
    "```\n",
    "\n",
    "The underscore in `_balance` signals that it is an *internal* attribute — callers\n",
    "should use the `balance` property rather than accessing `_balance` directly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "42572d7e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.957167Z",
     "iopub.status.busy": "2026-04-27T03:21:51.957125Z",
     "iopub.status.idle": "2026-04-27T03:21:51.958431Z",
     "shell.execute_reply": "2026-04-27T03:21:51.958266Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "150.0\n",
      "Error: property 'balance' of 'BankingAccount' object has no setter\n",
      "170.0\n"
     ]
    }
   ],
   "source": [
    "acct = BankAccount(\"Mia\", 150.0)\n",
    "# Access like an attribute — no parentheses needed\n",
    "print(acct.balance)   # 150.0\n",
    "\n",
    "# Attempting to set it raises AttributeError (read-only)\n",
    "try:\n",
    "    acct.balance = 9999\n",
    "except AttributeError as e:\n",
    "    print(f\"Error: {e}\")\n",
    "\n",
    "# Use deposit and withdraw to change the balance\n",
    "acct.deposit(50.0)\n",
    "acct.withdraw(30.0)\n",
    "print(acct.balance)   # 170.0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00cc1003",
   "metadata": {},
   "source": [
    "### The `__str__` Method\n",
    "\n",
    "When you write a class, you can define a method named `__str__` to control\n",
    "how Python converts instances to a string.\n",
    "For example, here is the `__str__` method from our `BankAccount` class:\n",
    "\n",
    "```python\n",
    "    def __str__(self):\n",
    "        return f\"BankAccount(owner={self.owner}, balance=${self._balance:.2f})\"\n",
    "```\n",
    "\n",
    "This method returns a formatted string. You can invoke it directly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "f949934a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.948387Z",
     "iopub.status.busy": "2026-04-27T03:21:51.948327Z",
     "iopub.status.idle": "2026-04-27T03:21:51.949799Z",
     "shell.execute_reply": "2026-04-27T03:21:51.949664Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'BankingAccount(owner=Max, balance=$500.00)'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "acct2.__str__()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4618a6a1",
   "metadata": {},
   "source": [
    "But Python can also invoke it for you.\n",
    "If you use the built-in function `str` to convert a `BankAccount` object to a string, Python uses the `__str__` method in the class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "ea9da0f1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.950668Z",
     "iopub.status.busy": "2026-04-27T03:21:51.950620Z",
     "iopub.status.idle": "2026-04-27T03:21:51.952127Z",
     "shell.execute_reply": "2026-04-27T03:21:51.951966Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'BankingAccount(owner=Max, balance=$500.00)'"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "str(acct2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00a8caca",
   "metadata": {},
   "source": [
    "And it does the same if you print a `BankAccount` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "8dabd633",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.953027Z",
     "iopub.status.busy": "2026-04-27T03:21:51.952982Z",
     "iopub.status.idle": "2026-04-27T03:21:51.954074Z",
     "shell.execute_reply": "2026-04-27T03:21:51.953931Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankingAccount(owner=Max, balance=$500.00)\n"
     ]
    }
   ],
   "source": [
    "print(acct2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6efb95c7",
   "metadata": {},
   "source": [
    "Methods like `__str__` are called **special methods**.\n",
    "You can identify them because their names begin and end with two underscores."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbc71922",
   "metadata": {},
   "source": [
    "### The `__repr__` Method\n",
    "\n",
    "`__repr__` returns a string that ideally looks like a valid Python expression\n",
    "that could recreate the object. It is used in the interactive interpreter and\n",
    "by `repr()`. When `__str__` is not defined, Python falls back to `__repr__`.\n",
    "\n",
    "| Method | Called by | Purpose |\n",
    "|---|---|---|\n",
    "| `__str__` | `str()`, `print()`, f-strings | Human-readable output |\n",
    "| `__repr__` | `repr()`, interactive shell | Unambiguous, ideally reconstructable |\n",
    "\n",
    "Here is the `__repr__` method from our `BankAccount` class:\n",
    "\n",
    "```python\n",
    "    def __repr__(self):\n",
    "        return f\"BankAccount('{self.owner}', {self._balance})\"\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ddabbc29",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.954966Z",
     "iopub.status.busy": "2026-04-27T03:21:51.954919Z",
     "iopub.status.idle": "2026-04-27T03:21:51.956282Z",
     "shell.execute_reply": "2026-04-27T03:21:51.956113Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankingAccount(owner=Ava, balance=$945.30)\n",
      "BankingAccount('Ava', 945.3)\n"
     ]
    }
   ],
   "source": [
    "acct = BankAccount(\"Ava\", 945.30)\n",
    "print(str(acct))    # BankAccount(owner=Ava, balance=$945.30)\n",
    "print(repr(acct))   # BankAccount('Ava', 945.3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07c01dd6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.967511Z",
     "iopub.status.busy": "2026-04-27T03:21:51.967455Z",
     "iopub.status.idle": "2026-04-27T03:21:51.968722Z",
     "shell.execute_reply": "2026-04-27T03:21:51.968560Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Independent Class Practice\n",
    "#   Build a new class without using `BankAccount`.\n",
    "#   1. Define a class `TaskList` with `owner` and `tasks` in `__init__`.\n",
    "#      Use an empty list for `tasks` when no list is provided.\n",
    "#   2. Add `add_task(task)` to append one task to the list.\n",
    "#   3. Add `task_count()` to return how many tasks are stored.\n",
    "#   4. Create t1 and t2 with different owners, add tasks to each.\n",
    "#   5. Print each owner with task_count(), then compare counts.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e4e0d5a9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-27T03:21:51.969554Z",
     "iopub.status.busy": "2026-04-27T03:21:51.969513Z",
     "iopub.status.idle": "2026-04-27T03:21:51.970794Z",
     "shell.execute_reply": "2026-04-27T03:21:51.970654Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "BankingAccount(owner=Lia, balance=$123.00)\n",
      "BankingAccount(owner=Noah, balance=$98.00)\n",
      "173.0\n",
      "78.0\n",
      "True\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "class TaskList:\n",
    "    def __init__(self, owner, tasks=None):\n",
    "        self.owner = owner\n",
    "        self.tasks = tasks if tasks is not None else []\n",
    "\n",
    "    def add_task(self, task):\n",
    "        self.tasks.append(task)\n",
    "\n",
    "    def task_count(self):\n",
    "        return len(self.tasks)\n",
    "\n",
    "t1 = TaskList(\"Lia\")\n",
    "t2 = TaskList(\"Noah\")\n",
    "t1.add_task(\"Draft proposal\")\n",
    "t1.add_task(\"Email team\")\n",
    "t2.add_task(\"Review notes\")\n",
    "\n",
    "print(t1.owner, t1.task_count())\n",
    "print(t2.owner, t2.task_count())\n",
    "print(t1.task_count() > t2.task_count())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "255ef08d",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "In this section we covered:\n",
    "\n",
    "- **Programmer-defined types**: creating classes and working with object attributes\n",
    "- **Classes and methods**: `__init__`, `__str__`, `__repr__`, `@property`\n",
    "\n",
    "Next: **The Four Pillars of OOP** — applying encapsulation, polymorphism, inheritance, and abstraction with focused examples.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3beb4e83",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Chapter Review\n",
    "#   1. Define a class `Wallet` with attributes `owner` and `cash`.\n",
    "#   2. Add a method `spend(amount)` that subtracts from `cash` and\n",
    "#      raises `ValueError` if amount is greater than available cash.\n",
    "#   3. Create `w = Wallet(\"Kai\", 40)` and call `w.spend(15)`.\n",
    "#   4. Print the remaining cash.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a18fd537",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "25\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "class Wallet:\n",
    "    def __init__(self, owner, cash):\n",
    "        self.owner = owner\n",
    "        self.cash = cash\n",
    "\n",
    "    def spend(self, amount):\n",
    "        if amount > self.cash:\n",
    "            raise ValueError(\"Not enough cash\")\n",
    "        self.cash -= amount\n",
    "\n",
    "w = Wallet(\"Kai\", 40)\n",
    "w.spend(15)\n",
    "print(w.cash)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5910a1c2",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": ".venv (3.13.7)",
   "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.13.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
