Operator Overloading: __add__, __eq__, __lt__

RoadmapsPython

Overview

In Python, operators like `+`, `==`, and `<` are syntactic sugar for method calls on objects. When you run `a + b`, Python translates it to `a.__add__(b)`. Operator overloading allows you to define these double-underscore (dunder) methods in your own classes. class Point: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Point(self.x + other.x, self.y + other.y) Think of operators as universal remotes: the button `+` is standard, but the specific device (the object) decides what action is taken when the button is pressed. This integration lets custom objects work seamlessly with built-in functions like `sorted()`, which requires `__lt__` (less than) to be defined. To implement operator overloading properly, you must follow specific rules for each method: - `__eq__(self, other)`: Compares two objects for equality. Returns a boolean, or `NotImplemented` if the type is unknown. It does not raise exceptions on type mismatch. - `__lt__(self, other)`: Compares if `self` is less than `other`. Returns a boolean, or `NotImplemented`. Python raises a `TypeError` if the fallback fails. - `__add__(self, other)`: Adds two objects. Returns a completely new instance of the object. Python raises a `TypeError` if neither operand supports the addition. When a dunder method doesn't recognize the other operand's type, it must return the singleton `NotImplemented`. This acts as a signal for Python to try the reverse operation (like `__radd__` or `__req__`) on the right-hand operand, enabling seamless interoperability between different types. # Trap: Raising an error prevents the fallback mechanism def __add__(self, other): if not isinstance(other, Point): raise TypeError("Expected Point") return Point(self.x + other.x, self.y + other.y) Raising an error instead of returning `NotImplemented` is a common edge case that breaks Python's fallback mechanism. If your method hard-fails, the right-hand operand never gets a chance to resolve the operation using its own dunder methods.

It allows custom objects to integrate seamlessly with Python's built-in syntax and functions, making code cleaner and more expressive. It also enables objects to be sorted or compared for equality without writing custom method names.

Where used: Pandas, NumPy, Pathlib

Why learn this

Code walkthrough

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

  def __add__(self, other):
    return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2
print(p3.x, p3.y)

Focus: p3 = p1 + p2

Aha moment

class Box:
  def __init__(self, value):
    self.value = value

b1 = Box(10)
b2 = Box(10)
print(b1 == b2)

Prediction: What does this print?

Common guess: `True`

By default, custom classes use object identity for equality (like `is`), so two separate instances evaluate to `False`. You must explicitly implement `__eq__` to compare their values.

Common mistakes

Glossary

syntactic sugar
Syntax in a programming language that makes things easier to read or to express, but doesn't add any new functionality. Example: `a + b`.
operand
The object or value that an operator (like + or -) works on or manipulates. Example: `3 + 5`.

Recall questions

Understanding checks

What is the output of this script?

15, 15

The `__add__` method mutates `self` instead of creating and returning a completely new instance. Thus, both `p1` and `p3` refer to the same object, and `p1` has been permanently modified by the addition.

When defining the `__eq__` method, what is the correct way to handle comparison with an unsupported type?

Return the singleton `NotImplemented`. This tells Python to try the reverse operation on the other object before falling back to default behavior.

Returning `NotImplemented` tells Python that this method does not know how to handle the other operand's type. Python will then attempt to call the reverse operation (e.g., `__req__` or the other object's `__eq__`) before finally falling back to default behavior.

Practice tasks

Refactor User Equality

The `User` class currently uses the default equality behavior (comparing memory identity). Modify the `__eq__` method so that two `User` instances are considered equal (`==`) if they have the same `user_id`. Also ensure that comparing a `User` with a non-`User` object returns `NotImplemented` instead of raising an error or returning `False`.

Challenge

Vector Mathematics

Write a `Vector` class that initializes with `x` and `y` components. Implement `__add__` to add two vectors together returning a new `Vector`. Also implement `__eq__` so that two vectors are equal if their `x` and `y` components are identical. Ensure that if `__add__` or `__eq__` is used with a non-`Vector` object, it returns `NotImplemented`.

Continue learning

Previous: Everything is an object in Python

Previous: is vs == : Identity vs Equality

Return to Python Roadmap