Magic Methods
Magic Methods
Section titled โMagic MethodsโWhat it is
Section titled โWhat it isโMagic methods (a.k.a. โdunderโ methods, for their double-underscore names like __init__) are special hooks Python calls automatically for built-in operations: constructing an object, printing it, comparing it, adding it, calling it like a function. Defining one on your class opts that class into the corresponding built-in behavior.
Before this feature
Section titled โBefore this featureโWithout magic methods, a class can only expose behavior through explicitly named methods โ nothing integrates with Pythonโs own syntax:
class Money: def __init__(self, amount): self.amount = amount
def to_string(self): return f"${self.amount}"
def add(self, other): return Money(self.amount + other.amount)
m = Money(10)print(m.to_string()) # have to remember the method nametotal = m.add(Money(5)) # can't just write m + Money(5)After this feature
Section titled โAfter this featureโclass Money: def __init__(self, amount): self.amount = amount
def __repr__(self): return f"Money({self.amount!r})" # unambiguous, for developers/debugging
def __str__(self): return f"${self.amount:.2f}" # readable, for end users
def __add__(self, other): return Money(self.amount + other.amount)
def __eq__(self, other): return self.amount == other.amount
def __call__(self, multiplier): return Money(self.amount * multiplier)
m = Money(10)print(m) # ${10.00} -> uses __str__repr(m) # 'Money(10)' -> uses __repr__m + Money(5) # Money(15) -> uses __add__m == Money(10) # True -> uses __eq__m(3) # Money(30) -> uses __call__, m behaves like a functionWhy this is better
Section titled โWhy this is betterโ- Integrates with the language:
print(),+,==,len(), the debugger, and f-strings all โjust workโ on your objects. __repr__vs__str__split intent:__repr__is for developers (ideallyeval-able or at least unambiguous);__str__is for end users. If only__repr__is defined, Python falls back to it forstr()too.__call__enables callable objects: a class instance can act like a function while still holding state between calls โ useful for configurable callbacks, decorators-as-classes, and memoizing wrappers.
Key notes / edge cases
Section titled โKey notes / edge casesโ__init__initializes an already-created instance (selfexists);__new__is what actually creates the instance and mustreturnit โ you rarely override__new__except for immutable types (subclassingstr/tuple) or singleton patterns.- If you define
__eq__, Python sets__hash__toNoneunless you define it too โ your objects become unhashable (canโt go in asetor be adictkey) unless you explicitly re-add__hash__. print(obj)uses__str__; if__str__is missing, Python falls back to__repr__. The reverse is not true.object.__new__(cls)is called before__init__; overriding__new__is how immutable-type subclasses set their value, since by the time__init__runs the (immutable) object already exists.
class Singleton: _instance = None
def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance
a, b = Singleton(), Singleton()print(a is b) # True โ __new__ returned the same object both timesQuick practice
Section titled โQuick practiceโ-
Whatโs the difference between
__str__and__repr__?Answer
`__str__` is for a readable, user-facing string (used by `print()`/`str()`); `__repr__` is for an unambiguous, developer-facing string (used by `repr()` and the interactive console), and is the fallback if `__str__` isn't defined. -
Why would you override
__new__instead of__init__?Answer
`__new__` controls object *creation* and must run before `__init__` โ you need it to subclass an immutable type (where the value has to be set at creation time) or to implement patterns like singletons that decide whether to create a new instance at all. -
What does defining
__call__on a class let you do?Answer
Call an instance like a function โ `instance(args)` โ while the instance still holds its own state between calls, unlike a plain function.