Python's object model is richer than most developers realise. Dunder (magic/special) methods let you control how objects behave with operators, iteration, context managers, and attribute access. Dataclasses and metaclasses take this further, enabling Python to act as a meta-programming language.

Key Points

  • __init__: initialiser (not constructor — __new__ creates the object, __init__ initialises it)
  • __repr__: unambiguous string for debugging — should ideally eval() back to the object
  • __str__: human-readable string — falls back to __repr__ if not defined
  • __eq__ / __hash__: define equality and hash — if you define __eq__, Python sets __hash__ = None (unhashable) unless you also define __hash__
  • __len__, __getitem__, __setitem__, __iter__, __next__: make objects behave like sequences/iterables
  • __enter__ / __exit__: context manager protocol — used by with statements; __exit__ receives exception info
  • __slots__: restricts instance attributes to a fixed set — reduces memory (no __dict__ per instance) for large object counts
  • @dataclass (Python 3.7+): auto-generates __init__, __repr__, __eq__ from field annotations; frozen=True makes it immutable
  • Metaclass: class of a class — type is the default metaclass; override __new__ or __init__ to customise class creation (used by ORMs, ABCs)
Dunder methodTriggered byExample use
__init__obj = MyClass()Set initial state
__repr__repr(obj), interactive shellDebug-friendly string
__eq__obj == otherValue equality
__hash__hash(obj), dict key, set memberMust be consistent with __eq__
__len__len(obj)Container size
__getitem__obj[key]Indexing / slicing
__iter__for x in obj, iter(obj)Make iterable
__enter__/__exit__with obj as x:Context manager
__call__obj(args)Make instance callable

Python OOP: frozen dataclass with validation, context manager protocol, custom iterator, metaclass plugin registry

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass(frozen=True, order=True)   # immutable + comparable
class Money:
    amount: float
    currency: str = "USD"
    _instances: ClassVar[int] = 0     # class var, not a field

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Negative amount")

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Currency mismatch")
        return Money(self.amount + other.amount, self.currency)

# Context manager using __enter__/__exit__
class Timer:
    def __enter__(self):
        import time; self.start = time.perf_counter(); return self
    def __exit__(self, *args):
        self.elapsed = time.perf_counter() - self.start

with Timer() as t:
    expensive_operation()
print(f"Took {t.elapsed:.3f}s")

# Custom iterator — __iter__ + __next__
class CountDown:
    def __init__(self, n): self.n = n
    def __iter__(self): return self
    def __next__(self):
        if self.n <= 0: raise StopIteration
        self.n -= 1; return self.n + 1

# Metaclass — auto-register subclasses (plugin pattern)
class PluginMeta(type):
    registry = {}
    def __new__(mcs, name, bases, ns):
        cls = super().__new__(mcs, name, bases, ns)
        if bases: mcs.registry[name] = cls  # skip base class
        return cls

class Plugin(metaclass=PluginMeta): pass
class CSVPlugin(Plugin): pass   # auto-registered

Real-World Example

Django models, SQLAlchemy's declarative base, and Pydantic all use metaclasses or __init_subclass__ to register and introspect model fields automatically. When you define class User(Base): id = Column(Integer, primary_key=True), SQLAlchemy's metaclass intercepts the class creation and builds the SQL mapping — no explicit registration needed.