Skip to content
aviral gupta

// A1.2 · ~30 min · Advanced

Operator overloading

After this lesson you can make +, * and the comparison operators work on your own classes, including sum(), 3 * x and sorted().

Lesson 2 of 6 in A1 The data model

You will be able to

  • Implement __add__, __radd__ and __mul__, returning NotImplemented for operands you do not support
  • Predict when Python calls a reflected method such as __radd__, and what += does
  • Define ordering with rich comparisons or with functools.total_ordering
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson A1.1: __eq__ returns NotImplemented when the other object is not its type. What does a == b do next?

  2. Predict · Activity 2 of 7

    Predict before you read on: Money supports Money + Money. What happens here?

    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __add__(self, other):
            if not isinstance(other, Money):
                return NotImplemented
            return Money(self.cents + other.cents)
    
    
    total = sum([Money(100), Money(250)])
    print(total.cents)
  3. Practice · Activity 3 of 7

    Money(5) * 3 already works. Name the method that makes 3 * Money(5) work too.

    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __mul__(self, factor):
            if not isinstance(factor, int):
                return NotImplemented
            return Money(self.cents * factor)
    
        def ____(self, factor):
            return self.__mul__(factor)
    def (self, factor):
  4. Practice · Activity 4 of 7

    Money defines only __lt__. What does this print?

    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __lt__(self, other):
            return self.cents < other.cents
    
    
    print(Money(5) > Money(3))
  5. Practice · Activity 5 of 7

    a and b are instances of a class with __add__, __radd__ and __lt__, and nothing else. Match each expression to the call Python makes.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. total_ordering fills in the missing comparisons. What does this print?

    from functools import total_ordering
    
    
    @total_ordering
    class Money:
        def __init__(self, cents):
            self.cents = cents
    
        def __lt__(self, other):
            return self.cents < other.cents
    
    
    print(Money(5) <= Money(5))
  7. Apply · Activity 7 of 7

    Mini-task. Write a class Distance(metres) whose objects add with +, work with sum(), sort with sorted() and compare with all four ordering operators. Use total_ordering. Try sum, sorted and max on Distance(1200), Distance(800) and Distance(450).

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A Money type with arithmetic and ordering

Money adds with + and sum(), multiplies by whole numbers from either side, and orders through total_ordering. It refuses to mix currencies with a ValueError, because that is a wrong value, but answers NotImplemented for a float, so Python raises the usual TypeError. Change the basket or try Money(1) * 1.5.

main.py

from functools import total_ordering


@total_ordering
class Money:
    """An amount in cents of one currency."""

    def __init__(self, cents: int, currency: str = "EUR") -> None:
        self.cents = cents
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.cents}, {self.currency!r})"

    def _check(self, other: "Money") -> None:
        if other.currency != self.currency:
            raise ValueError(f"cannot mix {self.currency} and {other.currency}")

    def __add__(self, other: object) -> "Money":
        if isinstance(other, int) and other == 0:  # sum() starts with 0
            return self
        if not isinstance(other, Money):
            return NotImplemented
        self._check(other)
        return Money(self.cents + other.cents, self.currency)

    __radd__ = __add__

    def __mul__(self, factor: object) -> "Money":
        if not isinstance(factor, int):
            return NotImplemented
        return Money(self.cents * factor, self.currency)

    __rmul__ = __mul__

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.cents, self.currency) == (other.cents, other.currency)

    def __lt__(self, other: "Money") -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        self._check(other)
        return self.cents < other.cents

    def __hash__(self) -> int:
        return hash((self.cents, self.currency))


basket = [Money(450), Money(120), Money(999)]
print("Total:", sum(basket))
print("Three coffees:", 3 * Money(320))
print("Cheapest first:", sorted(basket))
print("Over 5 EUR:", [m for m in basket if m >= Money(500)])

try:
    Money(100) + 2.5
except TypeError as err:
    print("TypeError:", err)

try:
    Money(100) < Money(100, "USD")
except ValueError as err:
    print("ValueError:", err)

Run it with

python main.py

Output

Total: Money(1569, 'EUR')
Three coffees: Money(960, 'EUR')
Cheapest first: [Money(120, 'EUR'), Money(450, 'EUR'), Money(999, 'EUR')]
Over 5 EUR: [Money(999, 'EUR')]
TypeError: unsupported operand type(s) for +: 'Money' and 'float'
ValueError: cannot mix EUR and USD
  • sum() works because __radd__ = __add__ accepts the starting 0.
  • 3 * Money(320) goes to __rmul__, because int.__mul__ returns NotImplemented for Money.
  • m >= Money(500) comes from total_ordering: the class only wrote __eq__ and __lt__.
  • Money + 2.5 returns NotImplemented, float has no answer either, so Python raises TypeError.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Adding durations

