Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 9 – Object-oriented Python and the data model

Operator overloading done right

Make +, -, * and comparisons work on your own classes: return NotImplemented, add reflected and in-place methods, and fill in orderings with total_ordering.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: Python asks the left operand first, then the right one · Every operator has a method, and most have a reflected twin · A reflected add makes sum() work · 3 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Implement arithmetic operators and their reflected versions, returning NotImplemented for unknown operands
  • Predict which method Python calls for a + b, and when it raises TypeError
  • Implement in-place operators only for mutable types, and explain what += does without them
  • Use functools.total_ordering to fill in comparisons from __eq__ and __lt__
  • Test a number type against a reference implementation such as fractions.Fraction

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement arithmetic operators and their reflected versions, returning NotImplemented for unknown operands
  • Predict which method Python calls for a + b, and when it raises TypeError
  • Implement in-place operators only for mutable types, and explain what += does without them
  • Use functools.total_ordering to fill in comparisons from __eq__ and __lt__
  • Test a number type against a reference implementation such as fractions.Fraction

Before you start

On this page

sum() adds up a list of floats, but on a list of your own Money amounts it raises TypeError at the first item. operator overloading: Giving Python's operators a meaning for your own classes by defining special methods: a + b calls type(a).__add__(a, b), a < b calls __lt__ and -a calls __neg__., giving Python’s operators a meaning for your own classes, fixes that: every operator is a call to a special method, so a + b runs type(a).__add__(a, b), a < b runs __lt__ and -a runs __neg__.

Doing it right comes down to four habits. A method that does not understand its other operand returns NotImplemented instead of raising, so Python can try the other operand’s reflected method: The method Python calls on the right operand when the left operand's method returns NotImplemented, such as __radd__ for + and __rmul__ for *. In 2 * v, int.__mul__ gives up and the vector's __rmul__ answers. method, such as __radd__ for +, which also makes 2 * v and sum() work. Value types return new objects, while only mutable types define in-place methods such as __iadd__. Comparisons can be filled in by functools.total_ordering. And operators are overloaded only where their meaning is obvious to every reader, as for vectors, money and fractions. Below you will build a 2D vector, an amount that sum() accepts and a shirt size that sorts.

Python asks the left operand first, then the right one

The vector below defines +, -, *, unary - and abs():

A 2D vector with +, -, unary -, scalar * and abs() Python · vector2.py
import math
from numbers import Real


