Special methods - almost always called dunders, from the “double underscore” __name__ spelling - are the hooks through which a Python object plugs into the language’s data model. Implementing __len__ is what makes len() work on your object, __iter__ is what makes it usable in a for loop, and so on. They are how an object earns its place as a first-class citizen, which is also why they underpin Python’s structural polymorphism.
They are the interpreter’s to call, not yours
Dunders are meant to be invoked by the Python interpreter, not called directly in your own code. __init__ is the usual exception: you call it explicitly via super().__init__() in a subclass constructor.
When you do need the behaviour a dunder provides, reach for the related built-in rather than the method:
len(obj)rather thanobj.__len__()iter(obj)rather thanobj.__iter__()str(obj)rather thanobj.__str__()
Going through the built-in is not just stylistic. It lets the interpreter apply fallbacks and optimisations that calling the dunder by hand would skip (see below).
Dispatch is implicit, and often has fallbacks
The interpreter’s invocation of special methods is implicit, and the dispatch chain can fall back to older protocols. Iteration is the classic case. for k in x first tries iter(x), which calls x.__iter__. If __iter__ is not defined, the interpreter falls back to the older sequence protocol and calls x.__getitem__(0), x.__getitem__(1), and so on until it hits an IndexError. This is why a class that implements only __getitem__ is still iterable.
Built-ins may bypass the dunder entirely
Built-in types are heavily optimised, and the optimisation can skip the special method altogether. For built-ins like list and str, CPython reads the ob_size field of the PyVarObject struct to get the length rather than calling __len__. This is transparent as long as you go through the built-in (len(obj)); call obj.__len__() directly and you forfeit the shortcut. Another reason to prefer the built-in.
__repr__ vs __str__
If you are only going to implement one of the two string dunders, implement __repr__. It is meant to produce an unambiguous representation of the object - ideally one you could paste back to recreate it - and it is the fallback that __str__ (and hence print()) uses when __str__ is not defined. __str__ is for the friendly, human-readable form; __repr__ is for the developer. Cover __repr__ and you always have something sensible to fall back on.
References
- Ramalho, L. (2022). Fluent Python, 2nd Edition. O’Reilly. (See book notes.)