Python Atlas
Core language

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: int

Value 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.parts

NotImplemented 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 x uses __enter__ and __exit__.
  • await x and async with x use 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 make if x ambiguous.
  • 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

  1. Build an immutable Vector supporting addition, equality, and a useful repr.
  2. Implement a replayable collection whose __iter__ returns a fresh iterator.
  3. Make an unsupported comparison return NotImplemented and inspect the final result.
  4. 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

The data model and magic methods

1Why is identity hashing invalid for a class whose `__eq__` implements value equality?
2What should `__eq__` return when it does not support the other operand’s type?