Python operator overloading — theory with Vector2D: __eq__, __add__, __str__, reflected operators and @total_ordering (and the two equal vectors Python said weren’t equal)

While preparing an FP2 lab exam I wrote a Vector2D class, created two vectors with the same coordinates and, just to check everything was fine, compared them. Python told me they weren’t equal. Then I tried adding them:

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


v1 = Vector2D(3, 4)
v2 = Vector2D(3, 4)
print(v1 == v2)        # same coordinates…
print(v1 is v2)
print(v1 + v2)

Output:

False
False
Traceback (most recent call last):
  File "hook.py", line 11, in <module>
    print(v1 + v2)
          ~~~^~~~
TypeError: unsupported operand type(s) for +: 'Vector2D' and 'Vector2D'

How can (3, 4) not be equal to (3, 4)? For Python it isn’t: with no instructions, == checks whether both names point to the same object (like is), and + has no idea what adding two objects of a class you just made up means. You have to teach it, and that’s operator overloading: writing in your class the magic methods (__eq__, __add__, __lt__…) that Python calls when you use ==, + or < with your objects.

It’s what was promised in the classes theory and the exceptions practice: __add__ and __eq__ would come “in the operator overloading topic”. And it comes right after Python inheritance because, deep down, overloading an operator is overriding a method your class inherits from object.

In this article we build Vector2D step by step and cover everything FP2 asks for: __str__ versus __repr__, __eq__ and the __hash__ that disappears, @total_ordering, arithmetic operators, NotImplemented, reflected operators (__radd__, __rmul__, __rsub__), unary operators, += with and without __iadd__, and why function overloading doesn’t exist in Python.

—

What is operator overloading?

In Python, operators come overloaded out of the box: + adds numbers, but it also joins strings and lists. The same symbol does different things depending on the type. The reason is that an operator is a method call:

print(3 + 5)                     # integer addition
print('Hello' + ' world')        # string concatenation
print([1, 2] + [3, 4])           # list concatenation
print((3).__add__(5))            # what Python does inside with 3 + 5
print('Hello'.__add__(' world'))
print('__add__' in dir(int), '__add__' in dir(str), '__add__' in dir(dict))

Output:

8
Hello world
[1, 2, 3, 4]
8
Hello world
True True False

3 + 5 is, inside, (3).__add__(5): the __add__ method of the integer 3. Strings have their own __add__, which concatenates. Dictionaries have no __add__, which is why {} + {} raises TypeError. Overloading an operator in your class means writing that method: if Vector2D has __add__, v + w works.

You writePython callsMagic method
a + ba.__add__(b)addition
a - ba.__sub__(b)subtraction
a * ba.__mul__(b)multiplication
a / ba.__truediv__(b)division
a == ba.__eq__(b)equality
a < ba.__lt__(b)less than
-aa.__neg__()negation
abs(a)a.__abs__()absolute value
len(a)a.__len__()length
x in aa.__contains__(x)membership
print(a)a.__str__()text for people

The FP2 notes compare this with Java, and there’s a difference that shows up in the test: Java doesn’t allow operator overloading. To compare objects in Java you implement equals or the Comparable interface; in Python, __eq__ and __lt__.

—

The Vector2D class: starting point

A vector in the plane has two coordinates and is perfect for this topic: it adds, subtracts, multiplies by a number, has a length (modulus) and can be compared. The first thing worth giving it is a way to print itself, because otherwise every test prints <__main__.Vector2D object at 0x…>. There are two methods for that, and mixing them up is one of the classic mistakes:

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

    def __str__(self):                  # for people: print(), str(), f-strings
        return f'({self.x}, {self.y})'

    def __repr__(self):                 # for programmers: lists, console, debugging
        return f'Vector2D({self.x!r}, {self.y!r})'


v = Vector2D(3, 4)
print(v)
print(str(v), repr(v))
print([v, Vector2D(1, 2)])              # a list shows each element's repr
print(f'v = {v}  ->  {v!r}')

Output:

(3, 4)
(3, 4) Vector2D(3, 4)
[Vector2D(3, 4), Vector2D(1, 2)]
v = (3, 4)  ->  Vector2D(3, 4)
  • __str__ is the text for people: print, str() and normal f-strings use it. Here, (3, 4).
  • __repr__ is the text for programmers: the console, repr(), !r in f-strings and, above all, lists, tuples and dictionaries use it when printed. By convention it looks like the code that creates the object: Vector2D(3, 4).

