Dunder (magic) methods

Python's syntax is a thin skin over method calls. a + b is really a.__add__(b); len(x) is x.__len__(); print(x) reaches for x.__str__(). Define these double-underscore methods on your class and the built-in operators "just work" on your own objects — Java's equals/toString, but for every operator.

__init____repr__ / __str__ __eq____add__ __len__ / __getitem__

Operators are syntactic sugar for method calls

When the interpreter sees an operator or a built-in applied to an object, it translates it into a call to a specially-named method on that object's type. There's no magic beyond the naming convention: implement the right dunder and the corresponding syntax starts working; leave it out and Python either falls back to a default or raises TypeError. Here's a Vector class — pick a piece of syntax and see what Python actually calls:

v = Vector(1, 2)  ·  w = Vector(3, 4)

=

The most important pair: __repr__ and __eq__

Two dunders earn their keep on almost every class you write:

a = Vector(1, 2)
b = Vector(1, 2)  # same components, different object
a == b  →  

A related caution: if you define __eq__ you usually want __hash__ too, so your objects still behave in dicts and sets — Python ties equality and hashing together.

Takeaways: operators & built-ins are sugar for dunder calls — +__add__, ==__eq__, len()__len__, x[i]__getitem__, print__str__/__repr__. Implement the dunder, get the syntax. Always add __repr__; if you add __eq__ remember == defaults to identity without it (and pair it with __hash__).