Magic (dunder) methods
Make objects work with ==, <, +, len(), indexing, iteration, calls and with statements.
Why does len() work on lists, strings and dicts? Why can you add numbers and also add lists? Python's secret is special methods — also called magic or dunder methods because they're surrounded by double underscores, like __len__ and __add__. When you write len(x), Python calls x.__len__(); when you write a + b, it calls a.__add__(b). Implement these methods in your own classes and your objects behave like built-ins.
You never call dunder methods directly in normal code — you write len(x), not x.__len__(). You define them so Python's syntax works on your objects.
String representations: __repr__ and __str__#
You met these in the classes lesson. As a rule, always define __repr__; add __str__ only if you want a different, friendlier version for end users.
Equality and ordering#
By default, == compares identity — two separate objects are never equal, even with identical data. Define __eq__ to compare by value, and the ordering methods (__lt__, __le__, __gt__, __ge__) to support <, sorted(), min() and max():
Notice Version("3.9") < Version("3.10") is True — comparing tuples of ints gets this right, whereas comparing the strings "3.9" < "3.10" would be wrong.
Two important rules:
- Return
NotImplemented(notFalse, and not raising) when you don't recognise the other type. Python then tries the other side, and finally falls back to a sensible result orTypeError. - Defining
__eq__sets__hash__toNone, making instances unhashable. If your objects are immutable and you need them in sets or as dict keys, define__hash__based on the same fields as__eq__.
Arithmetic operators#
When Python evaluates 3 * v, it first tries int.__mul__(3, v), which returns NotImplemented; it then tries the reflected v.__rmul__(3). Because we returned NotImplemented for v + 5, Python produced a clear TypeError for us.
Making containers: __len__, __getitem__, __contains__, __iter__#
Implement the sequence protocol and your class works with len(), indexing, slicing, in and for:
For mappings, add __setitem__ and __delitem__ too. If you only need __iter__, Python can use it for in checks as a fallback.
Callable objects: __call__#
An object with __call__ can be called like a function — useful for functions that need to remember configuration or state:
Context managers: __enter__ and __exit__#
The with statement calls __enter__ on entry and __exit__ on the way out — even if an exception occurred. __exit__ receives the exception details (or three Nones):
Other dunders worth knowing#
__format__— custom format specs in f-strings.__getattr__— called when normal attribute lookup fails (handy for proxies).__hash__— hashing for sets/dict keys.__init_subclass__,__class_getitem__— advanced class customisation.__slots__(an attribute, not a method) — saves memory by fixing the set of attributes.
Common mistakes#
- Returning
Falseor raising in__eq__for unknown types — returnNotImplemented. - Defining
__eq__and then using objects in sets — remember__hash__. - Mutating
selfin__add__— operators should return a new object; leave in-place changes to__iadd__. - Calling dunders directly — write
len(x),a + b,str(x). - Writing lots of boilerplate (
__init__,__repr__,__eq__) by hand for data classes — the dataclasses lesson shows how to generate it.
What's next#
Our Thermostat earlier needed a set_target() method for validation. Next you'll see the Pythonic alternative — properties — along with class methods and static methods.
Check your understanding
Quick quiz
1.Which method makes
len(obj)work on your class?2.If you define
__eq__but not__hash__, what happens?3.What should
__add__return when it doesn't know how to add the other operand's type?
Finished reading?
Mark this lesson complete to track your progress.