{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "new-4c8a032b",
   "metadata": {},
   "source": [
    "# The Four Pillars of OOP"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "0f6e77dd",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.033699Z",
     "start_time": "2026-04-29T07:19:15.016769Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.292137Z",
     "iopub.status.busy": "2026-04-26T20:10:23.292061Z",
     "iopub.status.idle": "2026-04-26T20:10:23.479413Z",
     "shell.execute_reply": "2026-04-26T20:10:23.479145Z"
    },
    "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": "new-b65c5afe",
   "metadata": {},
   "source": [
    "Object-oriented programming is often organized around four big ideas known as the **four pillars of OOP**. These pillars are not strict rules that every class must follow in the same way. Instead, they are design principles that help us build programs that are easier to understand, extend, and maintain.\n",
    "\n",
    "When people say a program is \"object-oriented,\" they usually mean that classes and objects are used to organize related data together with the operations that act on that data. The four pillars give us a vocabulary for describing how that organization works.\n",
    "\n",
    "As you read each section, keep three questions in mind:\n",
    "\n",
    "- What problem does this pillar solve?\n",
    "- What does the Python code gain by using it?\n",
    "- What would the code look like without it?\n",
    "\n",
    "| Pillar | Core idea |\n",
    "|--------|-----------|\n",
    "| **Encapsulation** | Bundle data and behavior together, and protect internal state |\n",
    "| **Inheritance** | Build a new class by reusing and extending an existing one |\n",
    "| **Polymorphism** | Use a common interface even when different classes behave differently |\n",
    "| **Abstraction** | Show what an object can do without exposing every implementation detail |\n",
    "\n",
    "In practice, these ideas overlap. A class might use encapsulation to protect its data, inheritance to reuse behavior, polymorphism to work with many object types, and abstraction to present a simple public interface. We study them separately here so each one is easier to see.\n",
    "\n",
    "Encapsulation comes first because it is one of the most immediate and practical ideas in everyday Python class design."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "new-462c4355",
   "metadata": {},
   "source": [
    "## Encapsulation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf7f2318",
   "metadata": {},
   "source": [
    "```{raw} html\n",
    "<div style=\"float: right; margin: 0 0 1rem 1.5rem; width: 315px;\">\n",
    "  <!-- <iframe width=\"315\" height=\"560\" -->\n",
    "  <iframe width=\"252\" height=\"448\"\n",
    "    src=\"https://www.youtube.com/embed/KZBX0NytylM\"\n",
    "    frameborder=\"0\" allowfullscreen>\n",
    "  </iframe>\n",
    "</div>\n",
    "```\n",
    "\n",
    "**Encapsulation** means that an object manages its own state (data at the point of time) by keeping related data and behavior together and by controlling how that data is accessed or changed.\n",
    "\n",
    "This idea has two parts:\n",
    "\n",
    "- A class bundles attributes and methods into one coherent unit (Class).\n",
    "- A class protects its internal state so values are changed in deliberate, valid ways rather than arbitrarily from outside the object.\n",
    "\n",
    "The idea of encapsulation are connected to some critical concepts in software engineering:\n",
    "\n",
    "- **Bundling**: Grouping data and methods into one unit (Classes). Python does this perfectly.\n",
    "- **Information Hiding**: Restricting access to internal state. Python does this via convention and consensus. \n",
    "- **Abstraction**: the class can expose a simple public interface while hiding implementation details.\n",
    "- **Data integrity**: the class can validate updates and keep the object in a consistent state.\n",
    "\n",
    "In Python, `encapsulation` is based on conventions and interface design rather than strict access control such as the `private` keyword to block access as in Java. This is achieved by using **underscores** to signal intent to other developers: \n",
    "\n",
    "- `_variable` (**Protected**): A single underscore tells other programmers, \"internal use by convention\", meaning \"This is internal; use it at your own risk because it might change.\" Python won't stop you from accessing it, but doing so is considered bad practice.\n",
    "- `__variable` (**Private**): A double underscore triggers **Name Mangling**. Python renames the attribute to `_ClassName__variable`, which makes accidental external access less likely and thus prevents accidental overwriting in subclasses. It's still accessible if you know the new name, but it's a \"keep out\" sign for the developer. \n",
    "\n",
    "The main goal of encapsulation is to make callers depend on the object's **public interface**, not on the details of how the object stores or computes its data.\n",
    "\n",
    "```{raw} html\n",
    "<div style=\"clear: both;\"></div>\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b941422",
   "metadata": {},
   "source": [
    "Consider a payroll system. An employee's name is usually safe to expose directly, but salary is more sensitive and should be accessed through a controlled interface. The example below uses a double-underscore attribute for the stored salary and a read-only `@property` for safe access."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0d7bfd2",
   "metadata": {},
   "source": [
    ":::{admonition} @property == getter\n",
    ":class: dropdown\n",
    "If you have used Java or C#, you can think of `@property` as serving a role similar to a getter method. It lets callers access a value through a public interface while the class still controls how that value is computed or exposed.\n",
    "\n",
    "One important difference is syntax: in Python, a property is accessed like an attribute, so we write `emp.salary` rather than something like `emp.getSalary()`.\n",
    "\n",
    "The most common use of @property in practice is actually the read-only computed value pattern with no setter:\n",
    "\n",
    "@property\n",
    "def bmi(self):\n",
    "    return self.weight / (self.height ** 2)\n",
    "\n",
    ":::"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "new-847d8503",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.059137Z",
     "start_time": "2026-04-29T07:19:15.040754Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.481163Z",
     "iopub.status.busy": "2026-04-26T20:10:23.481056Z",
     "iopub.status.idle": "2026-04-26T20:10:23.482830Z",
     "shell.execute_reply": "2026-04-26T20:10:23.482648Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Frederick\n",
      "50000\n"
     ]
    }
   ],
   "source": [
    "class Employee:\n",
    "    def __init__(self, name, salary):\n",
    "        if salary < 0:              ### simple validation check salary is not negative\n",
    "            raise ValueError('Salary cannot be negative')\n",
    "\n",
    "        self.name = name            ### public attribute\n",
    "        self.__salary = salary      ### double underscore triggers name mangling; this is meant\n",
    "                                    ### for internal use and should not be accessed directly\n",
    "                                    ### from outside the class.\n",
    "\n",
    "    @property                       ### user property lets us expose salary through a controlled read-only interface.\n",
    "    def salary(self):               ### \n",
    "        return self.__salary\n",
    "\n",
    "emp = Employee('Frederick', 50000)\n",
    "print(emp.name)\n",
    "print(emp.salary)   ### this calls the salary method, but we access it like an attribute (no parentheses)\n",
    "                    #   because of the @property decorator."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b5248cf",
   "metadata": {},
   "source": [
    "The same encapsulation idea appears in banking systems. A bank account should not allow outside code to change the balance arbitrarily; instead, the class should expose controlled operations such as `deposit` and `withdraw`, along with a read-only way to inspect the current balance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "3f8e20e0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.077891Z",
     "start_time": "2026-04-29T07:19:15.061551Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.484051Z",
     "iopub.status.busy": "2026-04-26T20:10:23.483991Z",
     "iopub.status.idle": "2026-04-26T20:10:23.486229Z",
     "shell.execute_reply": "2026-04-26T20:10:23.486056Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "120\n"
     ]
    }
   ],
   "source": [
    "class BankAccount:\n",
    "    def __init__(self, owner, balance=0):\n",
    "        if balance < 0:\n",
    "            raise ValueError('Starting balance cannot be negative')\n",
    "\n",
    "        self.owner = owner\n",
    "        self._balance = balance     ### single underscore is a convention indicating that\n",
    "                                    ### this attribute is intended for internal use, but it\n",
    "                                    ### can still be accessed from outside the class.\n",
    "\n",
    "    @property\n",
    "    def balance(self):              ### this property allows us to access the balance in a \n",
    "        return self._balance        ### controlled way, without allowing direct modification.\n",
    "\n",
    "    def deposit(self, amount):      ### business logic for depositing money\n",
    "        if amount <= 0:             ### simple validation check to ensure deposit amount is positive\n",
    "            raise ValueError('Deposit must be positive')\n",
    "        self._balance += amount\n",
    "\n",
    "    def withdraw(self, amount):     ### business logic for withdrawing money    \n",
    "        if amount <= 0:             ### simple validation check to ensure withdrawal amount is positive\n",
    "            raise ValueError('Withdrawal must be positive')\n",
    "        if amount > self._balance:\n",
    "            raise ValueError('Insufficient funds')\n",
    "        self._balance -= amount\n",
    "\n",
    "acct = BankAccount('Alice', 100)\n",
    "acct.deposit(50)\n",
    "acct.withdraw(30)\n",
    "print(acct.balance)  # 120"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e11c6620",
   "metadata": {},
   "source": [
    "### Access Level Conventions\n",
    "\n",
    "Python does not enforce strict access modifiers like Java or C++. Instead, it uses naming patterns to communicate intent:\n",
    "\n",
    "- `name`: public\n",
    "- `_name`: internal use by convention\n",
    "- `__name`: triggers name mangling to reduce accidental access or accidental override in subclasses\n",
    "\n",
    "That last point matters: double underscore is more than a social convention. Python actually rewrites the attribute name internally. It is still not true private security, but it does provide stronger protection against accidental misuse than a single underscore.\n",
    "\n",
    "In the examples above:\n",
    "\n",
    "- `Employee.__salary` shows double-underscore name mangling.\n",
    "- `BankAccount._balance` shows an internal attribute exposed safely through methods and `@property`.\n",
    "\n",
    "See the note below for how name mangling works."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09fb6061",
   "metadata": {},
   "source": [
    ":::{admonition} Name Mangling in Python\n",
    ":class: dropdown\n",
    "`__attribute` (double underscore) triggers **name mangling**, where Python rewrites the name internally to `_ClassName__attribute`.\n",
    "\n",
    "This is designed to prevent accidental access or accidental overrides in subclasses, not to provide true security.\n",
    "\n",
    "Example: `self.__salary` inside `Employee` is stored as `_Employee__salary`.\n",
    "\n",
    "As for single leading underscore `_name`, it is a convention meaning \"this is internal / don't touch it from outside the class.\" Python does nothing special: it's just a social contract between developers. The attribute is fully accessible, just signaled as private by convention.\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "227ff1ec",
   "metadata": {},
   "source": [
    "## Polymorphism\n",
    "\n",
    "**Polymorphism** means different object types can respond to the same method call in their own way.\n",
    "\n",
    "In the banking example below, each account type implements the same method, `month_end()`.\n",
    "The caller does not need `if/elif` logic for each account type — it can just call one method name on all accounts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "3a940b86",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.089093Z",
     "start_time": "2026-04-29T07:19:15.079822Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.487240Z",
     "iopub.status.busy": "2026-04-26T20:10:23.487172Z",
     "iopub.status.idle": "2026-04-26T20:10:23.489233Z",
     "shell.execute_reply": "2026-04-26T20:10:23.489048Z"
    }
   },
   "outputs": [],
   "source": [
    "class CheckingAccount:\n",
    "    def __init__(self, owner, balance):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "\n",
    "    def month_end(self):\n",
    "        self.balance -= 10   # monthly service fee\n",
    "\n",
    "\n",
    "class SavingsAccount:\n",
    "    def __init__(self, owner, balance, rate=0.01):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "        self.rate = rate\n",
    "\n",
    "    def month_end(self):\n",
    "        self.balance += self.balance * self.rate   # monthly interest\n",
    "\n",
    "\n",
    "class LoanAccount:\n",
    "    def __init__(self, owner, balance, rate=0.015):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "        self.rate = rate\n",
    "\n",
    "    def month_end(self):\n",
    "        self.balance += self.balance * self.rate   # interest on amount owed\n",
    "\n",
    "\n",
    "accounts = [                        ### suppose we have a list of different types of accounts\n",
    "    CheckingAccount('Alice', 1200), ### we can call month_end on all of them without worrying about the specific type of account.\n",
    "    SavingsAccount('Bob', 5000),\n",
    "    LoanAccount('Charlie', 2000),\n",
    "]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "771862a7",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.097092Z",
     "start_time": "2026-04-29T07:19:15.089593Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.490226Z",
     "iopub.status.busy": "2026-04-26T20:10:23.490176Z",
     "iopub.status.idle": "2026-04-26T20:10:23.491454Z",
     "shell.execute_reply": "2026-04-26T20:10:23.491264Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Before month-end:\n",
      "Alice: 1200.00\n",
      "Bob: 5000.00\n",
      "Charlie: 2000.00\n"
     ]
    }
   ],
   "source": [
    "print('Before month-end:')\n",
    "for account in accounts:\n",
    "    print(f'{account.owner}: {account.balance:.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7e155bb",
   "metadata": {},
   "source": [
    "All objects in `accounts` provide `month_end()`, so we can apply month-end updates with one uniform loop.\n",
    "No type-specific `if/elif` branches are needed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "62ae4907",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.107462Z",
     "start_time": "2026-04-29T07:19:15.101426Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.492378Z",
     "iopub.status.busy": "2026-04-26T20:10:23.492329Z",
     "iopub.status.idle": "2026-04-26T20:10:23.493622Z",
     "shell.execute_reply": "2026-04-26T20:10:23.493444Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "After month-end:\n",
      "Alice: 1190.00\n",
      "Bob: 5050.00\n",
      "Charlie: 2030.00\n"
     ]
    }
   ],
   "source": [
    "for account in accounts:\n",
    "    account.month_end()   # polymorphic call\n",
    "\n",
    "print('\\nAfter month-end:')\n",
    "for account in accounts:\n",
    "    print(f'{account.owner}: {account.balance:.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69c8440f",
   "metadata": {},
   "source": [
    "In this loop, `account` can be a `CheckingAccount`, `SavingsAccount`, or `LoanAccount`,\n",
    "but the caller code is identical:\n",
    "\n",
    "`account.month_end()`\n",
    "\n",
    "Python dispatches to the correct **class-specific** implementation at runtime.\n",
    "That is the core idea of **polymorphism**: one interface, many behaviors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "74ee140005db2bee",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.118090Z",
     "start_time": "2026-04-29T07:19:15.113884Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.526144Z",
     "iopub.status.busy": "2026-04-26T20:10:23.526100Z",
     "iopub.status.idle": "2026-04-26T20:10:23.527284Z",
     "shell.execute_reply": "2026-04-26T20:10:23.527134Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Banking Polymorphism\n",
    "#   The classes CheckingAccount, SavingsAccount, and LoanAccount are already defined above.\n",
    "#\n",
    "#   1. Define a new class InvestmentAccount with:\n",
    "#         - __init__(self, owner, balance, rate=0.02)\n",
    "#         - month_end(self) that applies:  self.balance += self.balance * self.rate\n",
    "#\n",
    "#   2. Create one instance of each of the four account types:\n",
    "#         CheckingAccount('Alice', 1200)\n",
    "#         SavingsAccount('Bob', 5000)\n",
    "#         LoanAccount('Charlie', 2000)\n",
    "#         InvestmentAccount('Diana', 3000, 0.02)\n",
    "#\n",
    "#   3. Put all four in a list called accounts.\n",
    "#   4. Loop over accounts and call month_end() on each one.\n",
    "#   5. Loop again and print each owner's updated balance.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "b236ea9676058ea8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.122902Z",
     "start_time": "2026-04-29T07:19:15.118349Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.528171Z",
     "iopub.status.busy": "2026-04-26T20:10:23.528115Z",
     "iopub.status.idle": "2026-04-26T20:10:23.529791Z",
     "shell.execute_reply": "2026-04-26T20:10:23.529597Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Alice: 1190.00\n",
      "Bob: 5050.00\n",
      "Charlie: 2030.00\n",
      "Diana: 3060.00\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "class InvestmentAccount:\n",
    "    def __init__(self, owner, balance, rate=0.02):\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "        self.rate = rate\n",
    "\n",
    "    def month_end(self):\n",
    "        self.balance += self.balance * self.rate\n",
    "\n",
    "accounts = [\n",
    "    CheckingAccount('Alice', 1200),\n",
    "    SavingsAccount('Bob', 5000),\n",
    "    LoanAccount('Charlie', 2000),\n",
    "    InvestmentAccount('Diana', 3000, 0.02),\n",
    "]\n",
    "\n",
    "for account in accounts:\n",
    "    account.month_end()\n",
    "\n",
    "for account in accounts:\n",
    "    print(f'{account.owner}: {account.balance:.2f}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ced31782",
   "metadata": {
    "tags": []
   },
   "source": [
    "## Inheritance\n",
    "\n",
    "The language feature most often associated with object-oriented programming is **inheritance**. Inheritance lets us define a new class as a modified or specialized version of an existing class.\n",
    "\n",
    "When inheritance is used well, it helps us avoid rewriting code that is already correct in the parent class. The child class reuses what it needs and adds or overrides only the parts that make it different.\n",
    "\n",
    "The examples in this section use bank accounts. That domain is close to the kinds of systems used in business settings, but the main idea is general: _**a child class can reuse behavior from a parent class and then extend it for a more specific purpose**_."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "4232fb35",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.150900Z",
     "start_time": "2026-04-29T07:19:15.127311Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.496684Z",
     "iopub.status.busy": "2026-04-26T20:10:23.496598Z",
     "iopub.status.idle": "2026-04-26T20:10:23.499646Z",
     "shell.execute_reply": "2026-04-26T20:10:23.499461Z"
    }
   },
   "outputs": [],
   "source": [
    "class BankAccount:\n",
    "    \"\"\"Represents a bank account with an owner, balance, and account number.\"\"\"\n",
    "\n",
    "    def __init__(self, account_number, owner, balance=0.0):\n",
    "        self.account_number = account_number\n",
    "        self.owner = owner\n",
    "        self.balance = balance\n",
    "\n",
    "    def __str__(self):\n",
    "        return f'{self.account_number}: {self.owner} - ${self.balance:.2f}'\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        if amount <= 0:\n",
    "            raise ValueError('Deposit must be positive')\n",
    "        self.balance += amount\n",
    "\n",
    "    def withdraw(self, amount):\n",
    "        if amount <= 0:\n",
    "            raise ValueError('Withdrawal must be positive')\n",
    "        if amount > self.balance:\n",
    "            raise ValueError('Insufficient funds')\n",
    "        self.balance -= amount\n",
    "\n",
    "    def transfer_to(self, other_account, amount):\n",
    "        if not isinstance(other_account, BankAccount):\n",
    "            raise TypeError('Transfers require another BankAccount')\n",
    "        self.withdraw(amount)\n",
    "        other_account.deposit(amount)\n",
    "\n",
    "\n",
    "### This base class will be extended by more specialized account types below."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd9ee184",
   "metadata": {},
   "source": [
    "A subclass is defined with the same `class` statement as any other class. The difference is that the parent class name appears in parentheses after the subclass name."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0502961b",
   "metadata": {},
   "source": [
    "### Parents and children\n",
    "\n",
    "Inheritance is the ability to define a new class that is a modified version of an existing class.\n",
    "A useful banking example is a `SavingsAccount`, which is a specific kind of `BankAccount`.\n",
    "\n",
    "* A savings account is similar to a general bank account because it still has an owner, an account number, a balance, and common operations such as deposit and withdrawal; which we may want to reuse instead of defining them again.\n",
    "\n",
    "* A savings account is also different because it has behavior that does not apply to every account type. For example, it may earn interest at the end of each month; which we may need to add to the child class.\n",
    "\n",
    "This relationship between classes, where one is a **specialized** version of another, lends itself naturally to inheritance. To define a new class that is based on an existing class, the name of the existing class is placed in parentheses.\n",
    "\n",
    "The basic pattern for Python syntax for inheritance is:\n",
    "\n",
    "```python\n",
    "class Parent:\n",
    "    def __init__(self, x):\n",
    "        self.x = x\n",
    "\n",
    "class Child(Parent):\n",
    "    def __init__(self, x, y):\n",
    "        super().__init__(x)   # call Parent.__init__\n",
    "        self.y = y\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71c06d43",
   "metadata": {},
   "source": [
    ":::{admonition} `super()`\n",
    ":class: dropdown\n",
    "`super()` returns a proxy object that makes it possible to call a method from a parent class without naming that parent explicitly. This is especially useful in `__init__`, where the child class often relies on the parent class to set up shared state before adding child-specific attributes.\n",
    "\n",
    "Example in constructors:\n",
    "\n",
    "```python\n",
    "class Animal:\n",
    "    def __init__(self, name, sound):\n",
    "        self.name = name\n",
    "        self.sound = sound\n",
    "\n",
    "    def speak(self):\n",
    "        return f'{self.name} says {self.sound}!'\n",
    "\n",
    "class Dog(Animal):\n",
    "    def __init__(self, name, breed):\n",
    "        super().__init__(name, sound='Woof')  # call parent __init__ first\n",
    "        self.breed = breed                    # then add child-specific data\n",
    "\n",
    "    def info(self):\n",
    "        return f'{self.name} is a {self.breed}'\n",
    "\n",
    "d = Dog('Rex', 'Labrador')\n",
    "print(d.speak())   # inherited behavior uses parent setup\n",
    "print(d.info())    # child-specific behavior\n",
    "```\n",
    "\n",
    "There is also a pattern for **methods overriding**:\n",
    "\n",
    "```python\n",
    "class Parent:\n",
    "    def greet(self):\n",
    "        return \"Hello\"\n",
    "\n",
    "class Child(Parent):            ### inheritance \n",
    "    def greet(self):            ### same method\n",
    "        return super().greet() + \", from Child\"\n",
    "```\n",
    ":::\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61923c48",
   "metadata": {},
   "source": [
    "In this pattern, ChildClass is the subclass (child), and ParentClass is the superclass (parent). To create an inheritance relation for our example below (`BankAccount` is the `parent` class and `SavingsAccount` is the `child` class), \n",
    "\n",
    "```python\n",
    "class SavingsAccount(BankAccount):\n",
    "    pass                        ### no code needed but can't leave it blank\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "965ddccd",
   "metadata": {},
   "source": [
    "Now let's define `SavingsAccount` class as a child class of `BankAccount`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "f39fc598",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.160174Z",
     "start_time": "2026-04-29T07:19:15.155314Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.500734Z",
     "iopub.status.busy": "2026-04-26T20:10:23.500682Z",
     "iopub.status.idle": "2026-04-26T20:10:23.501904Z",
     "shell.execute_reply": "2026-04-26T20:10:23.501728Z"
    }
   },
   "outputs": [],
   "source": [
    "class SavingsAccount(BankAccount):\n",
    "    \"\"\"Represents a savings account.\"\"\"\n",
    "\n",
    "    def __init__(self, account_number, owner, balance=0.0, rate=0.01):\n",
    "        super().__init__(account_number, owner, balance)    ### # call Parent.__init__ to initialize the common attributes\n",
    "        self.rate = rate    ### extra attribute specific to SavingsAccount"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "339295cd",
   "metadata": {},
   "source": [
    "This definition indicates that `SavingsAccount` inherits from `BankAccount`, which means that `SavingsAccount` objects can access methods defined in `BankAccount`, like `deposit` and `withdraw`.\n",
    "\n",
    "`SavingsAccount` also inherits `__init__` from `BankAccount`, but if we define `__init__` in the `SavingsAccount` class, it overrides the one in the `BankAccount` class.\n",
    "\n",
    "Note that:\n",
    "\n",
    "- `SavingsAccount` is a subclass of `BankAccount` because `BankAccount` is in parentheses of the header of `SavingsAccount`.\n",
    "- there is a `__init__` constructor here and it has all the parameters the parent class has and an additional parameter `rate=`.\n",
    "- there is a built-in function call `super()` to initialize the constructor of the parent `BankAccount` class.\n",
    "- we then initialize the specialized attribute: `rate`, which does not exist in the parent class.\n",
    "-\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b6a763a",
   "metadata": {},
   "source": [
    "This version of `__init__` takes the same basic account information as the parent class and adds an interest rate.\n",
    "When a `SavingsAccount` is created, Python invokes this method, but `super().__init__(...)` reuses the parent setup so the shared attributes do not have to be written again."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "8de8cff4",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.169531Z",
     "start_time": "2026-04-29T07:19:15.161222Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.505179Z",
     "iopub.status.busy": "2026-04-26T20:10:23.505120Z",
     "iopub.status.idle": "2026-04-26T20:10:23.507153Z",
     "shell.execute_reply": "2026-04-26T20:10:23.506996Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.02\n"
     ]
    }
   ],
   "source": [
    "savings = SavingsAccount('S-1001', 'Ava', 1200.00, rate=0.02)\n",
    "print(savings.rate)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1e2a67d",
   "metadata": {},
   "source": [
    "Because `SavingsAccount` inherits from `BankAccount`, it can use methods defined in the parent class without redefining them. The next example calls `deposit` method, which was written once in `BankAccount` and then reused by the child class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "9f582ce0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.182801Z",
     "start_time": "2026-04-29T07:19:15.171099Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.508081Z",
     "iopub.status.busy": "2026-04-26T20:10:23.508024Z",
     "iopub.status.idle": "2026-04-26T20:10:23.509426Z",
     "shell.execute_reply": "2026-04-26T20:10:23.509265Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "S-1001: Ava - $1800.00\n",
      "True\n",
      "True\n"
     ]
    }
   ],
   "source": [
    "savings.deposit(300.00)                 ### inherited method from BankAccount\n",
    "print(savings)\n",
    "print(isinstance(savings, SavingsAccount))\n",
    "print(isinstance(savings, BankAccount))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc2ce06b",
   "metadata": {},
   "source": [
    "The parent class also defines a method that is shared by every kind of bank account. A transfer operation belongs in the parent class because it is useful for a regular bank account, a savings account, and any future subclass that behaves like a bank account."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16e6c404",
   "metadata": {},
   "source": [
    "This method is defined once in the parent class, but it can be used by any child class that inherits from `BankAccount`. That is one of the main advantages of inheritance: common behavior lives in one place instead of being duplicated across several related classes."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e648a722",
   "metadata": {},
   "source": [
    "A child class is often called a ***subclass***, and the parent class is often called a ***superclass***. In this example, `SavingsAccount` is a subclass of `BankAccount`, and `BankAccount` is a superclass of `SavingsAccount`.\n",
    "\n",
    "A `SavingsAccount` should be usable anywhere a `BankAccount` is expected because it still supports the same core account behaviors. That idea makes inheritance practical: the parent class defines what related objects have in common, and the child class adds what makes it special."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e80873dd",
   "metadata": {},
   "source": [
    "### Specialization\n",
    "\n",
    "Inheritance becomes especially useful when subclasses need to keep the basic behavior of the parent class but also add rules of their own.\n",
    "A `BusinessAccount`, for example, is still a bank account, but it may charge a service fee for certain withdrawals. That makes it a good example of specialization."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "b9e949cf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.188434Z",
     "start_time": "2026-04-29T07:19:15.184329Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.512652Z",
     "iopub.status.busy": "2026-04-26T20:10:23.512609Z",
     "iopub.status.idle": "2026-04-26T20:10:23.513857Z",
     "shell.execute_reply": "2026-04-26T20:10:23.513679Z"
    }
   },
   "outputs": [],
   "source": [
    "class BusinessAccount(BankAccount):\n",
    "    \"\"\"Represents a business checking account.\"\"\"\n",
    "\n",
    "    def __init__(self, account_number, owner, balance=0.0, transaction_fee=5.0):\n",
    "        super().__init__(account_number, owner, balance)\n",
    "        self.transaction_fee = transaction_fee\n",
    "\n",
    "    def withdraw_with_fee(self, amount):\n",
    "        total = amount + self.transaction_fee\n",
    "        self.withdraw(total)\n",
    "        return total"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c038717",
   "metadata": {},
   "source": [
    "The `withdraw_with_fee` method deducts both the requested withdrawal amount and the transaction fee from the account balance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "0586b764",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.198012Z",
     "start_time": "2026-04-29T07:19:15.189219Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.514847Z",
     "iopub.status.busy": "2026-04-26T20:10:23.514778Z",
     "iopub.status.idle": "2026-04-26T20:10:23.516375Z",
     "shell.execute_reply": "2026-04-26T20:10:23.516219Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Withdrawn: $200.00, Fee: $5.00, Total deducted: $205.00\n",
      "B-1001: Acme Corp - $795.00\n"
     ]
    }
   ],
   "source": [
    "biz = BusinessAccount('B-1001', 'Acme Corp', 1000.0)    ### 1000 in initial balance\n",
    "total = biz.withdraw_with_fee(200)\n",
    "print(f'Withdrawn: $200.00, Fee: ${biz.transaction_fee:.2f}, Total deducted: ${total:.2f}')\n",
    "print(biz)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3a7820d",
   "metadata": {},
   "source": [
    "The `withdraw_with_fee` method belongs in the child class because the extra fee is specific to business accounts, not to every kind of bank account."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea7cb85216c27c29",
   "metadata": {},
   "source": [
    "On the other hand, the `transfer_to()` method is in the superclass so that the subclasses can use them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "227bef61",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.207678Z",
     "start_time": "2026-04-29T07:19:15.199368Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.519502Z",
     "iopub.status.busy": "2026-04-26T20:10:23.519455Z",
     "iopub.status.idle": "2026-04-26T20:10:23.520666Z",
     "shell.execute_reply": "2026-04-26T20:10:23.520520Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "B-2001: Northwind Supply - $4600.00\n",
      "S-2002: Mina - $2200.00\n"
     ]
    }
   ],
   "source": [
    "business = BusinessAccount('B-2001', 'Northwind Supply', 5000.00, transaction_fee=12.50)\n",
    "personal = SavingsAccount('S-2002', 'Mina', 1800.00, rate=0.03)\n",
    "\n",
    "business.transfer_to(personal, 400.00)\n",
    "print(business)\n",
    "print(personal)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1933ca904c358746",
   "metadata": {},
   "source": [
    "Now we can use `withdraw_with_fee` on the same account to apply a fee-based withdrawal."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a16dc6674bfa2b33",
   "metadata": {},
   "source": [
    "Now let's invoke the `business.withdraw_with_fee()` method again; this time without specifying the transaction fee."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "f7d9275a",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.219965Z",
     "start_time": "2026-04-29T07:19:15.211858Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.521555Z",
     "iopub.status.busy": "2026-04-26T20:10:23.521499Z",
     "iopub.status.idle": "2026-04-26T20:10:23.522846Z",
     "shell.execute_reply": "2026-04-26T20:10:23.522669Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total charged: $262.50\n",
      "B-2001: Northwind Supply - $4337.50\n"
     ]
    }
   ],
   "source": [
    "charged = business.withdraw_with_fee(250.00)\n",
    "print(f'Total charged: ${charged:.2f}')\n",
    "print(business)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1bd2521",
   "metadata": {},
   "source": [
    "`withdraw_with_fee` subtracts both the withdrawal amount and the fee, while the inherited methods from `BankAccount` still handle the core balance updates."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "ceb63a74",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.232319Z",
     "start_time": "2026-04-29T07:19:15.221093Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.523732Z",
     "iopub.status.busy": "2026-04-26T20:10:23.523690Z",
     "iopub.status.idle": "2026-04-26T20:10:23.525182Z",
     "shell.execute_reply": "2026-04-26T20:10:23.525004Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "4337.5"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "business.balance"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4f5e107",
   "metadata": {},
   "source": [
    "In this example, `BusinessAccount` inherited general account behavior from `BankAccount` and then added specialized behavior of its own. The **child class did not need to rewrite** deposit, basic withdrawal, string display, or transfer logic."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eeb70a14",
   "metadata": {},
   "source": [
    "The `SavingsAccount` and `BusinessAccount` classes inherited all the methods from `BankAccount`, such as `deposit`, `withdraw`, `__str__`, and `transfer_to`, but each subclass also gained attributes or methods that reflect its own role in the banking system."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2f48aae",
   "metadata": {},
   "source": [
    "### `issubclass()`\n",
    "\n",
    "The `issubclass()` checks whether one class inherits from another at runtime."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2eae40e",
   "metadata": {},
   "source": [
    "`issubclass(Child, Parent)` returns `True` if `Child` is a subclass of `Parent` or is `Parent` itself. It is useful when class relationships need to be checked at runtime.\n",
    "\n",
    "A practical detail matters here: the first argument to `issubclass()` must itself be a class. If user input or function arguments are being validated, that point is often worth checking explicitly before calling `issubclass()`.\n",
    "\n",
    "| Function | Checks |\n",
    "|---|---|\n",
    "| `isinstance(obj, cls)` | Is `obj` an instance of `cls` or one of its subclasses? |\n",
    "| `issubclass(A, B)` | Is class `A` a subclass of `B`? |\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "ae99919c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.239512Z",
     "start_time": "2026-04-29T07:19:15.233834Z"
    }
   },
   "outputs": [],
   "source": [
    "class Animal:\n",
    "    def __init__(self, name, sound):\n",
    "        self.name = name\n",
    "        self.sound = sound\n",
    "\n",
    "    def speak(self):\n",
    "        return f'{self.name} says {self.sound}!'\n",
    "\n",
    "class Dog(Animal):\n",
    "    def __init__(self, name, breed):\n",
    "        super().__init__(name, sound='Woof')\n",
    "        self.breed = breed\n",
    "\n",
    "    def info(self):\n",
    "        return f'{self.name} is a {self.breed}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "39215215",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.252912Z",
     "start_time": "2026-04-29T07:19:15.240991Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.533312Z",
     "iopub.status.busy": "2026-04-26T20:10:23.533270Z",
     "iopub.status.idle": "2026-04-26T20:10:23.534775Z",
     "shell.execute_reply": "2026-04-26T20:10:23.534602Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "False\n",
      "True\n",
      "Rex says Woof!\n"
     ]
    }
   ],
   "source": [
    "\n",
    "print(issubclass(Dog, Animal))   # True\n",
    "print(issubclass(Animal, Dog))   # False\n",
    "print(issubclass(Dog, Dog))      # True (a class is a subclass of itself)\n",
    "\n",
    "# Common use: guarding against wrong arguments\n",
    "def make_animal(cls, *args, **kwargs):\n",
    "    if not isinstance(cls, type):\n",
    "        raise TypeError('cls must be a class')\n",
    "    if not issubclass(cls, Animal):\n",
    "        raise TypeError(f'{cls} is not an Animal subclass')\n",
    "    return cls(*args, **kwargs)\n",
    "\n",
    "rex = make_animal(Dog, 'Rex', 'Labrador')\n",
    "print(rex.speak())   # Rex says Woof!"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bcadecf3",
   "metadata": {},
   "source": [
    "### Class Variables\n",
    "\n",
    "A **class variable** is defined in the class body and shared by all instances of that class.\n",
    "\n",
    "Use a class variable when the value belongs to the class as a whole (for example, a default fee or shared configuration), not to one specific object.\n",
    "\n",
    "A practical detail: assigning to that name through one instance creates or updates an **instance attribute** and no longer changes the shared class-level value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be36caa7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "River Valley Bank | River Valley Bank\n",
      "Initial fee -> a: $2.50, b: $2.50\n",
      "After class update -> a: $3.00, b: $3.00\n",
      "After instance assignment -> a: $3.00, b: $4.50, class: $3.00\n"
     ]
    }
   ],
   "source": [
    "class AccountSettings:\n",
    "    bank_name = 'Phelps County Bank'   # shared by all instances\n",
    "    transfer_fee = 2.50               # shared default fee\n",
    "\n",
    "    def __init__(self, owner):\n",
    "        self.owner = owner\n",
    "\n",
    "a = AccountSettings('Ava')\n",
    "b = AccountSettings('Ben')\n",
    "\n",
    "print(a.bank_name, '|', b.bank_name)\n",
    "print(f'Initial fee -> a: ${a.transfer_fee:.2f}, b: ${b.transfer_fee:.2f}')\n",
    "\n",
    "# Update class variable: both instances see the new shared value\n",
    "AccountSettings.transfer_fee = 3.00\n",
    "print(f'After class update -> a: ${a.transfer_fee:.2f}, b: ${b.transfer_fee:.2f}')\n",
    "\n",
    "# Assign via one instance: creates an instance attribute (shadows class variable for that object only)\n",
    "b.transfer_fee = 4.50\n",
    "print(f'After instance assignment -> a: ${a.transfer_fee:.2f}, b: ${b.transfer_fee:.2f}, class: ${AccountSettings.transfer_fee:.2f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33de8e90",
   "metadata": {},
   "source": [
    "### Method Overriding\n",
    "\n",
    "An **override** happens when a child class defines a method with the **same name** as a parent method to change or specialize behavior.\n",
    "\n",
    "A common pattern is:\n",
    "\n",
    "- add child-specific validation or rules\n",
    "- then call `super().method(...)` to reuse the parent logic\n",
    "\n",
    "This keeps shared behavior in one place while still allowing specialization."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "id": "e040971b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "S-9001: Nina - $250.00\n",
      "Cannot go below minimum balance of $200.00\n"
     ]
    }
   ],
   "source": [
    "class LimitedSavingsAccount(SavingsAccount):\n",
    "    def __init__(self, account_number, owner, balance=0.0, rate=0.01, min_balance=100.0):\n",
    "        super().__init__(account_number, owner, balance, rate)\n",
    "        self.min_balance = min_balance\n",
    "\n",
    "    def withdraw(self, amount):\n",
    "        # Child-specific rule before parent logic\n",
    "        if self.balance - amount < self.min_balance:\n",
    "            raise ValueError(f'Cannot go below minimum balance of ${self.min_balance:.2f}')\n",
    "        super().withdraw(amount)  # reuse parent validation and subtraction\n",
    "\n",
    "lsa = LimitedSavingsAccount('S-9001', 'Nina', 500.0, rate=0.02, min_balance=200.0)\n",
    "lsa.withdraw(250)\n",
    "print(lsa)\n",
    "\n",
    "try:\n",
    "    lsa.withdraw(100)\n",
    "except ValueError as e:\n",
    "    print(e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "new-cefe5105",
   "metadata": {},
   "source": [
    "## Abstraction\n",
    "\n",
    "**Abstraction** means exposing *what* an object can do while hiding *how* it does it. Callers interact with a clean public interface and never need to know the internal logic. For example, you can call `sorted()` without knowing Python's sorting algorithm, use `list.append()` without knowing how lists are stored in memory, or call `acct.deposit()` without knowing what happens inside. Good abstraction makes complex systems feel simple.\n",
    "\n",
    "While inheritance provides a mechanism for one class to reuse another's code, abstraction is a design goal for hiding complexity behind a simple **interface**.\n",
    "\n",
    "Here the `BankAccount` class is a good example. A caller using `deposit`, `withdraw`, or `transfer_to` does not need to know:\n",
    "\n",
    "- how `withdraw` validates the amount\n",
    "- how `transfer_to` coordinates two accounts\n",
    "- how `_balance` is stored internally\n",
    "\n",
    "The caller only needs the interface: the method names and what they do."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "id": "new-200d8f39",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-04-29T07:19:15.267256Z",
     "start_time": "2026-04-29T07:19:15.257224Z"
    },
    "execution": {
     "iopub.execute_input": "2026-04-26T20:10:23.535645Z",
     "iopub.status.busy": "2026-04-26T20:10:23.535603Z",
     "iopub.status.idle": "2026-04-26T20:10:23.537586Z",
     "shell.execute_reply": "2026-04-26T20:10:23.537429Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "S-3001: Eve - $3500.00\n",
      "B-3001: Eve Co - $3700.00\n"
     ]
    }
   ],
   "source": [
    "eve_savings  = SavingsAccount('S-3001', 'Eve', 2000.00, rate=0.02)\n",
    "eve_business = BusinessAccount('B-3001', 'Eve Co', 5000.00, transaction_fee=8.0)\n",
    "\n",
    "# None of these callers need to know the internal validation or storage logic\n",
    "eve_savings.deposit(500)\n",
    "eve_business.withdraw(300)\n",
    "eve_business.transfer_to(eve_savings, 1000)\n",
    "\n",
    "print(eve_savings)\n",
    "print(eve_business)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "57eb6196",
   "metadata": {},
   "source": [
    "### Abstract Base Classes (`abc`)\n",
    "\n",
    "Python supports abstraction explicitly through the built-in `abc` module (**A**bstract **B**ase **C**lasses). An Abstract Base Class (ABC) formalizes that pattern by declaring a contract: any subclass must implement certain methods, or Python will refuse to instantiate it. In other words, with ABC, an abstract base class defines a required interface, it declares methods that child classes **must** implement. \n",
    "\n",
    "To implement ABC, you need to import the `ABC` class and the `abstractmethod` decorator. \n",
    "\n",
    "```python\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "class Account(ABC):\n",
    "    @abstractmethod\n",
    "    def month_end(self):\n",
    "        \"\"\"Apply end-of-month rules for this account type.\"\"\"\n",
    "        pass\n",
    "```\n",
    "\n",
    "In this design, `Account` is not meant to be instantiated directly. Instead, concrete subclasses such as `SavingsAccount` or `CheckingAccount` provide their own implementation of `month_end()`.\n",
    "\n",
    "After a class inherits from an `ABC`, two behavior rules matter:\n",
    "\n",
    "- The subclass **must implement** every **abstract method** before it can be instantiated.\n",
    "- Any function that expects the `ABC` type can use different subclasses interchangeably through the same method names.\n",
    "\n",
    "`ABC` is useful when you want to enforce a shared interface across related classes, not just rely on convention."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "id": "5edd7312",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Charged $49.99 to card ending in 4242.\n",
      "Charged $49.99 via PayPal account student@example.com.\n"
     ]
    }
   ],
   "source": [
    "# Use case: unified payment processing interface with ABC\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "class PaymentMethod(ABC):\n",
    "    @abstractmethod                 ### this decorator marks charge as an abstract method \n",
    "    def charge(self, amount):       ### that must be implemented by subclasses\n",
    "        \"\"\"Return a message describing how payment is processed.\"\"\"\n",
    "        pass\n",
    "\n",
    "class CreditCard(PaymentMethod):\n",
    "    def __init__(self, last4):\n",
    "        self.last4 = last4\n",
    "\n",
    "    def charge(self, amount):       ### this method implements the abstract charge method defined in PaymentMethod\n",
    "        return f\"Charged ${amount:.2f} to card ending in {self.last4}.\"\n",
    "\n",
    "class PayPal(PaymentMethod):        ### another concrete implementation of PaymentMethod\n",
    "    def __init__(self, email):\n",
    "        self.email = email\n",
    "\n",
    "    def charge(self, amount):\n",
    "        return f\"Charged ${amount:.2f} via PayPal account {self.email}.\"\n",
    "\n",
    "\n",
    "def checkout(payment_method, amount):\n",
    "    if not isinstance(payment_method, PaymentMethod):\n",
    "        raise TypeError(\"checkout requires a PaymentMethod\")\n",
    "    return payment_method.charge(amount)\n",
    "\n",
    "methods = [CreditCard(\"4242\"), PayPal(\"student@example.com\")]\n",
    "for method in methods:\n",
    "    print(checkout(method, 49.99))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "595b2ae0",
   "metadata": {},
   "source": [
    "In the example above, both `CreditCard` and `PayPal` inherit from `PaymentMethod`, so each one must provide `charge(self, amount)`.\n",
    "\n",
    "That gives two practical benefits:\n",
    "\n",
    "- `checkout(...)` depends only on the abstract interface, not on a specific concrete class.\n",
    "- New payment types can be added later (for example, `GiftCard` or `BankTransfer`) without changing `checkout(...)`, as long as they inherit from `PaymentMethod` and implement `charge(...)`.\n",
    "\n",
    "This is abstraction in action: one stable contract, many concrete behaviors."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "new-bf42bc34",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "The four pillars are easiest to understand separately, but good object-oriented design usually combines them:\n",
    "\n",
    "- **Encapsulation** protects state and funnels access through a controlled interface.\n",
    "- **Inheritance** lets a new class reuse and extend the behavior of an existing one.\n",
    "- **Polymorphism** lets the same caller code work with different object types as long as they support the same interface.\n",
    "- **Abstraction** defines what an object must provide without exposing every implementation detail.\n",
    "\n",
    "When you read or design a class, a useful habit is to ask: What data does this object protect? What behavior does it inherit? What interface can be used polymorphically? What details are intentionally hidden?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "id": "b1aafd06",
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Four Pillars Review\n",
    "#   Create a small OOP example that demonstrates all four pillars.\n",
    "#   1. Define class Wallet with owner and _cash.\n",
    "#   2. Add deposit(amount) and spend(amount) methods.\n",
    "#      If spend amount is greater than _cash, raise ValueError.\n",
    "#   3. Define class SavingsWallet(Wallet) that adds apply_interest(rate).\n",
    "#   4. Define abstract class Notifier with abstract method send(amount).\n",
    "#      Implement one subclass EmailNotifier that returns a message string.\n",
    "#   5. Create one SavingsWallet, call deposit, spend, and apply_interest.\n",
    "#   6. Create an EmailNotifier object and call send with the wallet cash value.\n",
    "#   7. Print wallet cash and notifier message.\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "id": "d6f5f57a",
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "93.5\n",
      "Email sent: wallet balance is $93.50\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "class Wallet:\n",
    "    def __init__(self, owner, cash=0.0):\n",
    "        self.owner = owner\n",
    "        self._cash = float(cash)\n",
    "\n",
    "    @property\n",
    "    def cash(self):\n",
    "        return self._cash\n",
    "\n",
    "    def deposit(self, amount):\n",
    "        self._cash += amount\n",
    "\n",
    "    def spend(self, amount):\n",
    "        if amount > self._cash:\n",
    "            raise ValueError(\"Not enough cash\")\n",
    "        self._cash -= amount\n",
    "\n",
    "class SavingsWallet(Wallet):\n",
    "    def apply_interest(self, rate):\n",
    "        self._cash += self._cash * rate\n",
    "\n",
    "class Notifier(ABC):\n",
    "    @abstractmethod\n",
    "    def send(self, amount):\n",
    "        pass\n",
    "\n",
    "class EmailNotifier(Notifier):\n",
    "    def send(self, amount):\n",
    "        return f\"Email sent: wallet balance is ${amount:.2f}\"\n",
    "\n",
    "w = SavingsWallet(\"Lia\", 100.0)\n",
    "w.deposit(25.0)\n",
    "w.spend(40.0)\n",
    "w.apply_interest(0.10)\n",
    "\n",
    "n = EmailNotifier()\n",
    "msg = n.send(w.cash)\n",
    "\n",
    "print(w.cash)\n",
    "print(msg)"
   ]
  }
 ],
 "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
}
