Python special methods: when operators find your type contract

Trace Python's type-level operator lookup, reflected addition, and the Java boundary where a named method replaces user-defined operator syntax. Methods and type contracts · Object-oriented programming.

You type len(playlist) for a class you wrote, and Python answers. Nothing in your code calls the method that answers: Python does, because len(...) is a promise that the object's type supplies a hook named __len__. Names with double underscores on both sides are special methods, hooks the language calls for you when particular syntax runs. This lesson is about whom Python asks, and in what order.

Syntax is a call to a named hook

Each piece of syntax maps to one hook name. The class below answers four of them.

You writePython asks for
len(p)__len__
x in p__contains__, otherwise it loops using __iter__
for t in p__iter__
print(p) or str(p)__str__, otherwise __repr__
repr(p), or a list that holds p__repr__
a + b__add__ of a's type, then __radd__ of b's type
class Track:
    def __init__(self, title):
        self.title = title
    def __repr__(self):
        return f"Track({self.title!r})"

class Playlist:
    def __init__(self, *titles):
        self.tracks = [Track(t) for t in titles]
    def __len__(self):
        return len(self.tracks)
    def __iter__(self):
        return iter(self.tracks)
    def __contains__(self, title):
        return any(t.title == title for t in self.tracks)
    def __str__(self):
        return f"Playlist of {len(self.tracks)}"

p = Playlist("Rain", "Dusk", "Moss")
len(p)                 # 3
"Dusk" in p            # True
print(p)               # Playlist of 3
print(p.tracks[0])     # Track('Rain')
print(p.tracks[:1])    # [Track('Rain')]

Track defines only __repr__, so print falls back to it, and a list always shows its items with repr. Use __repr__ for the programmer reading a log, and __str__ for the person reading a screen. The fallbacks differ by hook: in falls back to iterating, but len has no fallback and raises TypeError when __len__ is missing.

The type is asked, not the object

Take a Meter that holds a number. Its class defines __add__, and one object, m1, also has an __add__ attribute of its own.

Python: operator syntax asks the type, not the object

Read the steps as text

  1. Meter defines __add__ (returns Meter(12)). m1 is a Meter with its own __add__ (returns patched). m2 is a Meter.
  2. The explicit call m1.__add__(m2) is an ordinary attribute read of __add__ on m1, so the object itself is searched before its type.
  3. Object m1 carries its own __add__, so the read stops at the object.
  4. The attribute __add__ stored on m1 answers m1.__add__(m2) with patched, so that is the value.
  5. For m1 + m2, operator syntax asks the operand types and never the objects themselves. The attribute stored on m1 is not consulted. Both operands are Meter, so only its __add__ gets a turn.
  6. The first turn goes to the left operand m1, of type Meter, which defines __add__.
  7. Meter.__add__ answers m1 + m2 with Meter(12), so that is the value.

An explicit call such as m1.__add__(m2) is ordinary attribute access, so it finds the attribute stored on m1 first. Operator syntax such as m1 + m2 does not search the object at all. Python goes straight to the type and effectively runs type(m1).__add__(m1, m2). The same holds for the other hooks: after p.__len__ = lambda: 99, the call p.__len__() returns 99 but len(p) still reports the class's answer. The rule keeps the meaning of syntax identical for every object of a type.

class Meter:
    def __init__(self, value):
        self.value = value
    def __add__(self, other):
        return Meter(self.value + other.value)
    def __repr__(self):
        return f"Meter({self.value})"

m1, m2 = Meter(5), Meter(7)
m1.__add__ = lambda other: "patched"
m1.__add__(m2)    # 'patched'
m1 + m2           # Meter(12)

When the other operand gets a turn

A Crate can only be added to things it understands. For anything else its __add__ returns NotImplemented, a built-in value that means "not for this pair". It is an answer to Python, not a result and not an exception. Python then asks the right operand's type for the reflected method __radd__, called on the right operand, with the left operand as its other argument.

Python: a declined pair goes to the right operand

Read the steps as text

  1. Crate defines __add__ (returns NotImplemented). Sticker defines __radd__ (returns Stickered crate). Note defines no operator method. c is a Crate, d a Crate, s a Sticker and n a Note.
  2. For c + s, operator syntax asks the operand types and never the objects themselves. Crate goes first and Sticker follows if it declines.
  3. The first turn goes to the left operand c, of type Crate, which defines __add__.
  4. Crate.__add__ returns NotImplemented for c + s, which declines the pair.
  5. The second turn goes to the right operand s, of type Sticker, which defines __radd__.
  6. Sticker.__radd__ answers c + s with Stickered crate, so that is the value.
  7. For d + n, operator syntax asks the operand types and never the objects themselves. Crate goes first and Note follows if it declines.
  8. The first turn goes to the left operand d, of type Crate, which defines __add__. Crate.__add__ returns NotImplemented for d + n, which declines the pair.
  9. The second turn goes to the right operand n, of type Note, which has no __radd__, so no turn is left.
  10. Every turn declined or was empty, so Python raises TypeError for d + n.