class Vector2:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Vector2({self.x!r}, {self.y!r})"

    def __eq__(self, other):
        if not isinstance(other, Vector2):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

    def __add__(self, other):
        if not isinstance(other, Vector2):
            return NotImplemented
        return Vector2(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        if not isinstance(other, Vector2):
            return NotImplemented
        return Vector2(self.x - other.x, self.y - other.y)

    def __mul__(self, k):
        if not isinstance(k, Real):
            return NotImplemented
        return Vector2(self.x * k, self.y * k)

    __rmul__ = __mul__  # k * v means the same as v * k; it runs after int or float gives up

    def __neg__(self):
        return Vector2(-self.x, -self.y)

    def __abs__(self):
        return math.hypot(self.x, self.y)


v, w = Vector2(3, 4), Vector2(1, -2)
print(v + w, v - w, -w)
print(v * 2, 2 * v, 0.5 * v)
print(abs(v), v + w == Vector2(4, 2))

for label, attempt in (("v + 1", lambda: v + 1), ("v * w", lambda: v * w)):
    try:
        attempt()
    except TypeError as error:
        print(f"{label}: TypeError: {error}")

Output

Vector2(4, 2) Vector2(2, 6) Vector2(-1, 2)
Vector2(6, 8) Vector2(6, 8) Vector2(1.5, 2.0)
5.0 True
v + 1: TypeError: unsupported operand type(s) for +: 'Vector2' and 'int'
v * w: TypeError: unsupported operand type(s) for *: 'Vector2' and 'Vector2'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vector2.py

  • v + w and v - w return new vectors and leave both operands alone, as + does for numbers.
  • v * 2 calls __mul__. 2 * v first calls int.__mul__(2, v), which returns NotImplemented because an int knows nothing about vectors, and Python then calls the vector’s reflected method, __rmul__. Multiplying by a number commutes, so __rmul__ = __mul__ is all it takes.
  • -w calls __neg__ and abs(v) calls __abs__, here the length of the vector.
  • v + 1 and v * w return NotImplemented from both sides, so Python raises TypeError with both type names, the same message the built-in types give.
a + b tries type(a).__add__, then type(b).__radd__ if the first returns NotImplemented, and raises TypeError if both do.a + btype(a).__add__(a, b)type(b).__radd__(b, a)the reflected methodTypeErrorunsupported operand type(s) for +the value of a + ba resultNotImplemented,or no __add__a resultNotImplemented,or no __radd__

How Python evaluates a + b

Text description of the diagram

The diagram is a flow from top to bottom.

  • It starts with the expression a + b, which calls the __add__ method of the left operand's type with a and b.
  • If that method returns a result, the result is the value of a + b.
  • If it returns NotImplemented, or the type has no __add__, Python calls the reflected method of the right operand's type, __radd__, with b and a.
  • If the reflected method returns a result, that is the value.
  • If it also returns NotImplemented, or there is no __radd__, Python raises TypeError: unsupported operand type(s) for +.

The same flow holds for every arithmetic operator, such as - with __sub__ and __rsub__ or * with __mul__ and __rmul__.

Step through three of the vector’s expressions in that order:

Interactive · stage by stage v * 2, 2 * v and v + 1, one call at a time Step through three expressions of vector2.py. Each stage is one thing Python tries, and it stops at the first real result.
v * 2, 2 * v and v + 1, one call at a time, step 5 of 5. Showing The left operand's method (__add__: NotImplemented), Right operand (int.__radd__: NotImplemented), TypeError (unsupported operand type(s)). In focus: TypeError.The left operand's method__add__: NotImplementedRight operandint.__radd__: NotImplementedTypeErrorunsupported operand type(s)NotImplemented

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. v * 2. The vector is on the left, so Python calls Vector2.__mul__(v, 2). It accepts any real number and returns Vector2(6, 8), and Python uses that result at once.
  2. 2 * v. Now the int is on the left, so Python calls int.__mul__(2, v) first. An int knows nothing about vectors and returns NotImplemented.
  3. The reflected method. Python calls the right operand's reflected method, Vector2.__rmul__(v, 2), which is __mul__ under a second name, and it returns Vector2(6, 8) as well.
  4. v + 1. Vector2.__add__ accepts only another vector, so for the int 1 it returns NotImplemented, and Python turns to the int.
  5. Both sides give up. int.__radd__(1, v) does not understand a vector either. With NotImplemented from both sides, Python raises TypeError and names both types, Vector2 and int.

Returning NotImplemented is the polite answer: “not with this operand”. Raising TypeError yourself would cut off the other operand’s chance to answer, and returning a wrong value, such as None or False, would hide a bug.

Every method of Vector2 builds a new vector and changes neither operand, which is what readers expect: after c = a + b, a is what it was before. One vector may be stored in several places, as a dictionary key, in a list of waypoints and in a variable, so a + that changed its left operand would change all of them at once. Amounts, coordinates, dates and versions of your own should follow the same rule; in-place methods, which do change an object, belong to containers such as lists.

Checkpoint

Check: How a + b is evaluated

  1. Question 1 of 2 Which calls does 2 * v make, in order, for the Vector2 of the program?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: int.__mul__(2, v), which returns NotImplemented, then Vector2.__rmul__(v, 2)

    For 2 * v, Python calls int.__mul__(2, v), which returns NotImplemented, and then the right operand's reflected method, Vector2.__rmul__(v, 2), which gives Vector2(6, 8).

  2. Question 2 of 2 What happens with v + 1, where Vector2.__add__ accepts only vectors?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Both sides return NotImplemented, and Python raises TypeError naming Vector2 and int

    The program prints v + 1: TypeError: unsupported operand type(s) for +: 'Vector2' and 'int', the same message the built-in types give when neither operand understands the other.

Every operator has a method, and most have a reflected twin

Each operator below, with the method Python calls first on the left operand and the reflected method it tries next on the right one:

Operator Method Reflected method
a + b __add__ __radd__
a - b __sub__ __rsub__
a * b __mul__ __rmul__
a / b __truediv__ __rtruediv__
a // b __floordiv__ __rfloordiv__
a % b __mod__ __rmod__
a ** b __pow__ __rpow__
a @ b __matmul__ __rmatmul__
a < b, a <= b __lt__, __le__ __gt__, __ge__ of the other side

Each arithmetic operator in the table also has an in-place twin, its name with an i after the leading underscores, such as __iadd__ for += and __imul__ for *=. Operators with one operand have no reflected form: -a calls __neg__, +a calls __pos__ and abs(a) calls __abs__.

Comparisons reflect into their mirror image: when a < b gets NotImplemented, Python asks b > a, and == reflects into == on the other side. An in-place operator falls back too: without __iadd__, a += b runs a = a + b, which builds a new object and binds the name a to it.

Checkpoint

Check: The operator methods

  1. Question 1 of 2 Which method does Python try second for a @ b, when type(a).__matmul__ returns NotImplemented?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: type(b).__rmatmul__(b, a)

    The table lists the pairs: __matmul__ and __rmatmul__ for @, as __add__ and __radd__ for +. Only operators with one operand, such as -a, have no reflected form.

  2. Question 2 of 2 In a < b, type(a).__lt__(a, b) returns NotImplemented. What does Python try next?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: type(b).__gt__(b, a), the mirror image of a < b

    When a < b gets NotImplemented, Python asks b > a; == reflects into == on the other side. Comparison methods are each other's reflections.

A reflected add makes sum() work

sum() starts from the integer 0 and adds each item to the running total, so its first step is 0 + items[0]:

Adding up a bill: a start value, or an __radd__ that accepts 0 Python · money_sum.py
class Money:
    def __init__(self, minor, currency="INR"):
        self.minor, self.currency = minor, currency

    def __repr__(self):
        return f"{type(self).__name__}({self.minor!r}, {self.currency!r})"

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if other.currency != self.currency:
            raise TypeError(f"cannot add {self.currency} and {other.currency}")
        return type(self)(self.minor + other.minor, self.currency)


bill = [Money(4000), Money(3000), Money(18000)]
try:
    print(sum(bill))
except TypeError as error:
    print("sum(bill): TypeError:", error)
print("sum(bill, Money(0)):", sum(bill, Money(0)))


class SummableMoney(Money):
    def __radd__(self, other):
        if other == 0 and type(other) is int:  # sum() starts from the int 0
            return self
        return NotImplemented


bill = [SummableMoney(4000), SummableMoney(3000), SummableMoney(18000)]
print("sum(bill):", sum(bill))
print("0 + bill[0]:", 0 + bill[0])
try:
    print(5 + bill[0])
except TypeError as error:
    print("5 + bill[0]: TypeError:", error)

Output

sum(bill): TypeError: unsupported operand type(s) for +: 'int' and 'Money'
sum(bill, Money(0)): Money(25000, 'INR')
sum(bill): SummableMoney(25000, 'INR')
0 + bill[0]: SummableMoney(4000, 'INR')
5 + bill[0]: TypeError: unsupported operand type(s) for +: 'int' and 'SummableMoney'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_sum.py

The first attempt fails because int.__add__ refuses an amount and Money has no reflected method. Two fixes work. Passing a start value of the right type, sum(bill, Money(0)), avoids the integer altogether. Or __radd__ can accept the 0 that sum() starts with and return the amount unchanged, while still refusing other integers: adding 5 to an amount of money means nothing, so 5 + bill[0] still raises TypeError. Step through the first addition, without a reflected method and with one:

Interactive · message by message The first addition of sum(bill) Step through money_sum.py. sum() starts from the int 0, so its first step is 0 + bill[0].
The first addition of sum(bill), step 6 of 6. Messages so far: sum(bill) to The int 0: 0 + bill[0]; The int 0 to sum(bill): NotImplemented; sum(bill) to The first amount: __radd__?; sum(bill) to The first amount: __radd__(bill[0], 0); The first amount to sum(bill): SummableMoney(4000).sum(bill)total = 25000The int 0int.__add__The first amountSummableMoney(4000)0 + bill[0]NotImplemented__radd__?__radd__(bill[0], 0)SummableMoney(4000)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first step. sum() keeps a running total that starts as the int 0, so its first addition is 0 + bill[0], with the int on the left.
  2. int gives up. int.__add__(0, Money(4000)) does not understand an amount and returns NotImplemented.
  3. No reflected method. Python asks the amount for __radd__. Money has none, so the first addition raises TypeError for int and Money, and the second amount is never reached.
  4. SummableMoney. SummableMoney adds __radd__. The same first step reaches it as __radd__(bill[0], 0), with the int 0 as the other operand.
  5. Only the 0 of sum(). __radd__ accepts exactly the int 0 and returns the amount itself, so the running total becomes SummableMoney(4000). Any other int still gets NotImplemented.
  6. The rest of the bill. From the second item on, both operands are amounts, so __add__ answers on the left, and sum() returns SummableMoney(25000, 'INR').

Each fix works around the int on the left: a start value removes it, and __radd__ answers for it.

Checkpoint

Check: sum() and a reflected add

  1. Question 1 of 2 What is the first addition that sum(bill) makes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0 + bill[0], because sum() starts from the int 0

    sum() keeps a running total that starts as the int 0 and adds each item to it, so its first step is 0 + items[0], which only the item's __radd__ can answer.

  2. Question 2 of 2 SummableMoney.__radd__ returns self only for the int 0. What does 5 + bill[0] do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It raises TypeError, because __radd__ returns NotImplemented for any other int

    The reflected method accepts exactly the start value of sum() and nothing else, so sum(bill) works while 5 + bill[0] still raises TypeError, as the program's last line shows.

One decorator fills in the missing comparisons

Six comparison methods are a lot to write and to keep consistent. functools.total_ordering is a class decorator that derives the missing ones from __eq__ and one ordering method:

Shirt sizes in the order a shop uses Python · shirt_sizes.py
from functools import total_ordering


@total_ordering
class ShirtSize:
    ORDER = ("XS", "S", "M", "L", "XL", "XXL")

    def __init__(self, label):
        if label not in self.ORDER:
            raise ValueError(f"unknown size {label!r}")
        self.label = label

    def __repr__(self):
        return f"ShirtSize({self.label!r})"

    def __eq__(self, other):
        if not isinstance(other, ShirtSize):
            return NotImplemented
        return self.label == other.label

    def __hash__(self):
        return hash(self.label)

    def __lt__(self, other):
        if not isinstance(other, ShirtSize):
            return NotImplemented
        return self.ORDER.index(self.label) < self.ORDER.index(other.label)


orders = [ShirtSize(label) for label in ("L", "S", "XL", "M", "S")]
print(sorted(orders))
print(min(orders), max(orders))
m, l = ShirtSize("M"), ShirtSize("L")
print(m < l, m <= l, m > l, m >= l, m != l)
print("written by total_ordering:", [name for name in ("__le__", "__gt__", "__ge__") if name in vars(ShirtSize)])

Output

[ShirtSize('S'), ShirtSize('S'), ShirtSize('M'), ShirtSize('L'), ShirtSize('XL')]
ShirtSize('S') ShirtSize('XL')
True True False False True
written by total_ordering: ['__le__', '__gt__', '__ge__']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shirt_sizes.py

ShirtSize defines only __eq__ and __lt__, and its order comes from the shop’s list rather than the alphabet, so "M" sorts before "L". sorted(), min() and max() all work, and the decorator wrote __le__, __gt__ and __ge__. Step through two questions that the derived <= answers:

Interactive · stage by stage How the derived <= of ShirtSize answers Step through m <= l and l <= m, with M and L from shirt_sizes.py. Each stage is one method the derived __le__ calls.
How the derived <= of ShirtSize answers, step 4 of 4. Showing The derived __le__, ShirtSize.__lt__ (L < M: False), ShirtSize.__eq__ (L == M: False), The answer (l <= m: False). In focus: ShirtSize.__eq__.True: doneThe derived __le__written by total_orderingShirtSize.__lt__L < M: FalseShirtSize.__eq__L == M: FalseThe answerl <= m: FalseFalse

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. m <= l. ShirtSize defines no __le__ of its own. total_ordering wrote one that asks a < b first, here M < L.
  2. Smaller, so done. __lt__ compares positions in ORDER, 2 for M and 3 for L, and returns True. a <= b is then True without any call of __eq__.
  3. l <= m. With the operands swapped, __lt__ compares L with M and returns False, so the derived method still has to ask whether the two are equal.
  4. The extra call. __eq__ compares the labels L and M and returns False, so l <= m is False after two calls where m <= l needed one.

__lt__ alone settled m <= l, while l <= m needed __eq__ as well, a second call for the same question. Each derived method also adds a call of its own around yours, so a class compared millions of times in a hot loop may still define all six by hand.

Checkpoint

Check: Comparisons from one decorator

  1. Question 1 of 2 ShirtSize defines __eq__ and __lt__. Which methods did total_ordering write?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: __le__, __gt__ and __ge__

    total_ordering derives the missing comparisons from __eq__ and one ordering method, so a class that writes two methods supports all six operators.

  2. Question 2 of 2 How many of the methods ShirtSize defines does the derived __le__ call for l <= m, where L < M is false?

    Type a number.

    A hint

    Step through the figure's second case.

    Show the answer: Question 2 of 2

    Answer: 2 calls

    The derived __le__ computes (a < b) or (a == b). L < M is False, so it must also ask L == M: two calls, where m <= l stopped after one.

Overload an operator only where its meaning is obvious

An operator is a promise that the code means what the symbol means in school arithmetic or in the field the class models. Keep it, and code reads like the formula it implements; break it, and every reader has to look up what + does this time.

Overload an operator when Write a named method when
The type is a number, vector, matrix, money or a set-like collection The meaning would have to be guessed, such as user + group
The operator keeps its usual laws, such as a + b == b + a The operation has side effects, such as sending or saving
Readers of the field already write it that way, as a @ b for matrices It needs options or can fail in several ways

The standard library follows the same line: pathlib overloads / to join paths, sets and dictionaries overload | for union and merging, and subtracting two dates gives a timedelta, because in each case the meaning is clear.

Python Online Compiler Remove __rmul__ from Vector2 or __radd__ from SummableMoney and read the TypeError that replaces the answer.

Checkpoint

Check: When to overload

  1. Question 1 of 2 Which of these is the clearest use of an overloaded operator?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: v + w for two vectors, giving a new vector

    Overload an operator when the type is a number, vector, matrix, money or a set-like collection and the operator keeps its usual meaning; write a named method for anything with side effects or a meaning to guess.

  2. Question 2 of 2 What does pathlib use the / operator for?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Joining paths

    The standard library overloads operators only where the meaning is clear: / joins paths, | merges sets and dictionaries, and subtracting two dates gives a timedelta.

Check yourself

Practice that adapts to you

Practise this lesson

14 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 In a + b, type(a).__add__(a, b) returns NotImplemented. What does Python do next?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: It calls type(b).__radd__(b, a)

    NotImplemented hands the operation to the right operand's reflected method. Only when that also returns NotImplemented, or does not exist, does Python raise TypeError.

  2. Question 2 of 14 What does vector2.py print?

    What does this program print? Choose one answer.

    import math
    from numbers import Real
    
    
    class Vector2:
        def __init__(self, x, y):
            self.x, self.y = x, y
    
        def __repr__(self):
            return f"Vector2({self.x!r}, {self.y!r})"
    
        def __eq__(self, other):
            if not isinstance(other, Vector2):
                return NotImplemented
            return (self.x, self.y) == (other.x, other.y)
    
        def __hash__(self):
            return hash((self.x, self.y))
    
        def __add__(self, other):
            if not isinstance(other, Vector2):
                return NotImplemented
            return Vector2(self.x + other.x, self.y + other.y)
    
        def __sub__(self, other):
            if not isinstance(other, Vector2):
                return NotImplemented
            return Vector2(self.x - other.x, self.y - other.y)
    
        def __mul__(self, k):
            if not isinstance(k, Real):
                return NotImplemented
            return Vector2(self.x * k, self.y * k)
    
        __rmul__ = __mul__  # k * v means the same as v * k; it runs after int or float gives up
    
        def __neg__(self):
            return Vector2(-self.x, -self.y)
    
        def __abs__(self):
            return math.hypot(self.x, self.y)
    
    
    v, w = Vector2(3, 4), Vector2(1, -2)
    print(v + w, v - w, -w)
    print(v * 2, 2 * v, 0.5 * v)
    print(abs(v), v + w == Vector2(4, 2))
    
    for label, attempt in (("v + 1", lambda: v + 1), ("v * w", lambda: v * w)):
        try:
            attempt()
        except TypeError as error:
            print(f"{label}: TypeError: {error}")
    Show the answer: Question 2 of 14

    Answer: it prints

    Vector2(4, 2) Vector2(2, 6) Vector2(-1, 2)
    Vector2(6, 8) Vector2(6, 8) Vector2(1.5, 2.0)
    5.0 True
    v + 1: TypeError: unsupported operand type(s) for +: 'Vector2' and 'int'
    v * w: TypeError: unsupported operand type(s) for *: 'Vector2' and 'Vector2'

    2 * v works because int.__mul__ returns NotImplemented for a vector and Python then calls Vector2.__rmul__. v * w fails: __mul__ accepts only real numbers, the reflected call on w refuses too, and the message names both operand types.

  3. Question 3 of 14 A teammate wants Vector2.__add__ to raise TypeError for unknown operands, "to fail fast". What does that break?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: The other operand never gets its turn, so a type written later that knows how to add itself to a vector cannot answer

    Returning NotImplemented is the polite answer, "not with this operand". Raising TypeError yourself cuts off the other operand's chance to answer, and returning a wrong value such as None or False hides a bug.

  4. Question 4 of 14 Which method does a // b call first?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: type(a).__floordiv__(a, b)

    Each arithmetic operator has a method and a reflected twin: // calls __floordiv__ on the left operand first and __rfloordiv__ on the right one if that gives up.

  5. Question 5 of 14 Money defines __add__ but no __iadd__. What does this print?

    Read the code, then choose one answer.

    total = saved = Money(100)
    total += Money(50)
    print(total.minor, saved.minor, total is saved)
    Show the answer: Question 5 of 14

    Answer: 150 100 False

    Without __iadd__, total += x runs total = total + x: a new object is bound to total, and saved still refers to the old one.

  6. Question 6 of 14 What happens when this runs?

    Read the code, then choose one answer.

    class Point:
        def __init__(self, x):
            self.x = x
    
        def __sub__(self, other):
            return Point(self.x - other.x)
    
    p = Point(3)
    print(-p)
    Show the answer: Question 6 of 14

    Answer: TypeError: bad operand type for unary -: 'Point'

    -a calls __neg__, +a calls __pos__ and abs(a) calls __abs__. They take one operand, so there is no other operand to ask, and a class without the method raises TypeError.

  7. Question 7 of 14 sum(prices) raises unsupported operand type(s) for +: 'int' and 'Money'. Which changes make it work?

    Choose every answer that is right.

    Show the answer: Question 7 of 14

    Answer:

    • Call sum(prices, Money(0)), so the sum starts from an amount
    • Give Money a __radd__ that returns self when the other operand is the int 0

    sum() starts from the int 0, so its first step is 0 + prices[0], which only the right operand's __radd__ can answer. Starting from an amount avoids that step altogether.

  8. Question 8 of 14 Why does sum(bill) fail on its very first addition rather than later?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: Its first addition is 0 + bill[0], with the int 0 on the left

    The running total starts as the int 0, so the first addition is 0 + bill[0]. Without a reflected method on the amount, that first step raises TypeError.

  9. Question 9 of 14 SummableMoney.__radd__ returns the amount itself, self, for the int 0, not a copy. Why is that safe?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: Amounts are value types that no operation changes, so sharing one object between two names is harmless

    Value types return new objects from every operator and never change in place, so returning self from __radd__ cannot let a later operation change the item. The same shortcut would be a bug in a mutable type.

  10. Question 10 of 14 What must a class define for @functools.total_ordering to fill in the other comparisons?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: __eq__ and one of __lt__, __le__, __gt__ or __ge__

    The decorator derives the missing comparisons from __eq__ and the one ordering method you wrote, for example a <= b from a < b or a == b.

  11. Question 11 of 14 With the ShirtSize class of shirt_sizes.py, what does this print?

    Read the code, then choose one answer.

    sizes = [ShirtSize("XL"), ShirtSize("S"), ShirtSize("M")]
    print(max(sizes), sorted(sizes)[0])
    Show the answer: Question 11 of 14

    Answer: ShirtSize('XL') ShirtSize('S')

    __lt__ compares positions in the tuple ORDER, so sizes sort as the shop sells them, not alphabetically, and max() and sorted() both work.

  12. Question 12 of 14 The shop adds a size XXS, which must sort before XS. What do you change in ShirtSize?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Put "XXS" first in the tuple ORDER, and nothing else

    The whole order lives in one tuple, and __lt__ reads it. A new size is one change in one place, which is the advantage of deriving every comparison from a single method.

  13. Question 13 of 14 A team wants group + user to add a user to a group and save the change to the database. What do you suggest?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: A named method, such as group.add_member(user), because the meaning would be guessed and the operation has side effects

    Overload an operator when the type is a number, vector, matrix, money or a set-like collection and the operator keeps its usual laws. An operation with side effects, options or several ways to fail is clearer as a method.

  14. Question 14 of 14 Which law should an overloaded + on a number-like type keep, so that readers can trust it?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: a + b == b + a, the law readers expect from school arithmetic

    An operator is a promise that the code means what the symbol means. Keep its usual laws, such as a + b == b + a, or write a named method instead.

In-place operators, subclass dispatch, query traps and Python 3.14, reproduced Pro: opens with a Pro, Premium or Ultimate pass

Four behaviours of operators surprise even experienced programmers: what += does to other names for the same object, which method runs first when a subclass is involved, what and, or and precedence do to conditions built from operators, and two changes in Python 3.14. Each is reproduced below with code you can run.

In this part:

  • In-place addition with and without its method, and the aliasing it causes
  • Who answers first: the reflected method of a subclass
  • and, or and precedence: the query-builder traps, reproduced
  • Two changes in Python 3.14

2 diagrams · 4 runnable examples (Python) · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Number types done right: reflected order, hashing like ints and a Ratio checked against Fraction Pro: opens with a Pro, Premium or Ultimate pass

A class that behaves like a number has to agree with Python’s own numbers: in the order of operands, in hashing and in every result. This part reproduces the reflected-subtraction bug with a random check, shows why a number equal to 2 must hash like 2, compares what Fraction and Decimal do with floats, and ends with an exact Ratio type tested against fractions.Fraction.

In this part:

  • Reflected subtraction: the argument-order bug, caught by a random check
  • Numbers that equal ints must hash like ints, reproduced
  • Floats and decimals: what the standard library decides, and why
  • Exercise: an exact Ratio type
  • An exact Ratio type that agrees with fractions.Fraction
    • Results of the sample tests

3 runnable examples (Python) · 1 exercise with sample tests · about 8 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between returning NotImplemented and raising NotImplementedError?

    They are unrelated apart from the name. NotImplemented is a special value that a binary special method such as __add__ or __eq__ returns to say "I cannot handle this operand": Python then tries the other operand's reflected method, such as __radd__, and raises TypeError only when both sides give up, or falls back to identity for ==. NotImplementedError is an exception: you raise it in a method that subclasses must override, so a call that reaches the base version fails clearly. Mixing them up breaks things. Raising NotImplemented fails, because it is not an exception, and returning NotImplementedError from __add__ would make a + b evaluate to the exception class itself. Raising NotImplementedError from __add__ is a mistake too: the error ends the expression before the other operand is asked.

  2. Fresher to mid

    sum(prices) works on a list of floats but raises TypeError on a list of your Money objects. Explain the sequence of calls that leads to the error, give two fixes, and say which one you prefer and why.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A teammate added __iadd__ to an immutable Money value type "for speed": it adds to self.minor and returns self. What can go wrong? Describe a concrete failure and how you would review the change.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Your Ratio type compares equal to ints, so Ratio(4, 2) == 2 is True, and both kinds end up as keys of one dictionary. What must __hash__ satisfy, how do you implement it, and how do you test it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • a + b calls type(a).__add__(a, b); when that returns NotImplemented, Python calls type(b).__radd__(b, a), and raises TypeError when both give up.
  • Return NotImplemented for operands a method does not understand; never raise from it or return None.
  • Reflected methods make 2 * v and sum() work; sum(items, start) avoids the integer 0 altogether.
  • functools.total_ordering derives the missing comparisons from __eq__ and one ordering method.

Terms in this lesson

operator overloading
Giving Python's operators a meaning for your own classes by defining special methods: a + b calls type(a).__add__(a, b), a < b calls __lt__ and -a calls __neg__.
reflected method
The method Python calls on the right operand when the left operand's method returns NotImplemented, such as __radd__ for + and __rmul__ for *. In 2 * v, int.__mul__ gives up and the vector's __rmul__ answers.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.