|$ 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

PythonIntermediate 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
2class 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
21rex = Dog("Rex", 3)
22bella = Dog("Bella", 5)
23
24print(rex.name) # → Rex
25print(rex.species) # → Canis familiaris
26print(rex.bark()) # → Rex says Woof!
27print(bella.bark("Yip")) # → Bella says Yip!
28
29# Instance vs class variable
30Dog.species = "Canis lupus familiaris"
31print(rex.species) # → changed for all instances
32
33# __dict__ — inspect attributes
34print(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
1class 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
53v1 = Vector(1, 2)
54v2 = Vector(3, 4)
55print(v1 + v2) # → Vector(4, 6)
56print(v1 * 3) # → Vector(3, 6)
57print(v1 == Vector(1, 2)) # → True
58print(abs(v1)) # → 2.236...
59print(v1[0]) # → 1
60print(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
2class 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
12class Dog(Animal):
13 def speak(self) -> str:
14 return f"{self.name} says Woof!"
15
16class Cat(Animal):
17 def speak(self) -> str:
18 return f"{self.name} says Meow!"
19
20animals = [Dog("Rex"), Cat("Luna")]
21for animal in animals:
22 print(animal.speak())
23
24# Super — call parent implementation
25class 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
34class Flyer:
35 def fly(self) -> str:
36 return f"{self.name} is flying"
37
38class Swimmer:
39 def swim(self) -> str:
40 return f"{self.name} is swimming"
41
42class Duck(Animal, Flyer, Swimmer):
43 def speak(self) -> str:
44 return f"{self.name} says Quack!"
45
46donald = Duck("Donald")
47print(donald.fly()) # → Donald is flying
48print(donald.swim()) # → Donald is swimming
49
50# MRO — Method Resolution Order
51print(Duck.__mro__)
52# → (<class 'Duck'>, <class 'Animal'>, <class 'Flyer'>,
53# <class 'Swimmer'>, <class 'object'>)
54
55# Abstract base classes
56from abc import ABC, abstractmethod
57
58class Shape(ABC):
59 @abstractmethod
60 def area(self) -> float:
61 pass
62
63 @abstractmethod
64 def perimeter(self) -> float:
65 pass
66
67class 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
2class 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
22acc = BankAccount("Alice", 1000)
23# acc.__balance # AttributeError
24print(acc._BankAccount__balance) # → 1000 (accessible but discouraged)
25
26# Property decorator — Pythonic getters/setters
27class 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
52t = Temperature()
53t.celsius = 100 # uses setter
54print(t.fahrenheit) # → 212.0 (uses getter)
55t.fahrenheit = 32 # uses fahrenheit setter
56print(t.celsius) # → 0.0
Data Classes

Data classes (Python 3.7+) automatically generate __init__, __repr__, __eq__, and more.

dataclasses.py
Python
1from dataclasses import dataclass, field, asdict, astuple
2
3@dataclass
4class 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
15p = Person("Alice", 30, "alice@example.com")
16print(p) # → Person(name='Alice', age=30, email='alice@example.com', tags=[])
17print(asdict(p)) # → {'name': 'Alice', 'age': 30, ...}
18
19# Frozen (immutable) data class
20@dataclass(frozen=True)
21class Point:
22 x: float
23 y: float
24
25p = Point(1.0, 2.0)
26# p.x = 3.0 # FrozenInstanceError
27
28# Comparison control
29@dataclass(order=True)
30class Score:
31 value: int = field(compare=True)
32 name: str = field(compare=False) # ignore in comparisons
33
34# Inheritance with data classes
35@dataclass
36class 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
1from typing import Protocol, runtime_checkable
2
3@runtime_checkable
4class Drawable(Protocol):
5 """Anything with a draw() method is Drawable."""
6 def draw(self) -> str:
7 ...
8
9class Circle:
10 def draw(self) -> str:
11 return "○"
12
13class Square:
14 def draw(self) -> str:
15 return "□"
16
17class NotDrawable:
18 pass
19
20def render(shape: Drawable):
21 print(shape.draw())
22
23render(Circle()) # OK — Circle satisfies Drawable
24render(Square()) # OK
25# render(NotDrawable()) # type checker would flag this
26
27# Runtime check
28print(isinstance(Circle(), Drawable)) # → True
29print(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