The data model & dunders
SeniorDunder-method categories and object defaults, repr vs str, __call__/__format__/__del__/pickling hooks, __instancecheck__, and __dict__, vars, globals and __slots__.
Created 2 September 2026 · Updated 23 October 2026
15 concepts · detail hidden
01The major dunder-method categories, with a representative method from each
#c-the-major-dunder-method-categories-with-a-representative-metThe dunder methods fall into a handful of natural categories: lifecycle (
__new__/__init__/__del__— object creation and destruction); representation (__repr__/__str__/__format__— how an object displays); comparison (__eq__/__lt__/__hash__— equality and ordering); container/sequence (__len__/__getitem__/__contains__/__iter__); numeric (the__add__family); context manager (__enter__/__exit__); callable (__call__); attribute access (__getattr__/__getattribute__/__setattr__); descriptor (__get__/__set__); class customization (__init_subclass__/__class_getitem__). Seeing these as categories rather than an isolated checklist of individual dunder names is what “the data model” actually refers to.02What `object` provides by default for `__repr__`, `__eq__`, and `__hash__`
#c-what-object-provides-by-default-for-__repr__-__eq__-and-__haThe default
__repr__(inherited automatically fromobject) produces something along the lines of<ClassName object at 0x...>— showing the class name and raw memory address, genuinely not useful, meaningful information about the object’s actual state. The default__eq__performs pure identity comparison (equivalent tois) — meaning two separately-created instances with identical field values are, by default, considered not equal unless you explicitly define your own__eq__. The default__hash__is based on the object’sid(). This directly explains a common surprise: a plain, undecorated custom class needs an explicit__eq__defined to support any kind of genuine value-based equality at all — it simply doesn’t happen automatically or “for free” just from defining a class.03`__call__` — making instances callable, and real uses beyond a fancy closure
#c-__call__-making-instances-callable-and-real-uses-beyond-a-fa__call__lets an instance of a class be invoked directly with the sameinstance(...)call syntax used for ordinary functions. Real production use cases beyond a toy example: a class-based decorator that also needs to maintain persistent state across calls; a configurable “strategy” object whose behavior is parameterized by state living on the instance itself, while still being usable at call sites exactly like a plain function; or a memoized/cached callable that additionally needs to expose extra methods (e.g.,.clear_cache()) beyond what a purely closure-based approach could cleanly offer to callers.04Why `__del__` is unreliable for critical cleanup — use a context manager instead
#c-why-__del__-is-unreliable-for-critical-cleanup-use-a-context__del__genuinely isn’t guaranteed to run at any specific, predictable time — or, in some real edge cases, at all — including for objects still alive when the interpreter itself shuts down, or (historically, before Python 3.4’s cycle-collector improvements) objects trapped inside an uncollectable reference cycle involving__del__. Additionally, any exception raised inside__del__is effectively swallowed and merely printed to stderr, rather than propagating normally the way an exception anywhere else in the program would — making failures inside__del__genuinely easy to silently miss. And relying on refcounting’s typically-prompt collection timing to make__del__feel reliable in practice is fundamentally a CPython implementation detail, not a language-level guarantee — PyPy’s own garbage collector, for instance, genuinely behaves differently here. The correct, reliable pattern for guaranteed cleanup remains a proper context manager (with/__exit__), not__del__.05`__format__` — how f-strings and `format()` delegate a custom format mini-language
#c-__format__-how-f-strings-and-format-delegate-a-custom-formatBoth the built-in
format(obj, spec)function and f-string interpolation syntax ({obj:spec}) ultimately callobj.__format__(spec)internally, passing along whatever text appears after the colon as the rawspecstring, for the object itself to interpret however it chooses. This is precisely the mechanism that lets custom classes define and support their own bespoke “format mini-languages” — not merely the familiar built-in numeric/string format specifiers — simply by parsing thatspecstring themselves, inside a custom__format__implementation.06`__getstate__`/`__setstate__` — controlling pickling for unpicklable state
#c-__getstate____setstate__-controlling-pickling-for-unpicklablpickle’s default behavior generally introspects an object’s__dict__directly and reconstructs it later via its class — this default approach genuinely breaks down for objects holding state that simply cannot be meaningfully pickled at all (an open file handle, a live network socket, a thread lock, aweakref), or for objects that need genuinely custom reconstruction logic beyond naive attribute copying.__getstate__/__setstate__let you take explicit control:__getstate__decides exactly what gets serialized (e.g., deliberately excluding a live connection object entirely from the pickled representation), and__setstate__controls exactly how the object is reconstructed on unpickling (e.g., re-opening a fresh connection at that point, rather than naively trying to restore the original, now-meaningless serialized connection state).07`__instancecheck__`/`__subclasscheck__` — the machinery behind virtual subclasses and runtime Protocols
#c-__instancecheck____subclasscheck__-the-machinery-behind-virtDefining these on a metaclass lets it override exactly what
isinstance(obj, cls)andissubclass(sub, cls)actually check, replacing Python’s default MRO-walking nominal-inheritance behavior with genuinely custom logic. This is precisely the underlying mechanism that makesABCMeta’s virtual subclass registration (SomeABC.register(SomeUnrelatedClass)) andruntime_checkableProtocols both work — enablingisinstanceto correctly returnTruefor objects that were never nominally, formally subclassed from the relevant type at all, purely because they’re structurally/behaviorally compatible, or explicitly registered as compatible.singledispatchpiggybacks on this exact same ABC-registry machinery.08`__dir__` — surfacing dynamically generated attributes to `dir()` and autocomplete
#c-__dir__-surfacing-dynamically-generated-attributes-to-dir-an__dir__controls precisely whatdir(obj)reports back to the caller — the default implementation genuinely introspects the object’s real, concretely-existing attributes and methods. A class that instead synthesizes attributes dynamically, on demand, via__getattr__(e.g., a dynamic API client proxying arbitrary endpoint names as if they were attributes) won’t have any of those “virtual,” synthesized attributes show up indir()— or, critically, in IDE autocomplete — unless__dir__is separately, explicitly overridden to report them. This genuinely matters for real-world usability and discoverability of any deliberately dynamic API.09`__str__` and `__repr__` — the one-directional fallback, and why containers use `repr` on elements
#c-__str__-and-__repr__-the-one-directional-fallback-and-why-coThe two methods have different contracts.
__str__is meant to produce a readable, user-facing display string (whatprint()and f-strings show).__repr__is meant to produce an unambiguous, developer-facing representation — ideally one that, if passed toeval(), would reconstruct an equal object (Point(x=3, y=4)), which makes it genuinely useful in debuggers, logs, and the REPL because it shows precisely what the object is, not merely a friendly summary of it. When aneval-able form isn’t practical, the convention is the angle-bracket form<Connection host='db1' open>.class Point: def __init__(self, x, y): self.x, self.y = x, y def __repr__(self): return f"Point(x={self.x!r}, y={self.y!r})" # eval-able, unambiguous def __str__(self): return f"({self.x}, {self.y})" # friendly p = Point(3, 4) print(p) # -> (3, 4) uses __str__ print([p]) # -> [Point(x=3, y=4)] containers use repr on elements repr(p) # -> 'Point(x=3, y=4)'If
__str__is left undefined, callingstr(obj)automatically falls back to invoking__repr__instead — but this relationship is genuinely one-directional:__repr__, if undefined, never falls back to a custom__str__you might have defined; it simply usesobject’s own default__repr__implementation directly, entirely independent of whatever__str__you wrote. A genuinely common, real surprise: built-in containers displaying their contained elements (e.g.,print([my_obj])) always callrepr()on each individual element, neverstr()— leaving a lot of developers confused about why their nicely-implemented__str__output never actually shows up when their custom object appears nested inside a printed list or dict.10An instance's `__dict__` vs a class's `__dict__` — what lives in each
#c-an-instances-__dict__-vs-a-classs-__dict__-what-lives-in-eacAn instance’s
__dict__holds that specific object’s own, individual per-instance attributes — data assigned viaself.x = ...inside that particular instance. A class’s__dict__(technically returned as a read-onlymappingproxy) instead holds everything genuinely defined at the class level — methods, class-level attributes, descriptors — all of which is shared across every single instance of that class rather than duplicated per instance. Ordinary attribute lookup on an instance checks that instance’s own__dict__first, then falls back to the class’s__dict__, walked through the MRO in the usual attribute-lookup priority order.11Why a class's `__dict__` is a read-only `mappingproxy` but an instance's is a mutable `dict`
#c-why-a-classs-__dict__-is-a-read-only-mappingproxy-but-an-insmappingproxyis a deliberately read-only view, specifically to prevent bypassing the proper class-creation/metaclass machinery by simply poking arbitrary entries directly into a class’s namespace after the fact — class-level attribute assignment is meant to genuinely go throughtype.__setattr__(which can, among other things, correctly trigger any relevant metaclass hooks along the way), rather than allowing silent, unmediated direct dict mutation to sidestep that entirely. An instance’s own__dict__, by contrast, has no such protective layer by default — direct mutation (instance.__dict__['x'] = 5) is fully legal and behaves exactly equivalently to the ordinaryinstance.x = 5for genuinely everyday, plain attribute assignment.12Why built-in immutable types have no per-instance `__dict__`
#c-why-built-in-immutable-types-have-no-per-instance-__dict__It raises
AttributeError— built-in immutable types are deliberately implemented without any per-instance__dict__at all, for two combined reasons: genuine memory efficiency, given the sheer scale at which these fundamental types are constantly created and used throughout any real program, and the simple fact that these types are immutable, so there’s fundamentally nothing meaningful to dynamically add to a given instance after creation anyway. This connects directly to__slots__: defining__slots__on your own custom class is deliberately opting your instances into this exact same “no per-instance__dict__” memory-saving behavior that built-in types already get automatically, for free.13`vars(obj)` vs `obj.__dict__` — and how no-argument `vars()` differs
#c-varsobj-vs-obj__dict__-and-how-no-argument-vars-differsvars(obj), called with a single argument, is essentially just a thin, convenient wrapper that returnsobj.__dict__directly — they refer to the exact same underlying dict object, not a copy of it; mutating whatevervars(obj)returns genuinely mutates the instance’s real attribute storage directly, precisely the same as mutatingobj.__dict__itself would.vars()called with no arguments at all, from inside a function, is instead equivalent tolocals()— a genuinely, meaningfully different (and considerably more dynamic and generally unreliable to mutate with any real effect) thing, worth carefully distinguishing from the single-argument form.14Legitimate uses for reading or writing `globals()` programmatically
#c-legitimate-uses-for-reading-or-writing-globals-programmaticaGenuine, legitimate uses: dynamically registering a whole family of similarly-shaped functions or classes into a module’s top-level namespace from within a loop (a real metaprogramming pattern generating related code from a shared specification), or plugin-loading code that genuinely needs to programmatically inspect exactly what’s currently defined at the module’s top level. Worth genuine skepticism too, though: reaching for
globals()outside of a narrow, deliberate, well-understood use case like these is usually more a sign of avoiding a cleaner, more explicit namespace/data-structure design than a genuinely necessary technique.15`__slots__` and `__dict__` — and how a subclass without `__slots__` silently regains one
#c-__slots__-and-__dict__-and-how-a-subclass-without-__slots__-Accessing
instance.__dict__on a properly__slots__-using base class raisesAttributeError— no__dict__exists at all, by deliberate design, since that absence is precisely the whole point and mechanism behind__slots__'s memory savings. But — a genuinely common, easy-to-miss real gotcha — if a subclass of that base class doesn’t also explicitly declare its own__slots__(even an empty__slots__ = ()), its own instances silently regain a full__dict__after all: the plain absence of a__slots__declaration in the subclass quietly reintroduces the default per-instance dict there, entirely defeating the memory savings the base class was originally trying to enforce throughout the hierarchy.class Base: __slots__ = ("a",) class Leaky(Base): pass # forgot to declare __slots__ class Tight(Base): __slots__ = () # explicitly claims "no new attributes" leaky = Leaky() leaky.a = 1 leaky.b = 2 # works — Leaky instances have a __dict__ after all leaky.__dict__ # {'b': 2} tight = Tight() tight.a = 1 tight.b = 2 # AttributeError: 'Tight' object has no attribute 'b'Leakystill benefits fromBase’s slot fora, but every instance also carries a full__dict__for anything else — so it pays for both layouts at once, with none of the memory saving__slots__was meant to provide.Tightis the version that actually preserves it: an empty__slots__ = ()adds no new attributes but, crucially, adds no__dict__either.