A Sticker accepts a crate, so c + s gives 'Stickered crate'. A Note defines nothing, so d + n leaves no turn that can answer, and Python raises TypeError. When both operands have the same type, only that type's __add__ is asked, and TypeError follows its decline.

class Crate:
    def __add__(self, other):
        return NotImplemented

class Sticker:
    def __radd__(self, other):
        return "Stickered crate"

class Note:
    pass

Crate() + Sticker()   # 'Stickered crate'
Crate() + Note()      # TypeError: unsupported operand type(s) for +: 'Crate' and 'Note'

A subclass can go first

There is one exception to "left first". If the right operand's type is a proper subclass of the left operand's type and its reflected method comes from a different class than the left type's, the subclass's reflected method goes first. The subclass gets to protect its richer contract before the base class handles the pair.

Python: a subclass with its own reflected method goes first

Read the steps as text

  1. Reading defines __add__ (returns plain sum) and __radd__ (returns base reflected). Spike(Reading) defines __radd__ (returns spike first). Steady(Reading) defines nothing new. r is a Reading, sp a Spike and t a Steady.
  2. For r + sp, operator syntax asks the operand types and never the objects themselves. Spike is a subclass of Reading with its own __radd__, so Spike goes first.
  3. The first turn goes to the right operand sp, of type Spike, which defines __radd__.
  4. Spike.__radd__ answers r + sp with spike first, so that is the value.
  5. For r + t, operator syntax asks the operand types and never the objects themselves. Steady inherits the __radd__ of Reading, which earns it no priority, so Reading goes first.
  6. The first turn goes to the left operand r, of type Reading, which defines __add__.
  7. Reading.__add__ answers r + t with plain sum, so that is the value.

Priority needs a different method. Steady only inherits the __radd__ that Reading already has, so asking it first would change nothing and Reading goes first. If the priority method itself returns NotImplemented, the left method still gets its turn afterwards.

class Reading:
    def __add__(self, other):  return "plain sum"
    def __radd__(self, other): return "base reflected"

class Spike(Reading):
    def __radd__(self, other): return "spike first"

class Steady(Reading):
    pass

Reading() + Spike()    # 'spike first'
Reading() + Steady()   # 'plain sum'

Java names the contract instead

Java gives ordinary classes no way to define +. The built-in + adds numbers and joins strings, and it never runs a method you define, so a Point offers a named method such as add. Java still has contracts that the language calls for you, but each one is a named method on a named interface or on Object.

record Point(int x, int y) {
    Point add(Point other) {
        return new Point(x + other.x, y + other.y);
    }
}

Point a = new Point(2, 3), b = new Point(4, 1);
Point c = a.add(b);
// Point d = a + b;       // compile error: bad operand types for binary operator '+'
String s = "sum: " + c;   // "sum: Point[x=6, y=4]": string + calls c.toString()
Python hookJava counterpart
__str__toString(), called by string + and println
__iter__Iterable.iterator(), called by the enhanced for
__add__a named method such as add, with no operator syntax
__len__size() by convention only, which no language syntax calls

Using it well

Return NotImplemented from a binary method when you do not recognise the other operand. Raising TypeError yourself ends the conversation, so a reflected method on the other type never runs. Define __repr__ before anything else, since it is what logs and debuggers print. Do not patch special names onto single objects and expect syntax to notice. Equality and hashing use special methods too, but they carry rules of their own, covered in equality and hash contracts.

Recap

  • Syntax such as len(x), x in y and a + b calls a hook looked up on the type, so a hook assigned to one object is not found by that syntax.
  • A binary method that returns NotImplemented hands the turn to the right operand's __radd__. A proper subclass with its own reflected method goes first, and once every turn declines Python raises TypeError.
  • Java has no user-defined operators: use named methods, and rely on toString(), Iterable and similar named contracts where the language calls you.
Go deeper

Other binary operators follow the same shape, for example __mul__ with __rmul__. The augmented form c += d first looks for __iadd__ and falls back to __add__ when it is missing. The truth value of NotImplemented has changed: in Python 3.14 bool(NotImplemented) raises TypeError, so never test it as a condition. Return it from the method and let Python do the fallback.

Continue to anonymous callbacks and capture when a callable needs delayed state, or revisit method dispatch when you need to separate runtime implementation selection from this operator protocol. Practise object updates in Linked List Fundamentals, where an operation chooses which node state to change.