|$ curl https://forge-ai.dev/api/markdown?path=docs/python/oop
$cat docs/python-—-object-oriented-programming.md
updated Recently·25 min read·published
Python — Object-Oriented Programming
◆Python◆Intermediate to Advanced
Introduction
Python is a multi-paradigm language with first-class support for object-oriented programming. Everything in Python is an object, including classes, functions, and types themselves.
Classes & Instances
classes.py
Python
| 1 | # Class definition |
| 2 | class Dog: |
| 3 | """A simple Dog class.""" |
| 4 | |
| 5 | # Class variable — shared across all instances |
| 6 | species = "Canis familiaris" |
| 7 | |
| 8 | # Constructor — called when instance is created |
| 9 | def __init__(self, name: str, age: int): |
| 10 | self.name = name # instance variable |
| 11 | self.age = age |
| 12 | |
| 13 | # Instance method |
| 14 | def description(self) -> str: |
| 15 | return f"{self.name} is {self.age} years old" |
| 16 | |
| 17 | def bark(self, sound: str = "Woof") -> str: |
| 18 | return f"{self.name} says {sound}!" |
| 19 | |
| 20 | # Creating instances |
| 21 | rex = Dog("Rex", 3) |
| 22 | bella = Dog("Bella", 5) |
| 23 | |
| 24 | print(rex.name) # → Rex |
| 25 | print(rex.species) # → Canis familiaris |
| 26 | print(rex.bark()) # → Rex says Woof! |
| 27 | print(bella.bark("Yip")) # → Bella says Yip! |
| 28 | |
| 29 | # Instance vs class variable |
| 30 | Dog.species = "Canis lupus familiaris" |
| 31 | print(rex.species) # → changed for all instances |
| 32 | |
| 33 | # __dict__ — inspect attributes |
| 34 | print(rex.__dict__) # → {'name': 'Rex', 'age': 3} |
Magic Methods (Dunder Methods)
Magic methods let you define how objects behave with Python's operators and built-in functions. They are surrounded by double underscores (dunder).
magic_methods.py
Python
| 1 | class Vector: |
| 2 | def __init__(self, x: float, y: float): |
| 3 | self.x = x |
| 4 | self.y = y |
| 5 | |
| 6 | # String representation |
| 7 | def __str__(self) -> str: |
| 8 | """Called by print() and str()""" |
| 9 | return f"Vector({self.x}, {self.y})" |
| 10 | |
| 11 | def __repr__(self) -> str: |
| 12 | """Called by repr() — should be unambiguous""" |
| 13 | return f"Vector({self.x!r}, {self.y!r})" |
| 14 | |
| 15 | # Arithmetic |
| 16 | def __add__(self, other: "Vector") -> "Vector": |
| 17 | return Vector(self.x + other.x, self.y + other.y) |
| 18 | |
| 19 | def __sub__(self, other: "Vector") -> "Vector": |
| 20 | return Vector(self.x - other.x, self.y - other.y) |
| 21 | |
| 22 | def __mul__(self, scalar: float) -> "Vector": |
| 23 | return Vector(self.x * scalar, self.y * scalar) |
| 24 | |
| 25 | # Comparison |
| 26 | def __eq__(self, other: object) -> bool: |
| 27 | if not isinstance(other, Vector): |
| 28 | return NotImplemented |
| 29 | return self.x == other.x and self.y == other.y |
| 30 | |
| 31 | def __lt__(self, other: "Vector") -> bool: |
| 32 | return (self.x ** 2 + self.y ** 2) < ( |
| 33 | other.x ** 2 + other.y ** 2 |
| 34 | ) |
| 35 | |
| 36 | # Length (for abs()) |
| 37 | def __abs__(self) -> float: |
| 38 | return (self.x ** 2 + self.y ** 2) ** 0.5 |
| 39 | |
| 40 | # Boolean |
| 41 | def __bool__(self) -> bool: |
| 42 | return self.x != 0 or self.y != 0 |
| 43 | |
| 44 | # Indexing |
| 45 | def __getitem__(self, index: int) -> float: |
| 46 | return (self.x, self.y)[index] |
| 47 | |
| 48 | # Callable |
| 49 | def __call__(self) -> str: |
| 50 | return f"Vector({self.x}, {self.y}) called!" |
| 51 | |
| 52 | # Usage |
| 53 | v1 = Vector(1, 2) |
| 54 | v2 = Vector(3, 4) |
| 55 | print(v1 + v2) # → Vector(4, 6) |
| 56 | print(v1 * 3) # → Vector(3, 6) |
| 57 | print(v1 == Vector(1, 2)) # → True |
| 58 | print(abs(v1)) # → 2.236... |
| 59 | print(v1[0]) # → 1 |
| 60 | print(v1()) # → Vector(1, 2) called! |
ℹ
info
Implement __repr__ for all your classes. It makes debugging significantly easier. A good rule: __repr__ should return something you could paste back into Python to recreate the object.
Inheritance
inheritance.py
Python
| 1 | # Single inheritance |
| 2 | class Animal: |
| 3 | def __init__(self, name: str): |
| 4 | self.name = name |
| 5 | |
| 6 | def speak(self) -> str: |
| 7 | raise NotImplementedError("Subclass must implement") |
| 8 | |
| 9 | def __str__(self) -> str: |
| 10 | return self.name |
| 11 | |
| 12 | class Dog(Animal): |
| 13 | def speak(self) -> str: |
| 14 | return f"{self.name} says Woof!" |
| 15 | |
| 16 | class Cat(Animal): |
| 17 | def speak(self) -> str: |
| 18 | return f"{self.name} says Meow!" |
| 19 | |
| 20 | animals = [Dog("Rex"), Cat("Luna")] |
| 21 | for animal in animals: |
| 22 | print(animal.speak()) |
| 23 | |
| 24 | # Super — call parent implementation |
| 25 | class Puppy(Dog): |
| 26 | def __init__(self, name: str, age: int): |
| 27 | super().__init__(name) # call Animal.__init__ |
| 28 | self.age = age |
| 29 | |
| 30 | def speak(self) -> str: |
| 31 | return f"{self.name} yips!" |
| 32 | |
| 33 | # Multiple inheritance |
| 34 | class Flyer: |
| 35 | def fly(self) -> str: |
| 36 | return f"{self.name} is flying" |
| 37 | |
| 38 | class Swimmer: |
| 39 | def swim(self) -> str: |
| 40 | return f"{self.name} is swimming" |
| 41 | |
| 42 | class Duck(Animal, Flyer, Swimmer): |
| 43 | def speak(self) -> str: |
| 44 | return f"{self.name} says Quack!" |
| 45 | |
| 46 | donald = Duck("Donald") |
| 47 | print(donald.fly()) # → Donald is flying |
| 48 | print(donald.swim()) # → Donald is swimming |
| 49 | |
| 50 | # MRO — Method Resolution Order |
| 51 | print(Duck.__mro__) |
| 52 | # → (<class 'Duck'>, <class 'Animal'>, <class 'Flyer'>, |
| 53 | # <class 'Swimmer'>, <class 'object'>) |
| 54 | |
| 55 | # Abstract base classes |
| 56 | from abc import ABC, abstractmethod |
| 57 | |
| 58 | class Shape(ABC): |
| 59 | @abstractmethod |
| 60 | def area(self) -> float: |
| 61 | pass |
| 62 | |
| 63 | @abstractmethod |
| 64 | def perimeter(self) -> float: |
| 65 | pass |
| 66 | |
| 67 | class Circle(Shape): |
| 68 | def __init__(self, radius: float): |
| 69 | self.radius = radius |
| 70 | |
| 71 | def area(self) -> float: |
| 72 | return 3.14159 * self.radius ** 2 |
| 73 | |
| 74 | def perimeter(self) -> float: |
| 75 | return 2 * 3.14159 * self.radius |
| 76 | |
| 77 | # circle = Shape() # TypeError: can't instantiate abstract class |
Encapsulation & Properties
encapsulation.py
Python
| 1 | # Name mangling for "private" attributes |
| 2 | class BankAccount: |
| 3 | def __init__(self, owner: str, balance: float = 0): |
| 4 | self.owner = owner |
| 5 | self.__balance = balance # name-mangled to _BankAccount__balance |
| 6 | |
| 7 | def deposit(self, amount: float): |
| 8 | if amount <= 0: |
| 9 | raise ValueError("Amount must be positive") |
| 10 | self.__balance += amount |
| 11 | |
| 12 | def withdraw(self, amount: float): |
| 13 | if amount > self.__balance: |
| 14 | raise ValueError("Insufficient funds") |
| 15 | self.__balance -= amount |
| 16 | |
| 17 | # Getter (not Pythonic — use @property instead) |
| 18 | def get_balance(self) -> float: |
| 19 | return self.__balance |
| 20 | |
| 21 | # Name mangling doesn't make it truly private |
| 22 | acc = BankAccount("Alice", 1000) |
| 23 | # acc.__balance # AttributeError |
| 24 | print(acc._BankAccount__balance) # → 1000 (accessible but discouraged) |
| 25 | |
| 26 | # Property decorator — Pythonic getters/setters |
| 27 | class Temperature: |
| 28 | def __init__(self, celsius: float = 0): |
| 29 | self._celsius = celsius |
| 30 | |
| 31 | @property |
| 32 | def celsius(self) -> float: |
| 33 | """Getter — accessed like an attribute""" |
| 34 | return self._celsius |
| 35 | |
| 36 | @celsius.setter |
| 37 | def celsius(self, value: float): |
| 38 | """Setter — validation on assignment""" |
| 39 | if value < -273.15: |
| 40 | raise ValueError("Below absolute zero") |
| 41 | self._celsius = value |
| 42 | |
| 43 | @property |
| 44 | def fahrenheit(self) -> float: |
| 45 | """Read-only property (no setter)""" |
| 46 | return self._celsius * 9 / 5 + 32 |
| 47 | |
| 48 | @fahrenheit.setter |
| 49 | def fahrenheit(self, value: float): |
| 50 | self._celsius = (value - 32) * 5 / 9 |
| 51 | |
| 52 | t = Temperature() |
| 53 | t.celsius = 100 # uses setter |
| 54 | print(t.fahrenheit) # → 212.0 (uses getter) |
| 55 | t.fahrenheit = 32 # uses fahrenheit setter |
| 56 | print(t.celsius) # → 0.0 |
Data Classes
Data classes (Python 3.7+) automatically generate __init__, __repr__, __eq__, and more.
dataclasses.py
Python
| 1 | from dataclasses import dataclass, field, asdict, astuple |
| 2 | |
| 3 | @dataclass |
| 4 | class Person: |
| 5 | name: str |
| 6 | age: int |
| 7 | email: str = "" # default value |
| 8 | tags: list[str] = field(default_factory=list) # mutable default |
| 9 | |
| 10 | def __post_init__(self): |
| 11 | """Called after __init__ — for validation""" |
| 12 | if self.age < 0: |
| 13 | raise ValueError("Age cannot be negative") |
| 14 | |
| 15 | p = Person("Alice", 30, "alice@example.com") |
| 16 | print(p) # → Person(name='Alice', age=30, email='alice@example.com', tags=[]) |
| 17 | print(asdict(p)) # → {'name': 'Alice', 'age': 30, ...} |
| 18 | |
| 19 | # Frozen (immutable) data class |
| 20 | @dataclass(frozen=True) |
| 21 | class Point: |
| 22 | x: float |
| 23 | y: float |
| 24 | |
| 25 | p = Point(1.0, 2.0) |
| 26 | # p.x = 3.0 # FrozenInstanceError |
| 27 | |
| 28 | # Comparison control |
| 29 | @dataclass(order=True) |
| 30 | class Score: |
| 31 | value: int = field(compare=True) |
| 32 | name: str = field(compare=False) # ignore in comparisons |
| 33 | |
| 34 | # Inheritance with data classes |
| 35 | @dataclass |
| 36 | class Employee(Person): |
| 37 | employee_id: str = "" |
Protocols & Structural Subtyping
Python 3.8+ supports structural subtyping via Protocol — "duck typing at scale." If an object has the right methods, it satisfies the protocol.
protocols.py
Python
| 1 | from typing import Protocol, runtime_checkable |
| 2 | |
| 3 | @runtime_checkable |
| 4 | class Drawable(Protocol): |
| 5 | """Anything with a draw() method is Drawable.""" |
| 6 | def draw(self) -> str: |
| 7 | ... |
| 8 | |
| 9 | class Circle: |
| 10 | def draw(self) -> str: |
| 11 | return "○" |
| 12 | |
| 13 | class Square: |
| 14 | def draw(self) -> str: |
| 15 | return "□" |
| 16 | |
| 17 | class NotDrawable: |
| 18 | pass |
| 19 | |
| 20 | def render(shape: Drawable): |
| 21 | print(shape.draw()) |
| 22 | |
| 23 | render(Circle()) # OK — Circle satisfies Drawable |
| 24 | render(Square()) # OK |
| 25 | # render(NotDrawable()) # type checker would flag this |
| 26 | |
| 27 | # Runtime check |
| 28 | print(isinstance(Circle(), Drawable)) # → True |
| 29 | print(isinstance(NotDrawable(), Drawable)) # → False |
✓
best practice
Use Protocols when you want interface-like behavior without inheritance. They enable loose coupling — consumers don't need to import or inherit from your classes.
$Blueprint — Engineering Documentation·Section ID: PYTHON-OOP·Revision: 1.0