The data model and magic methods
Integrate custom objects with Python through attribute access, representation, equality, hashing, containers, and operators.
Python syntax delegates to the object model. len(x) asks for x.__len__();
x[y] uses x.__getitem__(y); iteration starts with iter(x); and a + b
negotiates through arithmetic special methods. Implementing these protocols lets
custom types participate in ordinary Python without framework-specific adapters.
Special methods are usually looked up on the type, not the instance. Assigning
instance.__len__ therefore does not normally change len(instance).
Construction and attribute lookup
type.__call__ invokes __new__ to allocate or return an instance, then
__init__ to initialize it. Override __new__ mainly for immutable subclasses,
interning, or controlled instance creation.
Normal obj.name lookup considers data descriptors, the instance namespace,
non-data descriptors or class attributes, then __getattr__ as a fallback.
Properties and methods are descriptors.
class Config:
def __init__(self, values: dict[str, str]) -> None:
self._values = values
def __getattr__(self, name: str) -> str:
try:
return self._values[name]
except KeyError as error:
raise AttributeError(name) from error__getattr__ runs only after ordinary lookup fails. __getattribute__ intercepts
every lookup and can recurse forever if it accesses attributes without delegating
to object.__getattribute__.
Dynamic attribute access can be convenient, but explicit attributes and typed models are easier for readers, IDEs, and type checkers.
Developer and user representations
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
def __repr__(self) -> str:
return (
f"{type(self).__name__}("
f"latitude={self.latitude!r}, longitude={self.longitude!r})"
)
def __str__(self) -> str:
return f"{self.latitude:.4f}, {self.longitude:.4f}"repr(obj) should be unambiguous and useful for debugging; when practical it
resembles valid construction syntax. str(obj) is a readable user-facing form.
If __str__ is absent, object falls back to __repr__.
Never include secrets in either representation. Objects appear in logs, tracebacks, test failures, and interactive sessions.
Equality, identity, and hashing
from dataclasses import dataclass
@dataclass(frozen=True)
class UserId:
value: intValue equality should compare the same conceptual value. If a == b, hashable
objects must satisfy hash(a) == hash(b), and fields contributing to the hash
must not change while the object is in a set or dictionary.
Defining __eq__ on a mutable class normally makes it unhashable, which is safe.
Do not restore identity hashing with __hash__ = object.__hash__ when equality is
value-based; that violates the hash contract.
Return NotImplemented for unsupported operand types:
class Version:
def __init__(self, major: int, minor: int) -> None:
self.parts = (major, minor)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self.parts == other.partsNotImplemented lets Python try reflected operations or choose the appropriate
fallback. It is not the same as raising NotImplementedError, which signals that
a method body is intentionally absent.
Container protocols
from collections.abc import Iterator
class Playlist:
def __init__(self, tracks: list[str]) -> None:
self._tracks = list(tracks)
def __len__(self) -> int:
return len(self._tracks)
def __iter__(self) -> Iterator[str]:
return iter(self._tracks)
def __contains__(self, track: object) -> bool:
return track in self._tracks
def __getitem__(self, index: int) -> str:
return self._tracks[index]Copying the incoming list prevents later caller mutation from silently changing
the playlist. __iter__ returns a fresh iterator, so nested and repeated
iteration works. If only __getitem__ exists, Python can sometimes iterate using
successive integer indexes, but an explicit __iter__ states intent.
Sequence-style __getitem__ should respect negative indexes and slices if users
reason about the type as a sequence. Either implement the familiar contract or
choose a domain-specific method.
Operators and reflected methods
For a + b, Python first tries a.__add__(b). Depending on types and results, it
may try b.__radd__(a). In-place a += b first considers __iadd__; mutable
types may modify themselves, while immutable types return a new value.
from dataclasses import dataclass
@dataclass(frozen=True)
class Vector:
x: float
y: float
def __add__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)Operator overloads should preserve the operator's conventional meaning.
Using + to persist data or perform network requests is syntactically clever and
operationally misleading.
Truth, calls, and context
bool(x)uses__bool__, then__len__, then defaults to true.callable(x)is true for objects implementing__call__.with xuses__enter__and__exit__.await xandasync with xuse asynchronous protocols.
These hooks can make domain objects expressive, but hidden work remains hidden. Prefer named methods when cost, mutation, blocking, or failure deserves emphasis.
Tradeoffs and pitfalls
- Protocol integration gives familiar syntax but creates semantic obligations.
__del__is unsuitable for reliable cleanup; finalization timing is not a deterministic resource-management API.- Returning false from
__bool__for a valid but “empty” object can makeif xambiguous. - A surprising
__getattr__can mask spelling mistakes. - Overloaded equality across unrelated types can break transitivity.
- Mutable keys corrupt assumptions made by dictionaries and sets.
Practice
- Build an immutable
Vectorsupporting addition, equality, and a useful repr. - Implement a replayable collection whose
__iter__returns a fresh iterator. - Make an unsupported comparison return
NotImplementedand inspect the final result. - Audit a special method and write down the conventional semantic promise its syntax makes to callers.
LEARNING RECORD
Finish this lesson
Mark it complete when you can explain the main decision without looking.
KNOWLEDGE CHECK