Make Duration in main.py support Duration + Duration, sum() over a list of durations, and multiplication by an int from either side: Duration(15) * 3 and 3 * Duration(15). Always return a new Duration. For any other operand, return NotImplemented, so Duration(5) + "x" raises TypeError.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    sum() computes 0 + first item, so __radd__ must accept the int 0.

  2. Hint 2

    In __add__, handle a Duration, then the int 0 (return self), and return NotImplemented for everything else. Then write __radd__ = __add__.

  3. Hint 3

    __mul__ checks isinstance(factor, int) and returns Duration(self.minutes * factor); __rmul__ = __mul__ covers 3 * Duration(15).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Duration:
    """A length of time in whole minutes."""

    def __init__(self, minutes: int) -> None:
        self.minutes = minutes

    def __repr__(self) -> str:
        return f"Duration({self.minutes})"

    def __add__(self, other: object) -> "Duration":
        if isinstance(other, Duration):
            return Duration(self.minutes + other.minutes)
        if isinstance(other, int) and other == 0:
            return self
        return NotImplemented

    __radd__ = __add__

    def __mul__(self, factor: object) -> "Duration":
        if not isinstance(factor, int):
            return NotImplemented
        return Duration(self.minutes * factor)

    __rmul__ = __mul__


if __name__ == "__main__":
    print(sum([Duration(30), Duration(45)]), 2 * Duration(15))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Duration:
    """A length of time in whole minutes."""

    def __init__(self, minutes: int) -> None:
        self.minutes = minutes

    def __repr__(self) -> str:
        return f"Duration({self.minutes})"

    # Add __add__ (Duration + Duration, and Duration + 0 for sum()),
    # __radd__, __mul__ (Duration * int) and __rmul__ (int * Duration).
    # Return NotImplemented for any other operand.


if __name__ == "__main__":
    print(sum([Duration(30), Duration(45)]), 2 * Duration(15))

test_main.py

from main import Duration


def test_add():
    """Duration + Duration adds the minutes"""
    got = (Duration(30) + Duration(45)).minutes
    assert got == 75, f"Duration(30) + Duration(45) has {got} minutes, expected 75"


def test_sum():
    """sum() of durations works"""
    got = sum([Duration(10), Duration(20), Duration(30)]).minutes
    assert got == 60, f"sum() gave {got} minutes, expected 60: sum() starts with 0 + Duration(10)"


def test_multiply_both_sides():
    """Duration * 3 and 3 * Duration both work"""
    got = (Duration(15) * 3).minutes, (3 * Duration(15)).minutes
    assert got == (45, 45), f"got {got!r} minutes, expected (45, 45)"


def test_other_types():
    """Adding a string raises TypeError, because __add__ returns NotImplemented"""
    assert Duration(5).__add__("x") is NotImplemented, "Duration(5).__add__('x') should return NotImplemented"
    try:
        Duration(5) + "x"
    except TypeError:
        return
    raise AssertionError("Duration(5) + 'x' did not raise TypeError")

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Ordering version numbers

Version(1, 10, 0) must sort after Version(1, 2, 3). Decorate the class with @total_ordering and add __eq__ and __lt__ that compare (major, minor, patch). Both return NotImplemented for other types, so Version(1, 0, 0) < "2" raises TypeError. Add a matching __hash__, since you define __eq__.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Tuples compare item by item, so (1, 2, 3) < (1, 10, 0) is True, unlike the strings "1.2.3" and "1.10.0".

  2. Hint 2

    A helper def _key(self) returning (self.major, self.minor, self.patch) keeps __eq__, __lt__ and __hash__ in step.

  3. Hint 3

    Put @total_ordering on the line above class Version. It then derives <=, > and >= from __eq__ and __lt__.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from functools import total_ordering


@total_ordering
class Version:
    """A version number such as 1.10.2, ordered part by part."""

    def __init__(self, major: int, minor: int, patch: int) -> None:
        self.major = major
        self.minor = minor
        self.patch = patch

    def __repr__(self) -> str:
        return f"Version({self.major}, {self.minor}, {self.patch})"

    def _key(self) -> tuple[int, int, int]:
        return (self.major, self.minor, self.patch)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return NotImplemented
        return self._key() == other._key()

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return NotImplemented
        return self._key() < other._key()

    def __hash__(self) -> int:
        return hash(self._key())


