Operator Overloading
Reviewed & published by Brayan K
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.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
🔥 Operator Overloading & Custom Behaviours
Operator overloading lets your classes behave like built-in types:
- ✔ Dictionaries
- ✔ Comparisons
- ✔ Arithmetic
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:
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:
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.y * scalar)
# Reverse multiplication → __rmul__
def __rmul__(self, scalar):
return self.__mul__(scalar)
# Test
v1 = Vec(3, 4)
v2 = Vec(1, 2)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"3 * v1 = {3 * v1}") # Uses __rmul__
# ✅ Expected output:
# v1 = Vec(3, 4)
# v2 = Vec(1, 2)
# v1 + v2 = Vec(4, 6)
# v1 - v2 = Vec(2, 2)
# v1 * 3 = Vec(9, 12)
# 3 * v1 = Vec(9, 12)🧠 3. Overloading String & Representation Functions
Two methods control how your objects appear in logs, REPLs, and print statements:
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"List: {[Vec(1,2), Vec(3,4)]}")
# Good repr = easier debugging + readable logs
# ✅ Expected output:
# repr(v): Vec(10, 20)
# str(v): (10, 20)
# print(v): (10, 20)
# List: [Vec(1, 2), Vec(3, 4)]🧩 4. Comparison Operators
To support sorting, filtering, searching, or ordering, implement:
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) # magnitude = ~1.4
v3 = Vec(3, 4) # same as v1
print(f"v1 = {v1}, magnitude = {v1.magnitude():.2f}")
print(f"v2 = {v2}, magnitude = {v2.magnitude():.2f}")
print(f"v3 = {v3}, magnitude = {v3.magnitude():.2f}")
print(f"\nv1 < v2: {v1 < v2}")
print(f"v2 < v1: {v2 < v1}")
print(f"v1 == v3: {v1 == v3}")
# Sorting works automatically
vectors = [Vec(3,4), Vec(1,1), Vec(0,5)]
print(f"\nSorted: {sorted(vectors)}")
# ✅ Expected output:
# v1 = Vec(3, 4), magnitude = 5.00
# v2 = Vec(1, 1), magnitude = 1.41
# v3 = Vec(3, 4), magnitude = 5.00
#
# v1 < v2: False
# v2 < v1: True
# v1 == v3: True
#
# Sorted: [Vec(1, 1), Vec(3, 4), Vec(0, 5)]🟢 Worked Example: one small class, eight operators
Here is everything so far on one realistic class. Money is the classic case: you want to add it, multiply it, compare it and print it, and you want the wrong operations to fail loudly rather than silently produce nonsense. Read the comments, run it, then match each printed line to the method that produced it.
# 🟢 WORKED EXAMPLE — one small class, eight operators
from functools import total_ordering
@total_ordering # writes <=, >, >= for you, given __eq__ and __lt__
class Money:
"""An amount in whole pence. Integers keep the maths exact — floats do not."""
def __init__(self, pence):
self.pence = pence
def __str__(self):
# print() and f-strings use this: the human-facing form
return f"£{self.pence / 100:.2f}"
def __repr__(self):
# The REPL, lists and debuggers use this: aim for "how you would rebuild it"
return f"Money({self.pence})"
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # "I cannot handle this" — Python then tries the other side
return self.pence == other.pence
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.pence < other.pence
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
return Money(self.pence + other.pence) # a NEW Money — no surprise mutation
def __radd__(self, other):
# sum() starts from the integer 0, so its first step is 0 + Money.
# Handling that here is the whole reason sum() works on this class.
if other == 0:
return self
return NotImplemented
def __mul__(self, count):
if not isinstance(count, int):
return NotImplemented # Money * Money is meaningless, so refuse it
return Money(self.pence * count)
__rmul__ = __mul__ # 3 * price should mean the same as price * 3
coffee = Money(275)
cake = Money(340)
print(coffee) # __str__
print(repr(coffee)) # __repr__
print([coffee, cake]) # lists print their items with __repr__, never __str__
print(coffee + cake) # __add__
print(coffee * 3) # __mul__
print(3 * coffee) # __rmul__ — the int has no idea what Money is
print(coffee == Money(275)) # __eq__ compares value, not identity
print(coffee < cake) # __lt__
print(coffee >= cake) # total_ordering derived this from __eq__ and __lt__
print(sum([coffee, cake, coffee])) # __radd__
print(sorted([cake, coffee])) # sorting needs nothing but __lt__
print(coffee == "£2.75") # NotImplemented -> Python falls back to "not equal"
# Asking for something genuinely meaningless still raises, and that is the point:
try:
print(coffee + 100) # 100 what? pence? pounds? Refuse to guess.
except TypeError as err:
print("TypeError:", err)
# ✅ Expected output:
# £2.75
# Money(275)
# [Money(275), Money(340)]
# £6.15
# £8.25
# £8.25
# True
# True
# False
# £8.90
# [Money(275), Money(340)]
# False
# TypeError: unsupported operand type(s) for +: 'Money' and 'int'Two details worth stealing. First, return NotImplemented (the value, not the exception) is how you say "I do not know how to do this" — Python then tries the reflected operation on the other object and only raises TypeError if that fails too. Returning False instead would quietly lie. Second, once you define __eq__, Python removes the default __hash__, so Money cannot be a dict key or set member until you add __hash__ = lambda self: hash(self.pence) or a proper method.
🎯 Your Turn: finish the Duration class
Three blanks, one concept each: name the method the + operator looks for, and fill in the two comparisons. Everything else is written for you.
# 🎯 YOUR TURN — fill in the blanks marked with ___
class Duration:
"""A length of time in whole seconds."""
def __init__(self, seconds):
self.seconds = seconds
def __str__(self):
minutes, secs = divmod(self.seconds, 60) # divmod(90, 60) -> (1, 30)
return f"{minutes}m{secs:02d}s" # :02d pads to two digits: 0 -> "00"
# 1) Make "warm_up + run" work.
def ___(self, other): # 👉 which magic method does + look for?
return Duration(self.seconds + other.seconds)
# 2) Make min(), max() and sorted() work. They only ever ask "is a < b?".
def __lt__(self, other):
return self.seconds ___ other.seconds # 👉 which comparison operator belongs here?
# 3) Make a zero-length duration falsy, so "if duration:" reads naturally.
def __bool__(self):
return self.seconds ___ 0 # 👉 anything longer than nothing is True
warm_up = Duration(90)
run = Duration(1830)
print("warm_up :", warm_up)
print("warm_up + run:", warm_up + run)
print("shorter one :", min(run, warm_up))
print("bool(zero) :", bool(Duration(0)))
print("bool(run) :", bool(run))
# ✅ Expected output:
# warm_up : 1m30s
# warm_up + run: 32m00s
# shorter one : 1m30s
# bool(zero) : False
# bool(run) : True📦 5. Overloading Indexing & 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 = value
elif idx == 1:
self.y = value
else:
raise IndexError("Vec only has 2 dimensions")
# Test indexing
v = Vec(10, 20)
print(f"v = {v}")
print(f"v[0] = {v[0]}")
print(f"v[1] = {v[1]}")
# Slicing
print(f"v[:] = {v[:]}")
# Setting
v[0] = 100
print(f"After v[0] = 100: {v}")
# This is how lists, dicts, numpy arrays work internally
# ✅ Expected output:
# v = Vec(10, 20)
# v[0] = 10
# v[1] = 20
# v[:] = (10, 20)
# After v[0] = 100: Vec(100, 20)🔄 6-7. Membership & Iteration
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(f"20 in v: {20 in v}")
print(f"30 in v: {30 in v}")
# Iteration
print(f"\nIteration:")
for n in v:
print(f" {n}")
# Unpacking works too
x, y = v
print(f"\nUnpacked: x={x}, y={y}")
# Length
print(f"len(v) = {len(v)}")
# ✅ Expected output:
# v = Vec(10, 20)
# 10 in v: True
# 20 in v: True
# 30 in v: False
#
# Iteration:
# 10
# 20
#
# Unpacked: x=10, y=20
# len(v) = 2🔥 8-9. Boolean & Call Behavior
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:
print("nonzero is truthy!")
# Test callable
v = Vec(2, 3)
scaled = v(10) # Acts like a function
print(f"\n{v}(10) = {scaled}")
# Used by:
# - ML models (model(x))
# - PyTorch layers
# - Transformers
# - SQL expression builders
# ✅ Expected output:
# bool(Vec(0, 0)): False
# bool(Vec(1, 2)): True
# nonzero is truthy!
#
# Vec(2, 3)(10) = Vec(20, 30)⚡ 10-11. Reverse & 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: +=
def __iadd__(self, other):
if isinstance(other, Vec):
self.x += other.x
self.y += other.y
else:
self.x += other
self.y += other
return self
# Reverse operator test
v = Vec(1, 2)
print(f"v = {v}")
# This calls Vec.__radd__ since int doesn't know Vec
result = 5 + v
print(f"5 + v = {result}")
# Augmented assignment
v2 = Vec(3, 4)
print(f"\nBefore +=: v2 = {v2}")
v2 += Vec(1, 1)
print(f"After v2 += Vec(1,1): {v2}")
# Augmented avoids creating temporary objects → performance boost
# Used in: PyTorch tensors, NumPy ndarrays, game engine vectors
# ✅ Expected output:
# v = Vec(1, 2)
# 5 + v = Vec(6, 7)
#
# Before +=: v2 = Vec(3, 4)
# After v2 += Vec(1,1): Vec(4, 5)🎯 12-13. Rich Container & Matrix Indexing
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, col = key
return self.data[row][col]
return self.data[key]
def __setitem__(self, key, value):
if isinstance(key, tuple):
row, col = key
self.data[row][col] = value
else:
self.data[key] = value
def __delitem__(self, key):
if isinstance(key, tuple):
row, col = key
self.data[row][col] = None
else:
del self.data[key]
# Test
m = Matrix([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
print(f"m[1][2] = {m[1][2]}")
print(f"m[1, 2] = {m[1, 2]}") # NumPy-style indexing!
print(f"\n5 in m: {5 in m}")
print(f"10 in m: {10 in m}")
print(f"\nlen(m) = {len(m)}")
# This is how NumPy, PyTorch, Pandas work internally
# ✅ Expected output:
# m[1][2] = 6
# m[1, 2] = 6
#
# 5 in m: True
# 10 in m: False
#
# len(m) = 3🌀 14-16. Bitwise & Context Operators
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):
return Permission(~self.value)
# Permission flags
READ = Permission(0b100)
WRITE = Permission(0b010)
EXECUTE = Permission(0b001)
combined = READ | WRITE
print(f"READ | WRITE = {combined}")
print(f"Binary: {bin(combined.value)}")
# Context manager example
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
print("Timer started")
return self
def __exit__(self, exc_type, exc, traceback):
import time
duration = time.perf_counter() - self.start
print(f"Timer stopped: {duration:.4f}s")
return False # Don't suppress exceptions
# Using with statement
print("\nUsing context manager:")
with Timer():
# Simulate work
total = sum(range(1000000))
print(f"Calculated sum: {total}")👑 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"
# 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 > 18
query2 = User.name == "Alice"
query3 = User.age < 65
print("Query expressions:")
print(f"User.age > 18 → {query1}")
print(f"User.name == 'Alice' → {query2}")
print(f"User.age < 65 → {query3}")
# Pathlib-style path building
class Path:
def __init__(self, path):
self.path = path
def __truediv__(self, other):
return Path(f"{self.path}/{other}")
def __repr__(self):
return f"Path('{self.path}')"
# Building paths with /
home = Path("home")
user_dir = home / "user" / "documents"
print(f"\nPath building:")
print(f"home / 'user' / 'documents' → {user_dir}")
# ✅ Expected output:
# Query expressions:
# User.age > 18 → age > 18
# User.name == 'Alice' → name = 'Alice'
# User.age < 65 → age < 65
#
# Path building:
# home / 'user' / 'documents' → Path('home/user/documents')🧩 20-22. Attribute Access & Delegation
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:
x = 10
y = 20
# Wrap in proxy
data = Proxy(Data())
print("Accessing through proxy:")
print(f"data.x = {data.x}")
data.y = 100
print(f"data.y = {data.y}")
# Money class with delegation
class Money:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f"Money({self.amount})"
def __add__(self, other):
if isinstance(other, Money):
return Money(self.amount + other.amount)
return Money(self.amount + other)
def __mul__(self, x):
# Delegate to internal float
return Money(self.amount.__mul__(x))
print("\nMoney with delegation:")
m1 = Money(100)
m2 = Money(50.25)
print(f"{m1} + {m2} = {m1 + m2}")
print(f"{m1} * 3 = {m1 * 3}")
# ✅ Expected output:
# Accessing through proxy:
# Getting: x
# data.x = 10
# Setting: y = 100
# Getting: y
# data.y = 100
#
# Money with delegation:
# Money(100) + Money(50.25) = Money(150.25)
# Money(100) * 3 = Money(300)🔥 23-24. Complete Mathematical System
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):
if isinstance(other, Vec):
return Vec(self.x - other.x, self.y - other.y)
return Vec(self.x - other, self.y - other)
def __rsub__(self, other):
return Vec(other - self.x, other - self.y)
def __mul__(self, other):
if isinstance(other, Vec):
return Vec(self.x * other.x, self.y * other.y)
return Vec(self.x * other, self.y * other)
__rmul__ = __mul__
def __neg__(self):
return Vec(-self.x, -self.y)
# Comparison
def magnitude(self):
return (self.x**2 + self.y**2) ** 0.5
def __lt__(self, other):
return self.magnitude() < other.magnitude()
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
# Container
def __len__(self):
return 2
def __iter__(self):
yield self.x
yield self.y
def __getitem__(self, idx):
return (self.x, self.y)[idx]
# Boolean & callable
def __bool__(self):
return self.magnitude() != 0
def __call__(self, scale):
return Vec(self.x * scale, self.y * scale)
# Test complete system
v1 = Vec(3, 4)
v2 = Vec(1, 2)
print("Complete Vec system:")
print(f"v1 = {v1}, v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 2 = {v1 * 2}")
print(f"-v1 = {-v1}")
print(f"v1 < v2 = {v1 < v2}")
print(f"bool(v1) = {bool(v1)}")
print(f"v1(10) = {v1(10)}")
print(f"list(v1) = {list(v1)}")
print(f"Hashable: {hash(v1)}")🧵 25-27. Immutable vs Mutable Design
# 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._y)
def __hash__(self):
return hash((self._x, self._y))
# Mutable (modifies itself)
class MutableVec:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"MutableVec({self.x}, {self.y})"
def __iadd__(self, other):
self.x += other.x
self.y += other.y
return self
# Immutable test
print("Immutable (safe for caching, hashable):")
iv1 = ImmutableVec(1, 2)
iv2 = ImmutableVec(3, 4)
iv3 = iv1 + iv2
print(f"{iv1} + {iv2} = {iv3}")
print(f"Can be dict key: {hash(iv1)}")
# Mutable test
print("\nMutable (fast for games, simulations):")
mv = MutableVec(1, 2)
print(f"Before: {mv}")
mv += MutableVec(10, 20)
print(f"After +=: {mv}")
# Immutable: math, DDD, distributed systems, caching
# Mutable: 3D graphics, games, physics simulations🏁 Mini-Challenge: make a class behave like a list
No blanks this time. Build a Playlist class that wraps a list of track titles and feels like a built-in container: len() counts it, [0] indexes it, in searches it, + joins two playlists into a new one, and it prints as Playlist(['Roads', 'Teardrop']).
One thing to watch for at the end: the test loop iterates over a playlist even though you never write __iter__. Python falls back to calling __getitem__ with 0, 1, 2 … until it hits an IndexError. Getting that for free is the reward for implementing the standard protocol instead of inventing your own method names.
🎯 Summary — You Now Understand the Full Python Operator Model
- ✔ 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.
Practice quiz
Which magic method implements the + operator?
- __plus__
- __sum__
- __add__
- __concat__
Answer: __add__. Defining __add__(self, other) makes a + b work on your objects.
Which magic method enables == comparisons?
- __eq__
- __equals__
- __cmp__
- __is__
Answer: __eq__. __eq__ controls the == operator.
Implementing __lt__ on a class primarily enables what?
- Addition
- Indexing
- Iteration
- Sorting and < comparisons
Answer: Sorting and < comparisons. __lt__ defines < and lets Python sort instances with sorted().
When is __rmul__ called?
- Never
- When the left operand (e.g. an int) doesn't know how to multiply your object
- Only inside loops
- When you call the object
Answer: When the left operand (e.g. an int) doesn't know how to multiply your object. __rmul__ handles the reflected case like 3 * v where the left operand can't handle it.
Which method makes an object respond to the len() built-in?
- __len__
- __size__
- __count__
- __length__
Answer: __len__. __len__ defines what len(obj) returns.
Which method makes an object callable like a function, obj(x)?
- __invoke__
- __run__
- __call__
- __exec__
Answer: __call__. __call__ lets an instance be called like obj(x).
Which method controls the [] indexing operator, obj[key]?
- __index__
- __getitem__
- __getattr__
- __get__
Answer: __getitem__. __getitem__ handles obj[key] indexing and slicing.
In the lesson, what does the Path class's __truediv__ let you write?
- Path('a') + 'b'
- Path('a') * 2
- Path('a') - 'b'
- Path('home') / 'user'
Answer: Path('home') / 'user'. __truediv__ overloads /, so Path('home') / 'user' builds a path like Pathlib.
What should an arithmetic operator return for an unsupported operand type?
- None
- NotImplemented
- Raise immediately
- False
Answer: NotImplemented. Returning NotImplemented lets Python try the reflected operator before raising TypeError.
Which method makes a permission flags class support the | operator (READ | WRITE)?
- __bitor__
- __pipe__
- __or__
- __union__
Answer: __or__. __or__ overloads the bitwise | operator.
Continue this course
- Previous: Magic Methods & the Python Data Model
- Next: Mixins, Multiple Inheritance & OOP Patterns — Compose behaviour with mixins and navigate multiple inheritance safely
- Quick reference: Python cheat sheet