Python operator overloading exercises — complex numbers and polynomials that add, multiply and differentiate, with solutions, a quiz and a cheat sheet (and the shelf that turned into None)
The night before an FP2 lab exam I was revising with an Shelf that used += to add books. First +=: fine. Next line: AttributeError: 'NoneType' object has no attribute 'books'. It took me a good while to see it: my __iadd__ added the book… and returned nothing. Python assigned that None to the variable and the shelf vanished. That slip, the __rsub__ written as if subtraction commuted and the __eq__ that keeps objects out of sets are the three most common stumbles with operator overloading.
You’ve already seen the theory with Vector2D and the practice with Rational, Money and the shelf. Now it’s your turn.
Table of Contents
—
How to use these exercises
- Read the statement and the expected output. Your program must give exactly that output with that data.
- Try it without looking at the hints. If you’re stuck for more than 15 minutes, open the hint.
- Before the solutions, do the “What’s wrong here?” section: three snippets with typical bugs to predict.
- Compare with the commented solution: what matters is understanding why it’s built that way.
- Finish with the exam-style quiz and keep the cheat sheet.
Every output is the real output of the solutions.
—
Basic level
Exercise 1: complex number class
Implement a Complex class for numbers of the form a + bi (real part a, imaginary part b):
- Private attributes
realandimag, with read-only properties;imagdefaults to 0. __str__with the format3+4ior1-2i(no spaces) and__repr__with the formatComplex(3, 4).==between complex numbers and with real numbers (Complex(5) == 5isTrue), and a consistent__hash__. Don’t implement<or>: complex numbers have no natural order.+,-and*between complex numbers and with numbers (int or float), both ways round:c + 2and2 + c,c - 3and3 - c,3 * c. The product is (a + bi)(c + di) = (ac − bd) + (ad + bc)i.-c(negation),abs(c)(the modulus: the square root of a² + b²) and aconjugate()method that returns a − bi.
With c1 = Complex(3, 4) and c2 = Complex(1, -2), test every operation, compare two results with Python’s built-in complex numbers, and check that c1 < c2 raises TypeError. Expected output:
c1 = 3+4i c2 = 1-2i repr: Complex(3, 4) c1 + c2 = 4+2i c1 - c2 = 2+6i c1 * c2 = 11-2i c1 + 2 = 5+4i 2 + c1 = 5+4i 3 * c2 = 3-6i c1 - 3 = 0+4i 3 - c1 = 0-4i -c1 = -3-4i abs(c1) = 5.0 conjugate: 3-4i c1 * conjugate = 25+0i c1 == Complex(3, 4): True Complex(5) == 5: True Check with complex: (11-2j) -4j TypeError: '<' not supported between instances of 'Complex' and 'Complex'
First write a @staticmethod that turns a number into Complex(x) and returns None for anything else; that way every operator starts the same. Addition and multiplication commute, so __radd__ and __rmul__ can call the direct ones. Subtraction doesn’t: 3 - c1 is Complex(3) - c1 = 0 − 4i, not c1 - 3 = 0 + 4i. To make < raise TypeError you don’t have to do anything: if you don’t define __lt__, Python raises it by itself.
—
Intermediate level and final challenge
Exercise 2: polynomial class
Implement a one-variable Polynomial class. It stores the list of coefficients from lowest to highest degree: [1, -3, 2] is 1 − 3x + 2x², written 2x² - 3x + 1.
- The constructor drops trailing zeros (
[1, -3, 2, 0]is the same polynomial); the empty list is the zero polynomial. - A
coefficientsproperty that returns a copy of the list,degree()andevaluate(x). - A readable
__str__: from highest to lowest degree, no coefficient 1 in front of x (x + 2, not1x + 2), superscript exponents (x², x³) and the sign separated by spaces.__repr__:Polynomial([1, -3, 2]). +and-between polynomials and with numbers, both ways round (p + 5,3 - p). To add, pad the shorter one with zeros.*between polynomials (each term of the first times all of the second) and with numbers (2 * p).sum()of a list of polynomials.
With p1 = Polynomial([1, -3, 2]) and p2 = Polynomial([2, 1]), the expected output is:
p1 = 2x² - 3x + 1 p2 = x + 2 repr: Polynomial([1, -3, 2]) degree: 2 p1(0), p1(1), p1(3): 1 0 10 p1 + p2 = 2x² - 2x + 3 p1 - p2 = 2x² - 4x - 1 p1 * p2 = 2x³ + x² - 5x + 2 p1 + 5 = 2x² - 3x + 6 2 * p1 = 4x² - 6x + 2 p1 - 3 = 2x² - 3x - 2 3 - p1 = -2x² + 3x + 2 p1 - p1 = 0 degree: 0 sum: x³ + 2x² - 2x + 3
To evaluate, Horner’s scheme is short and fast: go through the coefficients from highest to lowest degree doing r = r * x + a. To subtract, don’t write another loop: p - q is p + (-q), so __neg__ is all you need. In the product, the degree-i term of the first times the degree-j term of the second goes to degree i + j of the result. And check p1 - p1: it must print 0 with degree 0.
Exercise 3 (challenge): polynomials you can compare, call and differentiate
Without touching Polynomial, create a subclass PolynomialPlus(Polynomial) (inheritance, as in the previous topic) that adds:
==between polynomials and with numbers, ignoring trailing zeros, and a consistent__hash__.__call__, to use the polynomial like a function:p(3)is the same asp.evaluate(3).derivative(): the derivative of a·xⁿ is n·a·xⁿ⁻¹.**with a non-negative integer exponent (q ** 2,q ** 0), reusing multiplication.
With p = PolynomialPlus([1, -3, 2]) and q = PolynomialPlus([2, 1]), test the integer roots of p between −5 and 5, a set with p twice and q, and check that (p · q)(2) = p(2) · q(2). Expected output:
p = 2x² - 3x + 1 p(0), p(1), p(3): 1 0 10 p' = 4x - 3 p'' = 4 q' = 1 q ** 2 = x² + 4x + 4 q ** 0 = 1 p == [1, -3, 2, 0]: True p == 1: False -p = -2x² + 3x - 1 Roots of p: [1] p(0.5) = 0.0 Set: 2 (p * q)(2) = 12 = p(2) * q(2) = 12
When you define __eq__ in the subclass, the inherited __hash__ disappears: write it with hash(tuple(...)). The derivative fits in one line with enumerate: multiply each coefficient by its degree and drop the first one (degree 0). And watch an inheritance detail: the parent’s operators return Polynomial, not PolynomialPlus, so the result of ** has to be “converted” at the end.
—
What’s wrong here?
Three snippets with one bug each. Before reading the explanation, predict what happens.
Snippet 1: the set of borrowed books
class Book:
def __init__(self, title, isbn):
self.title = title
self.isbn = isbn
def __eq__(self, other):
return isinstance(other, Book) and self.isbn == other.isbn
prestados = {Book('Calculus', '978-6075265490')}
What happens when you run it?
Traceback (most recent call last):
File "hash.py", line 10, in <module>
prestados = {Book('Calculus', '978-6075265490')}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'Book'
By defining __eq__, Python removes the __hash__ that Book inherited from object, and an object without a hash can’t go into a set. The fix is a __hash__ that uses the same data as __eq__ (the ISBN), so two equal books have the same hash and the set counts them as one:
class Book:
def __init__(self, title, isbn):
self.title = title
self.isbn = isbn
def __eq__(self, other):
return isinstance(other, Book) and self.isbn == other.isbn
def __hash__(self): # same data as __eq__
return hash(self.isbn)
prestados = {Book('Calculus', '978-6075265490'), Book('Calculus', '978-6075265490')}
print(len(prestados))
Output:
1
Snippet 2: the addition that doesn’t warn you
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector2D):
return Vector2D(self.x + other.x, self.y + other.y)
# for anything else… returns nothing
r = Vector2D(3, 4) + 'hello'
print(r)
What does it print?
None
None, with no error at all. When other isn’t a Vector2D, the method reaches the end without return and returns None, and Python accepts that as the “sum”. With return NotImplemented, Python asks the string (which can’t add vectors either) and raises a clear error:
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if isinstance(other, Vector2D):
return Vector2D(self.x + other.x, self.y + other.y)
return NotImplemented
r = Vector2D(3, 4) + 'hello'
Output:
Traceback (most recent call last):
File "none.py", line 12, in <module>
r = Vector2D(3, 4) + 'hello'
~~~~~~~~~~~~~~~^~~~~~~~~
TypeError: unsupported operand type(s) for +: 'Vector2D' and 'str'
An error that warns you is better than a None that sneaks three functions further down.
Snippet 3: the shelf that disappears
class Shelf:
def __init__(self):
self.books = []
def __iadd__(self, title):
self.books.append(title) # adds… but returns nothing
e = Shelf()
e += 'Calculus'
print(e)
print(len(e.books))
What happens?
None
Traceback (most recent call last):
File "iadd.py", line 12, in <module>
print(len(e.books))
^^^^^^^
AttributeError: 'NoneType' object has no attribute 'books'
e += 'Calculus' is e = e.__iadd__('Calculus'): Python assigns to e whatever the method returns. Since it returns nothing, e becomes None. The book was added, but to an object nobody points to any more. With return self:
class Shelf:
def __init__(self):
self.books = []
def __iadd__(self, title):
self.books.append(title)
return self
e = Shelf()
e += 'Calculus'
e += 'Understanding Networks'
print(e.books)
Output:
['Calculus', 'Understanding Networks']
—
Commented solutions
Solution to exercise 1
class Complex:
def __init__(self, real, imag=0):
self.__real = real
self.__imag = imag
@property
def real(self):
return self.__real
@property
def imag(self):
return self.__imag
@staticmethod
def _as_complex(x): # a real number is a complex with imag 0
if isinstance(x, Complex):
return x
if isinstance(x, (int, float)):
return Complex(x)
return None
def __str__(self):
sign = '+' if self.__imag >= 0 else '-'
return f'{self.__real}{sign}{abs(self.__imag)}i'
def __repr__(self):
return f'Complex({self.__real}, {self.__imag})'
def __eq__(self, other): # equality yes; ordering (<, >) makes no sense
o = Complex._as_complex(other)
if o is None:
return NotImplemented
return self.__real == o.real and self.__imag == o.imag
def __hash__(self):
return hash((self.__real, self.__imag))
def __add__(self, other):
o = Complex._as_complex(other)
if o is None:
return NotImplemented
return Complex(self.__real + o.real, self.__imag + o.imag)
def __sub__(self, other):
o = Complex._as_complex(other)
if o is None:
return NotImplemented
return Complex(self.__real - o.real, self.__imag - o.imag)
def __mul__(self, other): # (a+bi)(c+di) = (ac-bd) + (ad+bc)i
o = Complex._as_complex(other)
if o is None:
return NotImplemented
a, b, c, d = self.__real, self.__imag, o.real, o.imag
return Complex(a * c - b * d, a * d + b * c)
def __radd__(self, other):
return self + other
def __rmul__(self, other):
return self * other
def __rsub__(self, other): # other - self
o = Complex._as_complex(other)
return NotImplemented if o is None else o - self
def __neg__(self):
return Complex(-self.__real, -self.__imag)
def __abs__(self): # modulus: square root of a² + b²
return (self.__real ** 2 + self.__imag ** 2) ** 0.5
def conjugate(self):
return Complex(self.__real, -self.__imag)
c1, c2 = Complex(3, 4), Complex(1, -2)
print('c1 =', c1, ' c2 =', c2, ' repr:', repr(c1))
print('c1 + c2 =', c1 + c2, ' c1 - c2 =', c1 - c2, ' c1 * c2 =', c1 * c2)
print('c1 + 2 =', c1 + 2, ' 2 + c1 =', 2 + c1, ' 3 * c2 =', 3 * c2)
print('c1 - 3 =', c1 - 3, ' 3 - c1 =', 3 - c1)
print('-c1 =', -c1, ' abs(c1) =', abs(c1), ' conjugate:', c1.conjugate())
print('c1 * conjugate =', c1 * c1.conjugate())
print('c1 == Complex(3, 4):', c1 == Complex(3, 4), ' Complex(5) == 5:', Complex(5) == 5)
print('Check with complex:', complex(3, 4) * complex(1, -2), 3 - complex(3, 4))
try:
print(c1 < c2)
except TypeError as err:
print('TypeError:', err)
The decisions that matter:
_as_complexholds the type rule. A real number is a complex number with imaginary part 0; anything else givesNoneand the operator returnsNotImplemented.- The product, with the formula from class: (3 + 4i)(1 − 2i) = (3·1 − 4·(−2)) + (3·(−2) + 4·1)i = 11 − 2i. The “Check with complex” line gives
(11-2j): the same calculation done by Python (which usesjinstead ofi). __rsub__in its order:3 - c1=Complex(3) - c1= (3 − 3) + (0 − 4)i = 0 − 4i. Python also gives-4j.- The conjugate multiplies into a real number: (3 + 4i)(3 − 4i) = 9 + 16 = 25, and the imaginary part is 0 (
25+0i). And |3 + 4i| = 5.0. - No
__lt__: Python raises theTypeErrorby itself, with a message naming both types. __str__withabs(imag)so as not to write3+-4i: the sign is added separately.
Solution to exercise 2
class Polynomial:
SUP = str.maketrans('0123456789', '⁰¹²³⁴⁵⁶⁷⁸⁹')
def __init__(self, coefficients):
c = list(coefficients) or [0] # [1, -3, 2] = 1 - 3x + 2x²
while len(c) > 1 and c[-1] == 0: # drop trailing zeros
c.pop()
self._c = c
@property
def coefficients(self):
return list(self._c) # copy: nobody touches the inner list
def degree(self):
return len(self._c) - 1
def evaluate(self, x): # Horner: from highest to lowest degree
r = 0
for a in reversed(self._c):
r = r * x + a
return r
@staticmethod
def _as_poly(x):
if isinstance(x, Polynomial):
return x
if isinstance(x, (int, float)):
return Polynomial([x])
return None
def __add__(self, other):
o = Polynomial._as_poly(other)
if o is None:
return NotImplemented
n = max(len(self._c), len(o._c))
a = self._c + [0] * (n - len(self._c)) # pad the shorter one with zeros
b = o._c + [0] * (n - len(o._c))
return Polynomial([x + y for x, y in zip(a, b)])
def __radd__(self, other):
return self + other
def __neg__(self):
return Polynomial([-a for a in self._c])
def __sub__(self, other):
o = Polynomial._as_poly(other)
return NotImplemented if o is None else self + (-o)
def __rsub__(self, other): # other - self
o = Polynomial._as_poly(other)
return NotImplemented if o is None else o + (-self)
def __mul__(self, other):
o = Polynomial._as_poly(other)
if o is None:
return NotImplemented
r = [0] * (len(self._c) + len(o._c) - 1)
for i, a in enumerate(self._c): # each term times all of the other's
for j, b in enumerate(o._c):
r[i + j] += a * b
return Polynomial(r)
def __rmul__(self, other):
return self * other
def __str__(self):
parts = []
for g in range(len(self._c) - 1, -1, -1): # from the highest degree to the lowest
a = self._c[g]
if a == 0 and len(self._c) > 1:
continue
sign = '-' if a < 0 else '+'
n = abs(a)
coef = '' if n == 1 and g > 0 else str(n)
var = '' if g == 0 else 'x' if g == 1 else 'x' + str(g).translate(Polynomial.SUP)
parts.append((sign, coef + var))
s = ('-' if parts[0][0] == '-' else '') + parts[0][1]
return s + ''.join(f' {sg} {t}' for sg, t in parts[1:])
def __repr__(self):
return f'Polynomial({self._c})'
if __name__ == '__main__':
p1, p2 = Polynomial([1, -3, 2]), Polynomial([2, 1])
print('p1 =', p1, ' p2 =', p2, ' repr:', repr(p1), ' degree:', p1.degree())
print('p1(0), p1(1), p1(3):', p1.evaluate(0), p1.evaluate(1), p1.evaluate(3))
print('p1 + p2 =', p1 + p2, ' p1 - p2 =', p1 - p2)
print('p1 * p2 =', p1 * p2)
print('p1 + 5 =', p1 + 5, ' 2 * p1 =', 2 * p1)
print('p1 - 3 =', p1 - 3, ' 3 - p1 =', 3 - p1)
print('p1 - p1 =', p1 - p1, ' degree:', (p1 - p1).degree())
print('sum:', sum([p1, p2, Polynomial([0, 0, 0, 1])]))
Multiplication is the part with the most work:
def __mul__(self, other):
o = Polynomial._as_poly(other)
if o is None:
return NotImplemented
r = [0] * (len(self._c) + len(o._c) - 1)
for i, a in enumerate(self._c): # each term times all of the other's
for j, b in enumerate(o._c):
r[i + j] += a * b
return Polynomial(r)
Each coefficient a of the first (degree i) times each b of the second (degree j) accumulates in r[i + j]. The diagram does it with p1 · p2:
Let’s check the evaluation of p1 = 2x² − 3x + 1 at x = 3 with Horner (coefficients from highest to lowest: 2, −3, 1):
| Step | Coefficient | r = r · 3 + a |
|---|---|---|
| 1 | 2 | 0 · 3 + 2 = 2 |
| 2 | −3 | 2 · 3 − 3 = 3 |
| 3 | 1 | 3 · 3 + 1 = 10 |
And by hand: 2 · 9 − 9 + 1 = 10.
Why it’s built this way:
- Trailing zeros are removed in the constructor, once: that’s how
p1 - p1ends up as[0], its degree is 0 and__str__prints0. coefficientsreturns a copy: if it returned the inner list,p.coefficients.append(5)would change the polynomial without going through the constructor.__sub__and__rsub__lean on__add__and__neg__:3 - p1isPolynomial([3]) + (-p1)= [3, 0, 0] + [−1, 3, −2] = [2, 3, −2], that is −2x² + 3x + 2. A single addition loop for everything.__radd__calls__add__and that’s enough forsum(): 0 + p1 becomesPolynomial([0]) + p1.- The
if __name__ == '__main__':makes the tests run only when this file is executed, not when it’s imported from exercise 3. - Superscripts come from
str.translatewith a'0123456789' → '⁰¹²³⁴⁵⁶⁷⁸⁹'table.
Solution to exercise 3
from polynomial import Polynomial
class PolynomialPlus(Polynomial): # inheritance: reuses everything from exercise 2
def __eq__(self, other):
o = Polynomial._as_poly(other)
if o is None:
return NotImplemented
return self.coefficients == o.coefficients # already without trailing zeros
def __hash__(self):
return hash(tuple(self._c))
def __call__(self, x): # p(3) like a function
return self.evaluate(x)
def derivative(self): # a·xⁿ -> n·a·xⁿ⁻¹
return PolynomialPlus([n * a for n, a in enumerate(self._c)][1:])
def __pow__(self, n): # p ** n with integer n >= 0
if not isinstance(n, int) or n < 0:
return NotImplemented
r = PolynomialPlus([1])
for _ in range(n):
r = r * self
return PolynomialPlus(r.coefficients) # __mul__ returns Polynomial
p = PolynomialPlus([1, -3, 2])
q = PolynomialPlus([2, 1])
print('p =', p, ' p(0), p(1), p(3):', p(0), p(1), p(3))
print("p' =", p.derivative(), " p'' =", p.derivative().derivative(), " q' =", q.derivative())
print('q ** 2 =', q ** 2, ' q ** 0 =', q ** 0)
print('p == [1, -3, 2, 0]:', p == PolynomialPlus([1, -3, 2, 0]), ' p == 1:', p == 1, ' -p =', -p)
print('Roots of p:', [x for x in range(-5, 6) if p(x) == 0], ' p(0.5) =', p(0.5))
print('Set:', len({p, PolynomialPlus([1, -3, 2]), q}))
print('(p * q)(2) =', (p * q).evaluate(2), '= p(2) * q(2) =', p(2) * q(2))
With from polynomial import Polynomial we reuse the whole class from exercise 2 (both files go in the same folder) and only write what’s new. The maths:
- p(0) = 1, p(1) = 2 − 3 + 1 = 0 and p(3) = 10 (the trace above). Since p(1) = 0, 1 is a root; the other root is 0.5 (p(0.5) = 0.0), which the search misses because it only tries integers.
- p′ = [1·(−3), 2·2] = [−3, 4], that is 4x − 3, and p″ = 4.
- q² = (x + 2)(x + 2) = x² + 4x + 4, and q⁰ = 1: the
__pow__loop doesn’t run at all. - The set has 2 elements: p and
PolynomialPlus([1, -3, 2])are equal and have the same hash. - (p · q)(2) = 12 and p(2) · q(2) = 3 · 4 = 12: the multiplication is right.
Why it’s built this way:
- Inheritance instead of copying:
PolynomialPlusgets addition, subtraction, product,strandevaluatewithout writing them. It only adds what’s new. __hash__is required when redefining__eq__in the subclass, for the same reason as in snippet 1.__call__makes the object “callable” like a function. It’s not an arithmetic operator, but it’s overloading: brackets are an operator too.- The type detail:
-pandr * selfreturnPolynomial(that’s what the parent writes). That’s why__pow__creates aPolynomialPlusat the end. The real fix would be for the parent to usetype(self)(…)instead ofPolynomial(…): the same idea astype(self).__name__in the inheritance topic.
—
Exam-style quiz
Now test yourself as in the lab. FP2 quizzes look like this: about 10 questions in 10-15 minutes, single choice, multiple choice (wrong answers subtract), true or false, fill in the blanks, matching and “what does this code print?”. Here are 13 original questions about operator overloading. Answer without running anything, press Check and read the explanation of each one. These questions are also the question bank of the FP2 theory mock exam, which will mix them with those of the other topics.
Match each operator with the magic method that implements it:
a + b | |
a - b | |
a * b | |
a / b | |
a // b | |
a == b |
Definitions:
- __sub__
- __eq__
- __truediv__
- __div__
- __mul__
- __add__
- __floordiv__
Solution
a + b→ __add__a - b→ __sub__a * b→ __mul__a / b→ __truediv__a // b→ __floordiv__a == b→ __eq__
/ is «true» division (__truediv__) and // is floor division (__floordiv__). __div__ was Python 2 and isn’t used in Python 3.
Match each expression with the method Python calls:
a < b | |
a <= b | |
a != b | |
len(a) | |
a[i] | |
-a |
Definitions:
- __getitem__
- __cmp__
- __le__
- __lt__
- __minus__
- __ne__
- __len__
- __neg__
Solution
a < b→ __lt__a <= b→ __le__a != b→ __ne__len(a)→ __len__a[i]→ __getitem__-a→ __neg__
lt = less than, le = less or equal, ne = not equal. The minus in front of an object (the unary operator) is __neg__; the binary a - b is __sub__. __cmp__ disappeared in Python 3.
You define __eq__ and __lt__ in your class. Which decorator from the functools module adds <=, > and >= for you?
Solution
Answer: total_ordering
@total_ordering needs __eq__ and one of __lt__, __le__, __gt__ or __ge__, and works out the rest. It goes above the class.
What does this program print?
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k):
return Vector(self.x * k, self.y * k)
def __str__(self):
return f"<{self.x}, {self.y}>"
v = Vector(1, 2) + Vector(3, 4) * 2
print(v)Solution
Answer: <7, 10>
Overloaded operators keep the usual precedence: first Vector(3, 4) * 2 = <6, 8>, then <1, 2> + <6, 8> = <7, 10>.
The Vector class above has __mul__ but no __rmul__. What happens with 2 * Vector(1, 2)?
TypeError:intcan’t multiply itself by aVectorand there’s no__rmul__- It gives
<2, 4>, just likeVector(1, 2) * 2 - It gives
<1, 2, 1, 2> - The result is
NotImplemented
Solution
Answer: TypeError: int can’t multiply itself by a Vector and there’s no __rmul__
To make it work both ways you add the reflected method: def __rmul__(self, k): return self * k.
money + 3 works, but 3 + money gives TypeError. Which method do you need to add to money‘s class?
Solution
Answer: __radd__
With 3 + money, int.__add__ returns NotImplemented and Python tries money.__radd__(3). Every binary operator has its reflected version: __rsub__, __rmul__, __rtruediv__…
What does this program print?
class Coin:
def __init__(self, c):
self.c = c
a = Coin(50)
b = Coin(50)
print(a == b, a is b)Solution
Answer: False False
Without __eq__, == compares identity (like is): they’re two different objects even though they’re worth the same.
Now with __eq__. What does it print?
class Coin:
def __init__(self, c):
self.c = c
def __eq__(self, other):
return self.c == other.c
print(Coin(50) == Coin(50), Coin(50) != Coin(20))Solution
Answer: True True
== uses your __eq__ (50 == 50). And although you didn’t write __ne__, in Python 3 != returns the opposite of __eq__: since 50 == 20 is False, != gives True.
True or false?
__add__should return a new object and leaveselfunchanged.a += buses__iadd__if it exists and, if not,__add__.- If an operator method returns
NotImplemented, Python tries the other operand’s reflected method. - Defining
__eq__makes<work too. - With
__lt__defined,sorted()can already sort a list of those objects.
Solution
- T —
__add__should return a new object and leaveselfunchanged. - T —
a += buses__iadd__if it exists and, if not,__add__. - T — If an operator method returns
NotImplemented, Python tries the other operand’s reflected method. - F — Defining
__eq__makes<work too. - T — With
__lt__defined,sorted()can already sort a list of those objects.
sorted() only needs <. On the other hand, < isn’t deduced from ==: without __lt__ (or @total_ordering), comparing two objects with < gives TypeError.
What does this program print?
class Playlist:
def __init__(self, *songs):
self.songs = list(songs)
def __len__(self):
return len(self.songs)
def __getitem__(self, i):
return self.songs[i]
p = Playlist("a", "b", "c")
print(len(p), p[-1], "b" in p)Solution
Answer: 3 c True
len() uses __len__ (3). p[-1] calls __getitem__(-1), which delegates to the list ("c"). And in works even without __contains__: Python walks the object with __getitem__ (0, 1, 2…) until it finds it.
The Student class only defines def __lt__(self, other): return self.mark < other.mark. What does sorted(students) do?
- It sorts them from lowest to highest mark
- It sorts them by name
- It gives
TypeErrorbecause__gt__is missing - It leaves them as they are
Solution
Answer: It sorts them from lowest to highest mark
To sort from highest to lowest: sorted(students, reverse=True).
Which of these are good practices when overloading operators?
- Returning
NotImplementedif the other operand is of a type you can’t handle - Making the operator mean what anyone would expect (
+adds or joins) - Defining
__repr__so you can see objects while debugging - Modifying
selfinside__add__and returning it - Comparing with
isinside__eq__
Solution
Answer: Returning NotImplemented if the other operand is of a type you can’t handle · Making the operator mean what anyone would expect (+ adds or joins) · Defining __repr__ so you can see objects while debugging
Overloading is there so your class can be used like Python’s built-in types. If + does something surprising, the code becomes hard to read.
Complete how Python translates operators:
Python turns a + b into ____. If that returns NotImplemented, it tries ____. And a += b tries ____ first.
Words:
- b.__radd__(a)
- b.__add__(a)
- a.__iadd__(b)
- a.__add__(b)
- a.__radd__(b)
Solution
Python turns a + b into a.__add__(b). If that returns NotImplemented, it tries b.__radd__(a). And a += b tries a.__iadd__(b) first.
First the left operand with the normal method; if it can’t, the right one with the reflected method (r). The augmented ones (+=, -=…) have their i (in-place) version.
—
Watch it step by step in Python Tutor
Let’s watch snippet 3 live: the __iadd__ without return. On pythontutor.com, choose Python 3 and paste:
class Shelf:
def __init__(self):
self.books = []
def __iadd__(self, title):
self.books.append(title)
class ShelfFixed(Shelf):
def __iadd__(self, title):
self.books.append(title)
return self
a = Shelf()
a += 'Calculus'
b = ShelfFixed()
b += 'Calculus'
print(a, b.books)
Output:
None ['Calculus']
What to watch step by step:
- On
a += 'Calculus'it enters__iadd__withselfpointing to the shelfa. Watch the object’sbookslist get'Calculus'. - On the way out, the frame ends with Return value
None… and the global variableabecomesNone. The shelf with its book is still drawn, but nobody points to it any more. - The same happens with
b, but the frame returns the object itself andbkeeps pointing to the shelf. - In the
print,aisNoneandb.booksis['Calculus'].
—
Cheat sheet: Python operator overloading
The essentials in 30 seconds: an operator is a method call (a + b is a.__add__(b)); if the type doesn’t fit, return NotImplemented (Python tries b.__radd__(a) and, if that fails too, raises TypeError); __eq__ removes the inherited __hash__; @total_ordering fills in the ordering from __eq__ and __lt__; in reflected methods self is on the right, and subtraction and division don’t commute; __iadd__ modifies and returns self.
| I want… | Method | Example and result |
|---|---|---|
Readable print | __str__ | print(c1) → 3+4i |
| See the object in lists | __repr__ | [c1] → [Complex(3, 4)] |
== and != | __eq__ | Complex(5) == 5 → True |
Use it in a set | __hash__ | hash((self.real, self.imag)) |
<, <=, >, >= | __lt__ + @total_ordering | sorted(rationals) |
a + b | __add__ | c1 + c2 → 4+2i |
2 + a, sum() | __radd__ | 2 + c1 → 5+4i |
3 - a | __rsub__ | 3 - c1 → 0-4i (not 0+4i) |
2 * a | __rmul__ | 3 * c2 → 3-6i |
-a, abs(a) | __neg__, __abs__ | abs(c1) → 5.0 |
a += b modifying | __iadd__ | return self at the end |
len(a), a[i], x in a | __len__, __getitem__, __contains__ | The practice’s shelf |
p(3) | __call__ | Polynomial as a function |
p ** 2 | __pow__ | q ** 2 → x² + 4x + 4 |
Exercise templates:
| Pattern | Skeleton |
|---|---|
| Type rule in one place | o = Cls._as_x(other) and if o is None: return NotImplemented |
| Commutative reflected method | def __radd__(self, other): return self + other |
| Non-commutative reflected method | def __rsub__(self, other): return Cls(other) - self |
| Subtract by adding | def __sub__(self, other): return self + (-other) |
| Consistent hash | def __hash__(self): return hash(tuple(self._c)) |
| Symptom | Likely cause |
|---|---|
v1 == v2 is False with the same data | __eq__ is missing (it compares identity) |
TypeError: unhashable type | There’s __eq__ without __hash__ |
AttributeError when comparing with a number | __eq__ without isinstance |
2 * v or sum(list) raise TypeError | __rmul__ or __radd__ are missing |
10 - x comes out with the sign flipped | __rsub__ written as self - other |
An operation returns None with no error | A path of the method without return (or without return NotImplemented) |
'NoneType' object has no attribute … after += | __iadd__ without return self |
print([obj]) shows <… object at 0x…> | __repr__ is missing |
Before you hand it in, ask yourself:
- Does each operator return
NotImplemented(notNone,Falseor araise) when the type doesn’t fit? - Have I tested the reflected methods with a non-symmetric case (
3 - c,1 / r)? - If I defined
__eq__, do I need__hash__? Does it use the same data? - Do arithmetic operators return new objects without touching the originals, and does
__iadd__returnself?
The next topic is Python iterators and generators: __iter__ and __next__, the magic methods that make for work.
Did this article help you? Here is how you can support SergioLearns →




The hardware that holds up the operating system — theory with my IC2 assignment on the printer, an interrupt that stops everything and a cache that hits (and the infinite loop that didn’t freeze my laptop)