if __name__ == "__main__":
    print(sorted([Version(1, 10, 0), Version(1, 2, 3)]))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from functools import total_ordering


class Version:
    """A version number such as 1.10.2, ordered part by part."""

    def __init__(self, major: int, minor: int, patch: int) -> None:
        self.major = major
        self.minor = minor
        self.patch = patch

    def __repr__(self) -> str:
        return f"Version({self.major}, {self.minor}, {self.patch})"

    # Decorate the class with @total_ordering, then add __eq__ and __lt__.
    # Both return NotImplemented when other is not a Version.


if __name__ == "__main__":
    print(sorted([Version(1, 10, 0), Version(1, 2, 3)]))

test_main.py

from main import Version


def test_sorted():
    """1.2.3 sorts before 1.10.0, which is not the string order"""
    got = sorted([Version(1, 10, 0), Version(1, 2, 3), Version(0, 9, 9)])
    want = [Version(0, 9, 9), Version(1, 2, 3), Version(1, 10, 0)]
    assert got == want, f"sorted() gave {got!r}, expected {want!r}"


def test_all_four_orderings():
    """<, <=, > and >= all work"""
    a, b = Version(1, 2, 3), Version(1, 2, 4)
    got = a < b, a <= b, b > a, b >= a, a <= Version(1, 2, 3)
    assert got == (True, True, True, True, True), f"the comparisons gave {got!r}, all should be True"


def test_equality():
    """Equal parts mean equal versions; a string is never equal"""
    assert Version(2, 0, 0) == Version(2, 0, 0), "Version(2, 0, 0) == Version(2, 0, 0) is False"
    assert Version(2, 0, 0) != "2.0.0", "a Version should not equal the string '2.0.0'"


def test_other_types():
    """Ordering against a string raises TypeError"""
    try:
        Version(1, 0, 0) < "2"
    except TypeError:
        return
    raise AssertionError("Version(1, 0, 0) < '2' did not raise TypeError: return NotImplemented for other types")

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

sum() without __radd__

class Money:
    def __init__(self, cents):
        self.cents = cents

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return Money(self.cents + other.cents)


total = sum([Money(100), Money(250)])

What Python prints

TypeError: unsupported operand type(s) for +: 'int' and 'Money'

Why, and the fix

sum() starts with 0, and 0 + Money(100) needs Money.__radd__. Accept 0 in __add__ and add __radd__ = __add__, or give sum() a start value: sum(items, Money(0)).

total_ordering without an ordering method

from functools import total_ordering


@total_ordering
class Version:
    def __init__(self, major):
        self.major = major

    def __eq__(self, other):
        return self.major == other.major

What Python prints

ValueError: must define at least one ordering operation: < > <= >=

Why, and the fix

total_ordering derives the missing comparisons from one it is given. __eq__ does not count; define one of __lt__, __le__, __gt__ or __ge__ as well. The error appears when the class is created, before any object exists.

__add__ that changes self and returns nothing

class Money:
    def __init__(self, cents):
        self.cents = cents

    def __add__(self, other):
        self.cents += other.cents


total = Money(100) + Money(250)
print(total.cents)

What Python prints

AttributeError: 'NoneType' object has no attribute 'cents'

Why, and the fix

An operator method must return its result; without return, x + y evaluates to None. It should also leave both operands unchanged: return Money(self.cents + other.cents). Changing self in place is the job of __iadd__, which serves +=.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Binary operators and NotImplemented

x + y calls type(x).__add__(x, y); * calls __mul__, - calls __sub__. Return a new object and leave both operands unchanged. When the other operand has a type you do not support, return NotImplemented; do not raise and do not return False. Python then tries the other operand, and if nobody handles it, raises the familiar TypeError: unsupported operand type(s) for +.

Reflected methods and +=

If x.__add__(y) is missing or returns NotImplemented and y has a different type, Python calls y.__radd__(x). That is how 3 * money and sum() work: sum() starts with 0, so the first step is 0 + item, and int cannot add your class. For a commutative operation, __radd__ = __add__ in the class body is enough. x += y calls __iadd__ if it exists; otherwise it falls back to x = x + y, which binds x to a new object.

Rich comparisons and total_ordering

<, <=, >, >= call __lt__, __le__, __gt__, __ge__. __lt__ and __gt__ are each other's reflection, so with only __lt__, a > b still works through b < a. But Python implies nothing else: <= is not derived from < and ==. functools.total_ordering fills in the rest from __eq__ plus one ordering method. Without __eq__, the derived <= falls back to identity, and Money(5) <= Money(5) is False.

Sources

Last reviewed September 29, 2026