Look at the third line: when printing a list, Python uses each element’s repr, not its str. So if you only write one, make it __repr__: when __str__ is missing, print uses __repr__; not the other way round.

Writing only __str__ and being surprised that print([v, w]) still shows <__main__.Vector2D object at 0x…>. Collections use __repr__.

—

Comparison operators

__eq__ and NotImplemented

For == to compare coordinates, Vector2D needs __eq__. It receives self (the left one) and the other object (the right one) and returns True or False:

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

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented           # "I don't know how to compare with that"
        return self.x == other.x and self.y == other.y


v = Vector2D(3, 4)
print(v == Vector2D(3, 4), v is Vector2D(3, 4))
print(v != Vector2D(1, 2))                  # __ne__ comes for free from __eq__
print(v == (3, 4), v == 'hello')            # different types: False, no error
print(Vector2D(3, 4) in [Vector2D(1, 2), Vector2D(3, 4)])
print(Vector2D.__hash__)                    # defining __eq__ removes the hash
try:
    print({v})
except TypeError as err:
    print('TypeError:', err)

Output:

True False
True
False False
True
None
TypeError: unhashable type: 'Vector2D'

The key is the first line of __eq__: if the other one isn’t a Vector2D, we return NotImplemented. It’s not False and it’s not an error: it’s how you tell Python “I don’t know how to compare myself with that”. Python then asks the other object and, if it doesn’t know either, decides they’re not equal. That’s why v == (3, 4) and v == 'hello' give False without blowing up.

And if we don’t check the type?

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

    def __eq__(self, other):                 # without checking the type
        return self.x == other.x and self.y == other.y


v = Vector2D(3, 4)
print(v == Vector2D(3, 4))
print(v == 5)

Output:

True
Traceback (most recent call last):
  File "eq_wrong.py", line 12, in <module>
    print(v == 5)
          ^^^^^^
  File "eq_wrong.py", line 7, in __eq__
    return self.x == other.x and self.y == other.y
                     ^^^^^^^
AttributeError: 'int' object has no attribute 'x'

With another vector it works, but v == 5 tries to read 5.x and crashes with AttributeError. Comparing with == should never raise an error: that’s why the isinstance always comes first.

Free with __eq__: Python creates != as its negation (object does it, no __ne__ needed), and in on lists, list.count, list.index and list.remove all work, since they compare with ==. Look at the True on the fourth line: in found the Vector2D(3, 4) in the list.

The __hash__ that disappears

The output has a surprise at the end: Vector2D.__hash__ is None and putting the vector in a set raises TypeError: unhashable type. When you define __eq__, Python removes the __hash__ inherited from object, because that one is identity-based and would no longer match your equality. If you want to use your objects in a set or as dictionary keys, write a __hash__ consistent with __eq__: two equal objects must have the same hash. The easy way is hashing a tuple with the same data __eq__ compares:

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

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):                      # equal -> same hash
        return hash((self.x, self.y))


points = {Vector2D(3, 4), Vector2D(3, 4), Vector2D(1, 2)}
print(len(points), Vector2D(3, 4) in points)

Output:

2 True

The set keeps 2 elements: the two Vector2D(3, 4) are equal and have the same hash, so they count as one.

Computing the hash from data that can change (a mutable object inside a set whose x you later change). The set stored it in the “drawer” of the old hash and can’t find it any more. Only use __hash__ on objects you won’t modify while they’re inside.

—

@functools.total_ordering: the shortcut that confuses most

Comparing with <, <=, > and >= would take four methods (__lt__, __le__, __gt__, __ge__). The @total_ordering decorator from the functools module lets you write __eq__ and one of the four, and it generates the rest. We order vectors by their length (the modulus):

from functools import total_ordering
import math


@total_ordering                              # fills in <=, > and >= from == and <
class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def modulus(self):
        return math.hypot(self.x, self.y)    # square root of x² + y²

    def __eq__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __lt__(self, other):                 # "less" = shorter
        if not isinstance(other, Vector2D):
            return NotImplemented
        return self.modulus() < other.modulus()


a, b, c = Vector2D(3, 4), Vector2D(1, 2), Vector2D(0, 6)
print(a < c, a > b, a <= Vector2D(4, 3), c >= a)
print(sorted([a, b, c]))
print(max([a, b, c]), min([a, b, c]))
print([m for m in ('__le__', '__gt__', '__ge__') if m in Vector2D.__dict__])

