Classes and OOP
Object-oriented patterns with modern dataclasses and typing-friendly design. Results appear in the same fence: same-line # when short, multiline # blocks below the sample when not.
Search across all documentation pages
Object-oriented patterns with modern dataclasses and typing-friendly design. Results appear in the same fence: same-line # when short, multiline # blocks below the sample when not.
Dataclasses generate init/repr/eq for record-like types.
from dataclasses import dataclass
@dataclass
class Item:
id: str
qty: int = 1
Item("a") # Item(id='a', qty=1)
Item("a", 2) == Item("a", 2) # TrueExpose computed attributes with @property and optional setters.
class Temp:
def __init__(self, c: float) -> None:
self._c = c
@property
def f(self) -> float:
return self._c * 9 / 5 + 32
Temp(0).f # 32.0
Temp(100).f # 212.0Alternative constructors use @classmethod.
@dataclass
class User:
id: str
name: str
@classmethod
def from_pair(cls, a: str, b: str) -> "User":
return cls(id=f"{a}:{b}", name=a)
User.from_pair("Ada", "L") # User(id='Ada:L', name='Ada')slots=True reduces memory and blocks surprise attributes (3.10+).
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
p.x # 1.0
# p.z = 3 # AttributeErrorStructural subtyping with typing.Protocol for duck typing with checkers.
from typing import Protocol
class Closer(Protocol):
def close(self) -> str: ...
class File:
def close(self) -> str: return "closed"
def shutdown(x: Closer) -> str:
return x.close()
shutdown(File()) # 'closed'Distinguish human str from debug repr.
class User:
def __init__(self, id: str, name: str) -> None:
self.id, self.name = id, name
def __repr__(self) -> str:
return f"User(id={self.id!r})"
def __str__(self) -> str:
return self.name
u = User("1", "Ada")
repr(u) # "User(id='1')"
str(u) # 'Ada'Frozen dataclasses can be hashable for set/dict keys.
@dataclass(frozen=True)
class Key:
tenant: str
id: str
{Key("t", "1"): "ok"}[Key("t", "1")] # 'ok'Call parent initialization with super().
class User:
def __init__(self, id: str, name: str) -> None:
self.id, self.name = id, name
class Admin(User):
def __init__(self, id: str, name: str) -> None:
super().__init__(id, name)
self.roles = ["admin"]
Admin("1", "Ada").roles # ['admin']Force subclasses to implement methods with ABC.
from abc import ABC, abstractmethod
class Repo(ABC):
@abstractmethod
def get(self, id: str) -> str: ...
class MemRepo(Repo):
def get(self, id: str) -> str: return id
MemRepo().get("x") # 'x'
# Repo() # TypeError: can't instantiate abstract classNamed constants with Enum.
from enum import Enum
class Status(Enum):
OPEN = "open"
CLOSED = "closed"
Status.OPEN.value # 'open'
Status("open") # <Status.OPEN: 'open'>Implement __enter__/__exit__ for resource classes.
class Tag:
def __enter__(self):
return "in"
def __exit__(self, *exc):
return False
with Tag() as t:
t # 'in'Mutable defaults need field(default_factory=...).
from dataclasses import dataclass, field
@dataclass
class Bag:
items: list[str] = field(default_factory=list)
a, b = Bag(), Bag()
a.items.append("x")
b.items # [] (not shared)Dynamic attribute access when building flexible objects - use carefully.
class Obj: ...
obj = Obj()
setattr(obj, "name", "Ada")
getattr(obj, "name", None) # 'Ada'
getattr(obj, "missing", 0) # 0Classic __slots__ on non-dataclass types also saves memory.
class Node:
__slots__ = ("value", "next")
def __init__(self, value: int) -> None:
self.value = value
self.next = None
Node(1).value # 1
# Node(1).extra = 2 # AttributeErrorSmall mixin classes add orthogonal behavior - keep them thin.
import json
class JsonMixin:
def to_json(self) -> str:
return json.dumps(self.__dict__)
class User(JsonMixin):
def __init__(self, name: str) -> None:
self.name = name
User("Ada").to_json() # '{"name": "Ada"}'Stack versions: Python 3.14.0 (stable 3.14, maintenance 3.13) · uv 0.6+ · ruff 0.9+
Reviewed by Chris St. John·Last updated Jul 31, 2026