Dunder Methods (Python Data Model)
Special methods prefixed and suffixed with double underscores (__init__, __str__, etc.) that Python calls implicitly to implement operators and built-in functions.
Dunder methods = secret handshakes. When Python sees 'len(x)', it whispers '__len__?' to your object. If your object knows the handshake, it responds. Double underscores = double secret.
__init__(self) initializes an instance (not the constructor — __new__ constructs it). __str__ is called by str() and print() — for end users. __repr__ is called by repr() and in the REPL — for developers, should be unambiguous. __len__, __getitem__, __contains__ make objects work with len(), [], and in. __eq__ enables ==; __hash__ enables use as dict keys / set members (if you define __eq__ you should define __hash__ too).
__new__(cls) actually creates the instance — it's a static method that returns the new object, then __init__ initializes it. __getattr__ is called only when normal lookup fails (for missing attributes). __getattribute__ is called for every attribute access — override with extreme care. __slots__ uses descriptors internally. __call__(self) makes instances callable. __enter__/__exit__ implement the context manager protocol. The data model is how Python's 'everything is an object' works — even classes are objects (instances of their metaclass).
Dunder methods let your classes integrate with Python's operators and built-ins. __repr__ should return a developer-facing unambiguous string — ideally eval()-able. __str__ is user-facing. __eq__ enables ==. __hash__ must be defined consistently with __eq__ — if two objects are equal they must have the same hash. __getitem__ and __len__ make your class work with [] and len(). I use them to make domain objects feel Pythonic rather than requiring explicit method calls.
Defining __eq__ automatically sets __hash__ to None in Python 3, making instances unhashable (can't be dict keys or in sets). You must explicitly define __hash__ if you want both equality comparison and hashability.