Output:

True True False True
[Vector2D(1, 2), Vector2D(3, 4), Vector2D(0, 6)]
Vector2D(0, 6) Vector2D(1, 2)
['__le__', '__gt__', '__ge__']

There’s only __eq__ and __lt__, and yet >, <=, >=, sorted, max and min all work. The last line confirms it: the decorator added __le__, __gt__ and __ge__ to the class. The diagram sums up who writes each method:

Comparison methods: which you write and which are generatedTable: the programmer writes __eq__ and __lt__; object provides __ne__; @total_ordering generates __le__, __gt__ and __ge__ with their rules. Warning: ordering by modulo, two different vectors with the same modulo are not <=.With @total_ordering you write two and get six__eq__a == bwritten by: yousame x and y__lt__a < bwritten by: youshorter modulo()__ne__a != bwritten by: objectnot (a == b)__le__a <= bwritten by: @total_orderinga < b or a == b__gt__a > bwritten by: @total_orderingnot (a < b) and a != b__ge__a >= bwritten by: @total_orderingnot (a < b)Vector2D(3, 4) <= Vector2D(4, 3) → Falsesame modulo (5): neither < nor == → <= is False. Order by data consistent with __eq__.
Violet: you write it. Coral: the decorator generates it from < and ==. If < and == tell different stories, the ordering gets weird.

And now the trap: a <= Vector2D(4, 3) is False. Both have length 5, but they aren’t equal (different coordinates), so neither < nor == holds, and <= is built from those two pieces. If your < and your == don’t look at the same thing, the ordering gets weird. With numbers, fractions or money it doesn’t happen, because “equal” and “neither less nor greater” mean the same.

For the exam: @total_ordering needs __eq__ + one of __lt__, __le__, __gt__, __ge__. The most natural choice is __eq__ and __lt__. And sorted() and list.sort() only need __lt__, even without the decorator.

—

Binary arithmetic operators

A binary operator has two operands: self on the left and the other one on the right. Each method returns a new object and doesn’t touch the originals:

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

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def __add__(self, other):                    # v + w
        if isinstance(other, Vector2D):
            return Vector2D(self.x + other.x, self.y + other.y)
        return NotImplemented

    def __sub__(self, other):                    # v - w
        if isinstance(other, Vector2D):
            return Vector2D(self.x - other.x, self.y - other.y)
        return NotImplemented

    def __mul__(self, other):                    # v * number  or  v * w (dot product)
        if isinstance(other, (int, float)):
            return Vector2D(self.x * other, self.y * other)
        if isinstance(other, Vector2D):
            return self.x * other.x + self.y * other.y
        return NotImplemented

    def __truediv__(self, number):               # v / number
        if isinstance(number, (int, float)):
            return Vector2D(self.x / number, self.y / number)
        return NotImplemented


v, w = Vector2D(3, 4), Vector2D(1, 2)
print(v + w, v - w)
print(v * 2, v * w, v / 2)
print(v + w + Vector2D(10, 10))                  # (v + w) + …: each + returns a Vector2D
print(v, w)                                      # the originals don't change
try:
    print(v + 2)
except TypeError as err:
    print('TypeError:', err)

Output:

Vector2D(4, 6) Vector2D(2, 2)
Vector2D(6, 8) 11 Vector2D(1.5, 2.0)
Vector2D(14, 16)
Vector2D(3, 4) Vector2D(1, 2)
TypeError: unsupported operand type(s) for +: 'Vector2D' and 'int'

Some decisions worth noticing:

  • v * 2 and v * w use the same __mul__, which checks the other’s type: with a number it scales the vector; with another vector it returns the dot product, 3 · 1 + 4 · 2 = 11 (a number, not a vector).
  • v + w + Vector2D(10, 10) works because v + w returns a Vector2D, and the next one is added to that: (4, 6) + (10, 10) = (14, 16).
  • v and w are unchanged at the end: arithmetic operators create new objects, just as 3 + 5 doesn’t change the 3.
  • v + 2 ends in a TypeError with a clear message. That’s the effect of return NotImplemented: Python tries the other operand, it doesn’t know either, and Python raises the error itself.

Raising the TypeError yourself (or returning None, False or a string) when the type doesn’t fit. If you raise it, you deny Python the chance to ask the other object; if you return None, v + 'hello' “works” and returns None silently. You’ll see it in the “What’s wrong here?” section of the exercises.

