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.
Table of Contents
—
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 write | Python calls | Magic method |
|---|---|---|
a + b | a.__add__(b) | addition |
a - b | a.__sub__(b) | subtraction |
a * b | a.__mul__(b) | multiplication |
a / b | a.__truediv__(b) | division |
a == b | a.__eq__(b) | equality |
a < b | a.__lt__(b) | less than |
-a | a.__neg__() | negation |
abs(a) | a.__abs__() | absolute value |
len(a) | a.__len__() | length |
x in a | a.__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(),!rin 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:
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 * 2andv * wuse 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 becausev + wreturns aVector2D, and the next one is added to that: (4, 6) + (10, 10) = (14, 16).vandware unchanged at the end: arithmetic operators create new objects, just as3 + 5doesn’t change the 3.v + 2ends in aTypeErrorwith a clear message. That’s the effect ofreturn 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:
Vector2DFixed (which inherits from Vector2D, as you saw in the previous topic) adds two reflected methods:
__rmul__returnsself * other: multiplying by a number is commutative, so reusing__mul__is enough.__radd__only accepts 0, which is whatsum()needs: 0 + v1 = v1, and from theresumcarries on withv1 + 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.
| Expression | First attempt | If it returns NotImplemented |
|---|---|---|
v + w | v.__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 intov = v + w: it calls__add__, creates a new object and changes whatvpoints to. - With
__iadd__, it callsv.__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
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):
| Method | What for | Essential? |
|---|---|---|
__repr__ (and __str__) | Printing and debugging | Yes, always |
__eq__ | ==, !=, in | If comparing by value makes sense |
__hash__ | Sets and dictionary keys | Only if the objects don’t change |
__lt__ + @total_ordering | <, <=, >, >=, sorted, max | If there’s a natural order |
__add__, __sub__, __mul__, __truediv__ | Arithmetic | If 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 object | Only 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:
- On the line
w = v + Vector2D(1, 2)you’ll see an__add__frame appear on the stack, withselfpointing tovandotherto the new vector. The+turned into a call. - Inside
__add__a third object is created, the result. Notice thatvdoesn’t change. - On
z = 2 * v, Python doesn’t enter any method of the 2 (integers don’t show on the stack): it jumps straight tov‘s__rmul__, withnequal to 2. - From
__rmul__a second frame appears, the one of__mul__, because ofreturn self * n. Two stacked frames for a single*.
—
Quick summary
| I want… | I write | Detail |
|---|---|---|
| Nice printing | __str__ and __repr__ | Lists use __repr__ |
== by value | __eq__ + isinstance | return NotImplemented if the type doesn’t fit |
The object in a set | __hash__ | Equal → same hash |
| Ordering | __eq__ + __lt__ + @total_ordering | The 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 function | Default parameters | Python has no function overloading |
Three ideas to keep:
- An operator is a method call:
a + bisa.__add__(b). Overloading means writing that method. NotImplementedisn’t an error, it means “ask the other one”: that’s how reflected operators and clear error messages work.- In reflected methods,
selfis 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 →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)