> ## Documentation Index
> Fetch the complete documentation index at: https://genai.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 06-Advanced Oops

> Learn advanced object-oriented programming concepts in Python, including class members, magic methods, abstract base classes, and multiple inheritance.

# Object-Oriented Programming

Object-oriented programming (OOP) is a programming paradigm that organizes code around objects. Python provides powerful OOP features that make it easier to build reusable, maintainable, and extensible applications.

## Topics Covered

In this module, you'll learn:

1. [OOP Refresher](#oop-refresher)
2. [Class Members vs Instance Members](#class-members-vs-instance-members)
3. [Class Methods](#class-methods)
4. [Static Methods](#static-methods)
5. [Magic (Dunder) Methods](#magic-dunder-methods)
6. [Abstract Base Classes (ABC)](#abstract-base-classes-abc)
7. [Multiple Inheritance](#multiple-inheritance)
8. [Method Resolution Order (MRO)](#method-resolution-order-mro)
9. [Best Practices](#best-practices)

> **Try Yourself:** [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/DSA%20With%20Python/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/genai-course/blob/main/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises-colab.ipynb) | <a href="/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises.ipynb" download>📥 Download</a><br />
> **Verify Solutions:** [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/DSA%20With%20Python/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/genai-course/blob/main/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-colab.ipynb) | <a href="/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop.ipynb" download>📥 Download</a>

By the end of this module, you'll understand how Python objects work internally, customize object behavior, design reusable class hierarchies, and apply advanced OOP concepts effectively.

<div align="right">[Back to Top ↑](#topics-covered)</div>

## OOP Refresher

Before exploring advanced concepts, let's quickly revisit the fundamentals of object-oriented programming.

### What is OOP?

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around **objects**.

An object combines:

* **Attributes** (data)
* **Methods** (behavior)

### Class and Object

A **class** is a blueprint for creating objects.

An **object** is an instance of a class.

```python theme={null}
class Student:

    def __init__(self, name):
        self.name = name

    def introduce(self):
        print(f"My name is {self.name}")

student = Student("Alice")

student.introduce()
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  My name is Alice
  ```
</Accordion>

### The Four Pillars of OOP

Python supports four fundamental principles of OOP.

| Principle     | Purpose                                 |
| ------------- | --------------------------------------- |
| Encapsulation | Bundle data and methods together        |
| Abstraction   | Hide implementation details             |
| Inheritance   | Reuse existing classes                  |
| Polymorphism  | One interface, multiple implementations |

In this module, we'll build upon these fundamentals and explore advanced OOP concepts.

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Everything in Python is an Object

Python follows an object-oriented design where **everything is an object**—numbers, strings, lists, dictionaries, functions, modules, and even classes.

Every object has:

* **Identity** (`id()`)
* **Type** (`type()`)
* **State** (data)
* **Behavior** (methods)

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Classes are Objects Too

A common misconception is that only instances are objects. In reality, **classes themselves are also objects**.

```python theme={null}
class Student:
    pass

print(type(Student))
print(type(int))
print(type(str))
print(type(list))
```

### What Happens When Python Creates a Class?

When Python executes a class definition, it performs these steps internally:

1. Reads the `class` statement.
2. Creates a temporary namespace (dictionary).
3. Executes every statement inside the class body.
4. Collects all attributes and methods into the namespace.
5. Calls `type(class_name, bases, namespace)`.
6. `type` creates the new class object.
7. Assigns the class object to the class name.

When a class inherits from another class, the base classes are passed as the `bases` argument to `type()`.

### Creating a Class Dynamically Using `type()`

Normally, we define classes statically using the `class` keyword. However, because classes are objects created by `type`, you can use the `type()` function as a constructor to create a class dynamically at runtime.

The signature of `type()` for class creation is:

```python theme={null}
type(class_name, bases, attributes_and_methods_dict)
```

#### Example: Static vs. Dynamic Class Creation

Let's compare the standard static way with the dynamic `type()` method:

**1. The Static Way (Standard):**

```python theme={null}
class Student:
    college = "ABC College"

    def __init__(self, name):
        self.name = name

    def introduce(self):
        return f"My name is {self.name} and I study at {self.college}"
```

**2. The Dynamic Way (Using `type`):**

```python theme={null}
# Define a function to use as a method
def introduce_method(self):
    return f"My name is {self.name} and I study at {self.college}"

# Define the initializer function
def init_method(self, name):
    self.name = name

# Create the class dynamically
DynamicStudent = type(
    "Student",                # Class name
    (object,),                # Base classes (tuple of parent classes)
    {                         # Attributes and methods namespace dictionary
        "college": "ABC College",
        "__init__": init_method,
        "introduce": introduce_method
    }
)

# Instantiate and use the dynamically created class
student = DynamicStudent("Alice")
print(student.introduce())
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  My name is Alice and I study at ABC College
  ```
</Accordion>

***

```text theme={null}
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
```

Every class is an **instance of the built-in `type` class**. When you define a class, Python is actually creating a **new custom type**.

```python theme={null}
student = Student()
print(type(student))
```

Output:

```text theme={null}
<class '__main__.Student'>
```

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Class Members vs Instance Members

A class can contain both **instance members** and **class members**.

### Instance Members

Instance members belong to individual objects.

Each object maintains its own copy.

```python theme={null}
class Student:

    def __init__(self, name):
        self.name = name

student1 = Student("Alice")
student2 = Student("Bob")

print(student1.name)
print(student2.name)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Alice
  Bob
  ```
</Accordion>

Changing one object's attribute does not affect another object.

```python theme={null}
student1.name = "Charlie"

print(student1.name)
print(student2.name)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Charlie
  Bob
  ```
</Accordion>

### Class Members

Class members are shared by all objects of a class.

```python theme={null}
class Student:

    college = "ABC College"

    def __init__(self, name):
        self.name = name

student1 = Student("Alice")
student2 = Student("Bob")

print(student1.college)
print(student2.college)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  ABC College
  ABC College
  ```
</Accordion>

Updating the class member affects every object.

```python theme={null}
Student.college = "XYZ College"

print(student1.college)
print(student2.college)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  XYZ College
  XYZ College
  ```
</Accordion>

### When Should You Use Class Members?

Use class members when a value is shared by every object.

Examples include:

* Company name
* College name
* Tax rate
* Currency
* Number of objects created

### Exercise 1

Create an `Employee` class with an instance member `name` and a class member `company`.

**Sample Input**

```python theme={null}
employee = Employee("Rahul")
```

**Expected Output**

```text theme={null}
Rahul
OpenAI
```

<Accordion title="Solution">
  ```python theme={null}
  class Employee:

      company = "OpenAI"

      def __init__(self, name):
          self.name = name

  employee = Employee("Rahul")

  print(employee.name)
  print(employee.company)
  ```
</Accordion>

### Exercise 2

Create two students and demonstrate that changing an instance member affects only one object.

**Sample Input**

```python theme={null}
student1.name = "Alice"

student2.name = "Bob"
```

**Expected Output**

```text theme={null}
Alice
Bob
```

<Accordion title="Solution">
  ```python theme={null}
  class Student:

      def __init__(self, name):
          self.name = name

  student1 = Student("Alice")
  student2 = Student("Bob")

  student1.name = "Alice"

  print(student1.name)
  print(student2.name)
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Properties

Properties provide controlled access to attributes while allowing them to be accessed like normal variables.

Instead of calling `get_name()` and `set_name()`, properties let you write:

```python theme={null}
student.name
student.name = "Alice"
```

using the `@property` decorator.

### Read-only Properties

A property that only defines a getter can be read but not modified.

```python theme={null}
class Circle:

    @property
    def area(self):
        return 3.14 * self.radius ** 2
```

### Write-only Properties

Although Python does not have true write-only variables, a property can emulate one.

```python theme={null}
class User:

    @property
    def password(self):
        raise AttributeError("Password cannot be read.")

    @password.setter
    def password(self, value):
        self._hashed = hash(value)
```

This is commonly used for passwords and other sensitive information.

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Class Methods

A **class method** operates on the class itself rather than individual objects.

A class method is declared using the `@classmethod` decorator.

Its first parameter is `cls`, which refers to the class.

```python theme={null}
class Student:

    college = "ABC College"

    @classmethod
    def display_college(cls):
        print(cls.college)

Student.display_college()
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  ABC College
  ```
</Accordion>

Class methods are commonly used to:

* Access or modify class members.
* Create alternative constructors.
* Perform operations related to the class.

### Alternative Constructor

```python theme={null}
class Student:

    def __init__(self, name):
        self.name = name

    @classmethod
    def from_uppercase(cls, name):
        return cls(name.title())

student = Student.from_uppercase("alice")

print(student.name)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Alice
  ```
</Accordion>

### Exercise 1

Create a class method that displays the company name.

**Sample Input**

```python theme={null}
Employee.display_company()
```

**Expected Output**

```text theme={null}
OpenAI
```

<Accordion title="Solution">
  ```python theme={null}
  class Employee:

      company = "OpenAI"

      @classmethod
      def display_company(cls):
          print(cls.company)

  Employee.display_company()
  ```
</Accordion>

### Exercise 2

Create an alternative constructor that converts a name to title case.

**Sample Input**

```python theme={null}
Student.from_title("john")
```

**Expected Output**

```text theme={null}
John
```

<Accordion title="Solution">
  ```python theme={null}
  class Student:

      def __init__(self, name):
          self.name = name

      @classmethod
      def from_title(cls, name):
          return cls(name.title())

  student = Student.from_title("john")

  print(student.name)
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Static Methods

A **static method** belongs to the class but does not access either the class (`cls`) or the object (`self`).

Static methods are declared using the `@staticmethod` decorator.

They are commonly used for utility functions related to the class.

```python theme={null}
class Student:

    @staticmethod
    def is_eligible(age):
        return age >= 18

print(Student.is_eligible(20))
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  True
  ```
</Accordion>

Another example.

```python theme={null}
class Calculator:

    @staticmethod
    def square(number):
        return number ** 2

print(Calculator.square(6))
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  36
  ```
</Accordion>

### When Should You Use Static Methods?

Use a static method when:

* The function is related to the class.
* It does not use `self`.
* It does not use `cls`.

### Exercise 1

Create a static method that checks whether a number is even.

**Sample Input**

```python theme={null}
Utility.is_even(10)
```

**Expected Output**

```text theme={null}
True
```

<Accordion title="Solution">
  ```python theme={null}
  class Utility:

      @staticmethod
      def is_even(number):
          return number % 2 == 0

  print(Utility.is_even(10))
  ```
</Accordion>

### Exercise 2

Create a static method that converts Celsius to Fahrenheit.

**Sample Input**

```python theme={null}
Converter.to_fahrenheit(25)
```

**Expected Output**

```text theme={null}
77.0
```

<Accordion title="Solution">
  ```python theme={null}
  class Converter:

      @staticmethod
      def to_fahrenheit(celsius):
          return (celsius * 9 / 5) + 32

  print(Converter.to_fahrenheit(25))
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Magic (Dunder) Methods

Magic methods, also known as **dunder methods** (double underscore methods), are special methods that allow you to customize the behavior of Python objects.

They are automatically invoked by Python in response to certain operations such as printing an object, comparing objects, or using operators.

Some commonly used magic methods are:

| Method       | Purpose                                  |
| ------------ | ---------------------------------------- |
| `__init__()` | Initialize an object                     |
| `__str__()`  | User-friendly string representation      |
| `__repr__()` | Developer-friendly object representation |
| `__len__()`  | Return the length of an object           |
| `__eq__()`   | Compare two objects                      |
| `__add__()`  | Customize the `+` operator               |

<div align="right">[Back to Top ↑](#topics-covered)</div>

## The `__str__()` Method

By default, printing an object displays its memory location.

```python theme={null}
class Student:

    def __init__(self, name, age):
        self.name = name
        self.age = age

student = Student("Alice", 20)

print(student)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  <__main__.Student object at 0x...>
  ```
</Accordion>

The `__str__()` method provides a readable string representation.

```python theme={null}
class Student:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Student(Name={self.name}, Age={self.age})"

student = Student("Alice", 20)

print(student)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Student(Name=Alice, Age=20)
  ```
</Accordion>

### Exercise 1

Implement `__str__()` for a `Book` class.

**Sample Input**

```python theme={null}
book = Book("Python Basics", 450)
print(book)
```

**Expected Output**

```text theme={null}
Book(Title=Python Basics, Price=450)
```

<Accordion title="Solution">
  ```python theme={null}
  class Book:

      def __init__(self, title, price):
          self.title = title
          self.price = price

      def __str__(self):
          return f"Book(Title={self.title}, Price={self.price})"

  book = Book("Python Basics", 450)

  print(book)
  ```
</Accordion>

### Exercise 2

Implement `__str__()` for an `Employee` class.

**Sample Input**

```python theme={null}
employee = Employee("Rahul", "Developer")

print(employee)
```

**Expected Output**

```text theme={null}
Employee(Name=Rahul, Role=Developer)
```

<Accordion title="Solution">
  ```python theme={null}
  class Employee:

      def __init__(self, name, role):
          self.name = name
          self.role = role

      def __str__(self):
          return f"Employee(Name={self.name}, Role={self.role})"

  employee = Employee("Rahul", "Developer")

  print(employee)
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## The `__repr__()` Method

The `__repr__()` method returns an official string representation of an object.

It is mainly intended for developers and debugging.

```python theme={null}
class Student:

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"Student('{self.name}')"

student = Student("Alice")

print(repr(student))
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Student('Alice')
  ```
</Accordion>

### Exercise 1

Implement `__repr__()` for a `Product` class.

**Sample Input**

```python theme={null}
product = Product("Laptop")

repr(product)
```

**Expected Output**

```text theme={null}
Product('Laptop')
```

<Accordion title="Solution">
  ```python theme={null}
  class Product:

      def __init__(self, name):
          self.name = name

      def __repr__(self):
          return f"Product('{self.name}')"

  product = Product("Laptop")

  print(repr(product))
  ```
</Accordion>

### Exercise 2

Implement `__repr__()` for a `Course` class.

**Sample Input**

```python theme={null}
course = Course("Python")

repr(course)
```

**Expected Output**

```text theme={null}
Course('Python')
```

<Accordion title="Solution">
  ```python theme={null}
  class Course:

      def __init__(self, title):
          self.title = title

      def __repr__(self):
          return f"Course('{self.title}')"

  course = Course("Python")

  print(repr(course))
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Operator Overloading

Magic methods can also customize the behavior of Python operators.

### The `__len__()` Method

The `len()` function internally calls `__len__()`.

```python theme={null}
class ShoppingCart:

    def __init__(self):
        self.items = [
            "Laptop",
            "Mouse",
            "Keyboard"
        ]

    def __len__(self):
        return len(self.items)

cart = ShoppingCart()

print(len(cart))
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  3
  ```
</Accordion>

### The `__eq__()` Method

The `==` operator internally calls `__eq__()`.

```python theme={null}
class Employee:

    def __init__(self, employee_id):
        self.employee_id = employee_id

    def __eq__(self, other):
        return self.employee_id == other.employee_id

employee1 = Employee(101)
employee2 = Employee(101)

print(employee1 == employee2)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  True
  ```
</Accordion>

### The `__add__()` Method

The `+` operator internally calls `__add__()`.

```python theme={null}
class BankAccount:

    def __init__(self, balance):
        self.balance = balance

    def __add__(self, other):
        return self.balance + other.balance

account1 = BankAccount(5000)
account2 = BankAccount(7000)

print(account1 + account2)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  12000
  ```
</Accordion>

### Exercise 1

Implement `__len__()` for a `Library` class that returns the number of books.

**Sample Input**

```python theme={null}
library = Library()

len(library)
```

**Expected Output**

```text theme={null}
4
```

<Accordion title="Solution">
  ```python theme={null}
  class Library:

      def __init__(self):
          self.books = [
              "Python",
              "Java",
              "C++",
              "SQL"
          ]

      def __len__(self):
          return len(self.books)

  library = Library()

  print(len(library))
  ```
</Accordion>

### Exercise 2

Implement `__add__()` for a `Wallet` class to combine balances.

**Sample Input**

```python theme={null}
wallet1 + wallet2
```

**Expected Output**

```text theme={null}
1500
```

<Accordion title="Solution">
  ```python theme={null}
  class Wallet:

      def __init__(self, amount):
          self.amount = amount

      def __add__(self, other):
          return self.amount + other.amount

  wallet1 = Wallet(500)
  wallet2 = Wallet(1000)

  print(wallet1 + wallet2)
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Abstract Base Classes (ABC)

An **Abstract Base Class (ABC)** defines a common interface that derived classes must implement.

Python provides the `abc` module to create abstract classes.

```python theme={null}
from abc import ABC, abstractmethod
```

### Why Use ABC?

* Enforces a common interface.
* Prevents incomplete implementations.
* Encourages consistent class design.

### Example

```python theme={null}
from abc import ABC, abstractmethod

class Payment(ABC):

    @abstractmethod
    def pay(self, amount):
        pass


class CreditCard(Payment):

    def pay(self, amount):
        print(f"Paid ₹{amount} using Credit Card")


payment = CreditCard()

payment.pay(2500)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Paid ₹2500 using Credit Card
  ```
</Accordion>

### Exercise 1

Create an abstract class `Vehicle` with an abstract method `start()`.

Implement it in a `Car` class.

<Accordion title="Solution">
  ```python theme={null}
  from abc import ABC, abstractmethod

  class Vehicle(ABC):

      @abstractmethod
      def start(self):
          pass


  class Car(Vehicle):

      def start(self):
          print("Car Started")

  car = Car()

  car.start()
  ```
</Accordion>

### Exercise 2

Create an abstract class `Shape` with an abstract method `area()`.

Implement it in a `Rectangle` class.

<Accordion title="Solution">
  ```python theme={null}
  from abc import ABC, abstractmethod

  class Shape(ABC):

      @abstractmethod
      def area(self):
          pass


  class Rectangle(Shape):

      def area(self):
          return 20 * 10

  rectangle = Rectangle()

  print(rectangle.area())
  ```
</Accordion>

### What Happens During Inheritance?

When Python creates a derived class, it performs these steps:

1. Resolves the parent classes.
2. Validates each base class.
3. Computes the Method Resolution Order (MRO).
4. Creates the child class namespace.
5. Calls `type()` to create the new class object.
6. Stores the inheritance hierarchy.
7. Uses the MRO whenever attributes or methods are searched.

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Multiple Inheritance

Python allows a class to inherit from more than one base class.

```python theme={null}
class Camera:

    def capture(self):
        print("Capturing Photo")


class Phone:

    def call(self):
        print("Calling")


class SmartPhone(Camera, Phone):
    pass


phone = SmartPhone()

phone.capture()
phone.call()
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Capturing Photo
  Calling
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Method Resolution Order (MRO)

When multiple parent classes define the same method, Python follows the **Method Resolution Order (MRO)** to determine which method to invoke.

```python theme={null}
class A:

    def show(self):
        print("Class A")


class B(A):

    def show(self):
        print("Class B")


class C(A):

    def show(self):
        print("Class C")


class D(B, C):
    pass


obj = D()

obj.show()
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  Class B
  ```
</Accordion>

View the MRO using:

```python theme={null}
print(D.__mro__)
```

Output ?

<Accordion title="Show Output">
  ```text theme={null}
  (<class '__main__.D'>,
   <class '__main__.B'>,
   <class '__main__.C'>,
   <class '__main__.A'>,
   <class 'object'>)
  ```
</Accordion>

### Exercise 1

Create a class that inherits from two parent classes and invoke methods from both.

<Accordion title="Solution">
  ```python theme={null}
  class Printer:

      def print_document(self):
          print("Printing")


  class Scanner:

      def scan_document(self):
          print("Scanning")


  class MultiFunctionPrinter(Printer, Scanner):
      pass


  device = MultiFunctionPrinter()

  device.print_document()
  device.scan_document()
  ```
</Accordion>

### Exercise 2

Print the MRO of a class that inherits from two parent classes.

<Accordion title="Solution">
  ```python theme={null}
  class A:
      pass

  class B(A):
      pass

  class C(A):
      pass

  class D(B, C):
      pass

  print(D.__mro__)
  ```
</Accordion>

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Best Practices

* Prefer **instance members** for object-specific data.
* Use **class members** for shared information.
* Use **class methods** to work with class-level data.
* Use **static methods** for utility functions.
* Override only the magic methods you actually need.
* Use abstract base classes to define common interfaces.
* Keep inheritance hierarchies simple and easy to understand.
* Avoid deep multiple inheritance unless necessary.

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Practice

To reinforce what you've learned in this section, practice with the interactive follow-along notebook:

<CardGroup cols={1}>
  <Card title="Follow-Along Practice" icon="laptop-code">
    Practice class/instance variables, class/static methods, custom properties, dunder methods, abstract classes, and multiple inheritance.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/DSA%20With%20Python/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/genai-course/blob/main/public/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises-colab.ipynb) | <a href="/notebooks/workshop-notebooks/06-advanced-oop/06-advanced-oop-exercises.ipynb" download>📥 Download</a>
  </Card>
</CardGroup>

***

<div align="right">[Back to Top ↑](#topics-covered)</div>

## Summary

In this module, you learned advanced object-oriented programming concepts in Python.

### Key Concepts Covered

* OOP Refresher
* Class Members vs Instance Members
* Class Methods
* Static Methods
* Magic (Dunder) Methods
* Operator Overloading
* Abstract Base Classes (ABC)
* Multiple Inheritance
* Method Resolution Order (MRO)
* Best Practices

These concepts help you design reusable, maintainable, and extensible applications while taking advantage of Python's powerful object-oriented features.