—

Unary operators

Unary operators act on a single operand: -v calls __neg__, +v calls __pos__ and abs(v) calls __abs__. And a very useful one that doesn’t look like an operator: __bool__, which decides whether the object is “truthy” in an if.

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

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def __neg__(self):                       # -v
        return Vector2D(-self.x, -self.y)

    def __abs__(self):                       # abs(v): the modulus
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __bool__(self):                      # if v: … (the zero vector is "falsy")
        return self.x != 0 or self.y != 0


v = Vector2D(3, 4)
print(-v, abs(v), abs(-v))
for u in [v, Vector2D(0, 0)]:
    print(u, 'is truthy' if u else 'is falsy')

Output:

Vector2D(-3, -4) 5.0 5.0
Vector2D(3, 4) is truthy
Vector2D(0, 0) is falsy

abs(v) is the modulus: the square root of 3² + 4² = 25, that is, 5.0. And the zero vector is “falsy”, like the number 0 or an empty list. If a class has no __bool__ but has __len__, Python uses len(obj) != 0; if it has neither, every object is truthy.

—

Reflected operators: the concept that confuses most

v * 2 works. What about 2 * v? There the left operand is the integer 2, and int doesn’t know how to multiply vectors. The same happens to sum(), which starts by adding 0 + first_element:

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

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

    def __add__(self, other):
        if isinstance(other, Vector2D):
            return Vector2D(self.x + other.x, self.y + other.y)
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            return Vector2D(self.x * other, self.y * other)
        return NotImplemented


v = Vector2D(3, 4)
print(v * 2)
for expr in ['2 * v', 'sum([v, v, v])']:
    try:
        print(expr, '->', eval(expr))
    except TypeError as err:
        print(expr, '-> TypeError:', err)


class Vector2DFixed(Vector2D):                # inherits everything and adds the reflected ones
    def __rmul__(self, other):                # other * self (the vector is on the right)
        return self * other                   # multiplying by a number is commutative

    def __radd__(self, other):                # for sum(): it starts with 0 + first
        if other == 0:
            return self
        return NotImplemented


v = Vector2DFixed(3, 4)
print('2 * v ->', 2 * v)
print('sum ->', sum([v, Vector2DFixed(1, 2), Vector2DFixed(10, 10)]))

Output:

Vector2D(6, 8)
2 * v -> TypeError: unsupported operand type(s) for *: 'int' and 'Vector2D'
sum([v, v, v]) -> TypeError: unsupported operand type(s) for +: 'int' and 'Vector2D'
2 * v -> Vector2D(6, 8)
sum -> Vector2D(14, 16)

When the left operand returns NotImplemented, Python doesn’t give up yet: it looks in the right operand for the reflected method, which has the same name with an r in front (__rmul__, __radd__, __rsub__, __rtruediv__…). In 2 * v, Python tries (2).__mul__(v), gets NotImplemented and calls v.__rmul__(2). The diagram follows the three cases:

How Python translates an operatorThree expressions: v + w is solved by v's __add__; v + 2 gets NotImplemented from v and from 2 and ends in TypeError; 2 * v gets NotImplemented from 2 and is solved by v's __rmul__.What Python does with "a op b": first a, then bv + w1st: leftv.__add__(w)Vector2D(4, 6)v + 21st: leftv.__add__(2)NotImplemented2nd: right(2).__radd__(v)NotImplementedTypeError2 * v1st: left(2).__mul__(v)NotImplemented2nd: rightv.__rmul__(2)Vector2D(6, 8)If a method returns NotImplemented, Python tries the next one; if both do, TypeError.
The left operand gets the first say; the right one, the second (the r methods). Data from the theory.

Vector2DFixed (which inherits from Vector2D, as you saw in the previous topic) adds two reflected methods:

  • __rmul__ returns self * other: multiplying by a number is commutative, so reusing __mul__ is enough.
  • __radd__ only accepts 0, which is what sum() needs: 0 + v1 = v1, and from there sum carries on with v1 + v2, a normal vector addition.

The ones that don’t commute: __rsub__ and __rtruediv__

Careful with subtraction and division: 10 - x is not x - 10. In __rsub__, self is the one on the right of the subtraction:

class Euros:
    def __init__(self, amount):
        self.amount = amount

    def __repr__(self):
        return f'{self.amount} €'

    def __sub__(self, other):                 # euros - number
        if isinstance(other, (int, float)):
            return Euros(self.amount - other)
        return NotImplemented


