OOP in Python
Python OOP is lighter than C# — fewer keywords, more conventions. Six ideas, no fluff.
▶ Watch this reelWhat you'll learn
- Classes & __init__
- Dunder methods
- Inheritance & super()
- dataclasses
- Protocol & ABC
- @property
Remember this
- self is explicit; attributes are created by assignment; _underscore = convention
- Dunders plug your class into Python syntax; dataclasses replace boilerplate records
- Protocol > ABC for interfaces; @property = computed fields
Classes & __init__
class Model:
def __init__(self, name: str, price: float = 0.0):
self.name = name # attribute created by assignment
self.price = price
self= C#this, but explicit in every method signature._x= internal by convention ·__x= name-mangled (not truly private).
Dunder methods
| Dunder | Enables |
|---|---|
__init__ | construction |
__len__ | len(obj) |
__str__ / __repr__ | print / REPL debug |
__eq__ | == |
__getitem__ | obj[i] |
Inheritance & super()
class Tool(BaseTool):·super().run()(Py3: baresuper()).- Keep hierarchies shallow — prefer composition + mixins.
dataclasses
@dataclass(frozen=True)
class Model:
name: str
tags: list = field(default_factory=list)
= records: __init__ + __eq__ + __repr__ generated.
Protocol & ABC
- ABC: nominal — inherit,
@abstractmethod, loud failures. - Protocol: structural — declare the shape; any match qualifies (mypy-checked). Prefer Protocol.
@property
- Computed attribute without
(); add@x.setterfor validation;functools.cached_propertyto memoize.
Code: OOP the Python way — 30 lines, everything from this reel
from dataclasses import dataclass, field
from typing import Protocol
import functools
# --- dataclass: the record you'll write most ------------------
@dataclass(frozen=True)
class Model:
name: str
input_price: float # per 1M tokens, USD
@property
def cost_usd(self, tokens: int = 1_000_000) -> float:
return self.input_price * tokens / 1_000_000
# --- Protocol: structural interface (no inheritance!) ---------
class Storage(Protocol):
def save(self, key: str, value: dict) -> None: ...
def load(self, key: str) -> dict | None: ...
class MemoryStore: # satisfies Storage automatically
def __init__(self): self._data = {}
def save(self, key, value): self._data[key] = value
def load(self, key): return self._data.get(key)
def persist(store: Storage): # mypy checks the SHAPE, not the parent
store.save("config", {"model": "gpt-4o"})
# --- classic class with dunders -------------------------------
class Pipeline:
def __init__(self, steps: list | None = None):
self.steps = steps if steps is not None else []
def __len__(self): return len(self.steps)
def __str__(self): return f"Pipeline({len(self.steps)} steps)"
def add(self, step): self.steps.append(step); return self
p = Pipeline().add("embed").add("search")
print(len(p), str(p))