Lesson 29 โข Advanced
Operator Overloading & Custom Behaviours
Master operator overloading to make your classes behave like built-in types. Learn the techniques used by NumPy, PyTorch, Pandas, and SQLAlchemy to create intuitive, powerful APIs through custom operator behaviors.
๐ฅ Operator Overloading & Custom Behaviours
Operator overloading lets your classes behave like built-in types:
- โ Numbers
- โ Strings
- โ Lists
- โ Dictionaries
- โ Booleans
- โ Comparisons
- โ Arithmetic
- โ Indexing
- โ Iteration
Real frameworks rely on this heavily:
- NumPy โ vector arithmetic
- PyTorch โ tensor operations
- SQLAlchemy โ query expressions
- Pandas โ Series/DataFrame math
- Pathlib โ path addition
- Datetime โ duration arithmetic
If you want to build professional-grade classes, operator overloading is mandatory.
โ๏ธ 1. Why Operator Overloading Exists
Python allows classes to define behavior for:
+ - * / // % **
== != < <= > >=
[] indexing
in membership
len(), bool(), iter(), call(), with
You override these by implementing "magic methods" such as:
__add__,__sub__,__mul__,__truediv____eq__,__lt____getitem__,__setitem____contains__,__len__,__bool__,__call__
These make your objects feel native and intuitive.
๐ข 2. Overloading Arithmetic Operators
Let's build a simple 2D vector class with all arithmetic operators:
Vector Arithmetic
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# Addition โ __add__
def __add__(self, other):
return Vec(self.x + other.x, self.y + other.y)
# Subtraction โ __sub__
def __sub__(self, other):
return Vec(self.x - other.x, self.y - other.y)
# Multiplication โ __mul__ (scalar)
def __mul__(self, scalar):
return Vec(self.x * scalar, self.
...๐ง 3. Overloading String & Representation Functions
Two methods control how your objects appear in logs, REPLs, and print statements:
__repr__ and __str__
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
# Developer view (unambiguous)
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# User-friendly view
def __str__(self):
return f"({self.x}, {self.y})"
v = Vec(10, 20)
# __repr__ is used by repr() and in collections
print(f"repr(v): {repr(v)}")
# __str__ is used by str() and print()
print(f"str(v): {str(v)}")
print(f"print(v):", v)
# In a list, __repr__ is used
print(f
...๐งฉ 4. Comparison Operators
To support sorting, filtering, searching, or ordering, implement:
__eq__โ ==__ne__โ !=__lt__โ <__le__โ <=__gt__โ >__ge__โ >=
Comparison Operators
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
def magnitude(self):
return (self.x**2 + self.y**2) ** 0.5
# Compare by magnitude
def __lt__(self, other):
return self.magnitude() < other.magnitude()
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# Test comparisons
v1 = Vec(3, 4) # magnitude = 5
v2 = Vec(1, 1) #
...๐ฆ 5. Overloading Indexing & Slicing
Indexing and Slicing
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
def __getitem__(self, idx):
if isinstance(idx, slice):
return (self.x, self.y)[idx]
if idx == 0:
return self.x
if idx == 1:
return self.y
raise IndexError("Vec only has 2 dimensions")
def __setitem__(self, idx, value):
if idx == 0:
self.x = val
...๐ 6-7. Membership & Iteration
__contains__ and __iter__
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# Membership test (in)
def __contains__(self, val):
return val == self.x or val == self.y
# Make iterable
def __iter__(self):
yield self.x
yield self.y
# Length
def __len__(self):
return 2
# Test
v = Vec(10, 20)
# Membership
print(f"v = {v}")
print(f"10 in v: {10 in v}")
print
...๐ฅ 8-9. Boolean & Call Behavior
__bool__ and __call__
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# Boolean behavior
def __bool__(self):
return bool(self.x or self.y)
# Make callable
def __call__(self, scale):
return Vec(self.x * scale, self.y * scale)
# Test boolean
zero = Vec(0, 0)
nonzero = Vec(1, 2)
print(f"bool({zero}): {bool(zero)}")
print(f"bool({nonzero}): {bool(nonzero)}")
if nonzero:
pr
...โก 10-11. Reverse & Augmented Operators
Reverse and Augmented Operators
class Vec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# Regular add
def __add__(self, other):
if isinstance(other, Vec):
return Vec(self.x + other.x, self.y + other.y)
return Vec(self.x + other, self.y + other)
# Reverse add: when left operand doesn't support +
def __radd__(self, other):
return self.__add__(other)
# Augmented add:
...๐ฏ 12-13. Rich Container & Matrix Indexing
Container Behaviors
class Matrix:
def __init__(self, rows):
self.data = rows
def __repr__(self):
return f"Matrix({self.data})"
def __len__(self):
return len(self.data)
def __iter__(self):
for row in self.data:
yield row
def __contains__(self, item):
return any(item in row for row in self.data)
# Multi-dimensional indexing like NumPy
def __getitem__(self, key):
if isinstance(key, tuple):
row,
...๐ 14-16. Bitwise & Context Operators
Bitwise and Context Managers
class Permission:
"""Bitwise operators for permission flags."""
def __init__(self, value):
self.value = value
def __repr__(self):
return f"Permission({self.value})"
def __or__(self, other):
return Permission(self.value | other.value)
def __and__(self, other):
return Permission(self.value & other.value)
def __xor__(self, other):
return Permission(self.value ^ other.value)
def __invert__(self):
ret
...๐ 17-19. DSL Building with Operators
Massive libraries rely on operator overloads to create readable "fake languages":
- SQLAlchemy:
User.age > 18 - PyTorch:
x = tensor * 3 + tensor2 - Pandas:
df["col"] + df["other"] - Pathlib:
path = Path("home") / "user"
Building DSLs
# Simple expression DSL like SQLAlchemy
class Column:
def __init__(self, name):
self.name = name
def __gt__(self, value):
return f"{self.name} > {value}"
def __lt__(self, value):
return f"{self.name} < {value}"
def __eq__(self, value):
return f"{self.name} = '{value}'"
# SQLAlchemy-style expressions
User = type('User', (), {
'age': Column('age'),
'name': Column('name'),
'active': Column('active')
})()
query1 = User.age
...๐งฉ 20-22. Attribute Access & Delegation
Attribute Access Control
class Proxy:
"""Intercept all attribute access."""
def __init__(self, target):
object.__setattr__(self, '_target', target)
def __getattr__(self, name):
print(f"Getting: {name}")
return getattr(self._target, name)
def __setattr__(self, name, value):
if name == '_target':
object.__setattr__(self, name, value)
else:
print(f"Setting: {name} = {value}")
setattr(self._target, name, value)
class Data:
...๐ฅ 23-24. Complete Mathematical System
Complete Vector Class
class Vec:
"""Fully overloaded 2D vector."""
__slots__ = ('x', 'y', '__weakref__')
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vec({self.x}, {self.y})"
# Arithmetic
def __add__(self, other):
if isinstance(other, Vec):
return Vec(self.x + other.x, self.y + other.y)
return Vec(self.x + other, self.y + other)
__radd__ = __add__
def __sub__(self, other):
...๐งต 25-27. Immutable vs Mutable Design
Immutable vs Mutable
# Immutable (returns new object)
class ImmutableVec:
__slots__ = ('_x', '_y')
def __init__(self, x, y):
object.__setattr__(self, '_x', x)
object.__setattr__(self, '_y', y)
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def __repr__(self):
return f"ImmutableVec({self._x}, {self._y})"
def __add__(self, other):
return ImmutableVec(self._x + other._x, self._y + other.
...โก 28-30. Computation Graphs & Best Practices
Computation Graph Pattern
# TensorFlow/PyTorch-style computation graph
class Node:
def __add__(self, other):
return Add(self, other)
def __mul__(self, other):
return Mul(self, other)
class Var(Node):
def __init__(self, name, value=None):
self.name = name
self.value = value
def __repr__(self):
return self.name
class Add(Node):
def __init__(self, a, b):
self.a = a
self.b = b
def __repr__(self):
return f"({self.a} + {sel
...๐ฏ Summary โ You Now Understand the Full Python Operator Model
You now understand:
- โ Operator dispatching
- โ Delegation
- โ Emulating built-ins
- โ Full numerical operator suite
- โ Boolean overloading
- โ Callable objects
- โ Immutable vs mutable design
- โ DSL construction
- โ Computation graph building
- โ Best practices for errors
- โ Weakref integration
This puts you at a framework engineer level (the people who build NumPy/Pandas/PyTorch, not just use them).
๐ Quick Reference โ Operator Overloading
| Method | Operator |
|---|---|
| __add__(self, other) | a + b |
| __eq__(self, other) | a == b |
| __lt__(self, other) | a < b (enables sorting) |
| __mul__(self, other) | a * b |
| @functools.total_ordering | Auto-fill comparison methods |
๐ Great work! You've completed this lesson.
Your classes can now support arithmetic, comparisons, and other operators โ making your APIs as intuitive as built-in Python types.
Up next: Mixins โ compose reusable behaviour across multiple classes without deep inheritance chains.
Sign up for free to track which lessons you've completed and get learning reminders.