class EurosWrong(Euros):
    def __rsub__(self, other):                # other - self… computed backwards
        return self - other


class EurosRight(Euros):
    def __rsub__(self, other):                # other - self, in that order
        return Euros(other - self.amount)


print('Euros(3) - 10  =', Euros(3) - 10)
print('10 - EurosWrong(3) =', 10 - EurosWrong(3))
print('10 - EurosRight(3) =', 10 - EurosRight(3))

Output:

Euros(3) - 10  = -7 €
10 - EurosWrong(3) = -7 €
10 - EurosRight(3) = 7 €

EurosWrong copies the idea of __rmul__ (return self - other) and computes 3 − 10 = −7 €, when 10 - EurosWrong(3) should give 7 €. EurosRight does the maths in the right order: other - self.amount.

Writing __rsub__ or __rtruediv__ as return self - other or return self / other, like the commutative ones. The result comes out with the sign flipped (or inverted, for division). In the practice you’ll see it with fractions: 1 - 3/4 must give 1/4, not -1/4.

ExpressionFirst attemptIf it returns NotImplemented
v + wv.__add__(w)w.__radd__(v)
2 * v(2).__mul__(v)v.__rmul__(2)
10 - e(10).__sub__(e)e.__rsub__(10) → 10 - e, in that order
sum([v, w])0 + v → (0).__add__(v)v.__radd__(0)

—

+= and friends: __iadd__

What does v += w do? It depends on whether the class has __iadd__:

  • Without __iadd__, Python turns it into v = v + w: it calls __add__, creates a new object and changes what v points to.
  • With __iadd__, it calls v.__iadd__(w), which modifies the object itself and must return it (return self).

The difference only shows when another name points to the same object:

class Vector2D:                       # immutable: __add__ only
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Vector2D({self.x}, {self.y})'

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


class Position(Vector2D):              # mutable: adds __iadd__
    def __iadd__(self, other):
        self.x += other.x              # changes THIS object…
        self.y += other.y
        return self                    # …and returns it: required


for Class in [Vector2D, Position]:
    v = Class(1, 2)
    alias = v                          # another name for the same object
    v += Vector2D(10, 10)
    print(f'{Class.__name__:<9} v = {v}  alias = {alias}  same object: {v is alias}')

Output:

Vector2D  v = Vector2D(11, 12)  alias = Vector2D(1, 2)  same object: False
Position  v = Vector2D(11, 12)  alias = Vector2D(11, 12)  same object: True
v += w with and without __iadd__Left: without __iadd__, += uses __add__, creates a Vector2D(11, 12) and only v points to it; alias stays on Vector2D(1, 2). Right: with __iadd__ the same object is modified and v and alias share it.Vector2D: __add__ onlyv += Vector2D(10, 10)valiasVector2D(11, 12)Vector2D(1, 2)v points to a NEW object; alias stays on the old onev is alias → FalsePosition: with __iadd__v += Vector2D(10, 10)valiasPosition: x=11, y=12the SAME object changes; both names see itv is alias → True
Without __iadd__, v += w is v = v + w: a new object. With __iadd__ (and its return self), the existing one changes.

With Vector2D, alias is still (1, 2): v now points to a new vector. With Position, both names see (11, 12), because it’s the same object, modified. Python’s own types behave the same: with a tuple, t += (1,) creates a new tuple; with a list, l += [1] extends the same list and every alias sees it.

(Why does the Position print as Vector2D(11, 12)? Because __repr__ writes 'Vector2D' by hand in the parent. In the inheritance topic you saw the fix: type(self).__name__.)

Writing __iadd__ and forgetting return self. Python assigns to v whatever the method returns, so v becomes None and the error appears on the next line ('NoneType' object has no attribute …).

—

The complete Vector2D class

Putting the pieces together, this is what a “numeric” class like Vector2D usually has, and when each part is worth it (containers, with __len__, __getitem__ and __contains__, are in the practice):

MethodWhat forEssential?
__repr__ (and __str__)Printing and debuggingYes, always
__eq__==, !=, inIf comparing by value makes sense
__hash__Sets and dictionary keysOnly if the objects don’t change
__lt__ + @total_ordering<, <=, >, >=, sorted, maxIf there’s a natural order
__add__, __sub__, __mul__, __truediv__ArithmeticIf the operations make sense
__radd__, __rmul__, __rsub__, __rtruediv__Number on the left, sum()If mixed with numbers
__neg__, __abs__, __bool__-v, abs(v), if v:Depending on the case
__iadd__+= modifying the objectOnly in mutable classes

