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:

Real frameworks rely on this heavily:

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:

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":

# 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

This puts you at a framework engineer level (the people who build NumPy/Pandas/PyTorch, not just use them).

📋 Quick Reference — Operator Overloading

MethodOperator
__add__(self, other)a + b
__eq__(self, other)a == b
__lt__(self, other)a < b (enables sorting)
__mul__(self, other)a * b
@functools.total_orderingAuto-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