The golden rule of overloading: the operator should do what anyone would expect. A + that deletes things or a < that compares something odd still runs, but it confuses whoever reads your code. If the operation isn’t obvious, a named method is better (v.rotate(90)).

—

Function overloading: polymorphism with parameters

In Java (and C++) you can write a method several times with the same name and different parameters: that’s method overloading. In Python it doesn’t exist: a second def with the same name replaces the first.

def area(side):
    return side * side


def area(base, height):                 # NOT overloading: replaces the one above
    return base * height


print(area(3, 4))
try:
    print(area(3))
except TypeError as err:
    print('TypeError:', err)


def area_fixed(base, height=None):      # one optional parameter does both jobs
    if height is None:
        return base * base              # square
    return base * height                # rectangle


print(area_fixed(3), area_fixed(3, 4))

Output:

12
TypeError: area() missing 1 required positional argument: 'height'
9 12

area(3, 4) works because the second definition overwrote the first, and area(3) raises TypeError: the one-parameter version no longer exists. In Python you get the same effect with a single function that accepts several ways of calling it: parameters with default values (height=None), *args, or checking the type with isinstance inside. And in classes, the polymorphism you saw in inheritance: the same method in different classes.

“Operator overloading” and “method overloading” sound alike, but they’re different things. The first (giving + a meaning in your class) exists in Python; the second (several methods with the same name and different signatures) doesn’t.

—

Common mistakes with operator overloading

__eq__ without isinstance: v == 5 raises AttributeError instead of giving False.

Defining __eq__ and then putting the objects in a set: TypeError: unhashable type. __hash__ is missing.

Returning False, None or raising TypeError when the type doesn’t fit, instead of return NotImplemented.

Forgetting the reflected methods: v * 2 works and 2 * v doesn’t; sum() of your objects fails with 'int' and 'Vector2D'.

__rsub__ written as self - other: the result comes out with the sign flipped.

__iadd__ without return self: the variable becomes None.

Writing only __str__: lists still show <… object at 0x…>.

—

Watch it step by step in Python Tutor

pythontutor.com draws the objects and the call stack, and there you can see that an operator is a method call. Choose Python 3 and paste:

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

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

    def __mul__(self, n):
        return Vector2D(self.x * n, self.y * n)

    def __rmul__(self, n):
        return self * n


v = Vector2D(3, 4)
w = v + Vector2D(1, 2)
z = 2 * v
print(w.x, w.y, z.x, z.y)

Output:

4 6 6 8

What to watch step by step:

  1. On the line w = v + Vector2D(1, 2) you’ll see an __add__ frame appear on the stack, with self pointing to v and other to the new vector. The + turned into a call.
  2. Inside __add__ a third object is created, the result. Notice that v doesn’t change.
  3. On z = 2 * v, Python doesn’t enter any method of the 2 (integers don’t show on the stack): it jumps straight to v‘s __rmul__, with n equal to 2.
  4. From __rmul__ a second frame appears, the one of __mul__, because of return self * n. Two stacked frames for a single *.

—

Quick summary

I want…I writeDetail
Nice printing__str__ and __repr__Lists use __repr__
== by value__eq__ + isinstancereturn NotImplemented if the type doesn’t fit
The object in a set__hash__Equal → same hash
Ordering__eq__ + __lt__ + @total_orderingThe decorator generates <=, >, >=
+, -, *, /__add__, __sub__, __mul__, __truediv__They return a new object
Number on the left__radd__, __rmul__, __rsub__, __rtruediv__Subtraction and division don’t commute
+= that modifies__iadd__Always return self
Several “versions” of a functionDefault parametersPython has no function overloading

Three ideas to keep:

  1. An operator is a method call: a + b is a.__add__(b). Overloading means writing that method.
  2. NotImplemented isn’t an error, it means “ask the other one”: that’s how reflected operators and clear error messages work.
  3. In reflected methods, self is on the right: addition and multiplication commute; subtraction and division don’t.

In the operator overloading practice we build three complete classes: fractions that simplify themselves, money without cent errors (and with currency conversion) and a library shelf you use like a list.

Next step: practicePython operator overloading practice — fractions that simplify themselves, money without cent errors and the library shelf (and the dinner kitty that was always one cent off)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *