Arithmetic in Python looks like the arithmetic you already know, and then it does not. The division operators behave differently from most languages, exponentiation binds more tightly than a minus sign, floor division rounds towards negative infinity rather than towards zero, and ties in round() go to the even neighbour. These are small rules, but each one has cost somebody an afternoon, so this lesson pins them down.
What arithmetic operators does Python have?
| Operator | Meaning | 17 and 5 |
|---|---|---|
+ | addition | 22 |
- | subtraction | 12 |
* | multiplication | 85 |
/ | true division, always a float | 3.4 |
// | floor division, discards the fractional part | 3 |
% | remainder after floor division | 2 |
** | exponentiation | 1419857 |
divmod(a, b) | quotient and remainder together | (3, 2) |
a, b = 17, 5
print('a + b ->', a + b)
print('a - b ->', a - b)
print('a * b ->', a * b)
print('a / b ->', a / b)
print('a // b ->', a // b)
print('a % b ->', a % b)
print('a ** b ->', a ** b)
print('divmod ->', divmod(a, b))
print('abs(-a) ->', abs(-a))
print('round(2.5) ->', round(2.5), '| round(3.5) ->', round(3.5))
The last line is worth a second look: round(2.5) gives 2 and round(3.5) gives 4. Python rounds ties to the nearest even value, which keeps repeated rounding free of drift in the way "always round up" does not. For a different tie-breaking rule, use the decimal module, which lets you choose exactly how ties are resolved.
Python also has bitwise operators for working with the underlying bits of integers: & for and, | for or, ^ for exclusive or, << and >> for shifting, and ~ for inverting. They operate on whole numbers rather than on text or booleans, and they are how you set and clear individual flags efficiently.
How does operator precedence actually work?
Precedence decides which operator binds first when several appear in one expression. The order you need most often is short: ** binds tightest, then * and / and // and %, then + and -, then comparisons. Everything else, including and and or, comes later still.
Two consequences surprise people who learned arithmetic in school. A unary minus binds less tightly than exponentiation, so -2 ** 2 means -(2 ** 2) and equals -4, not 4. And exponentiation is right-associative, so 2 ** 3 ** 2 means 2 ** (3 ** 2), which is 512 rather than 64.
print('-2 ** 2 ->', -2 ** 2)
print('(-2) ** 2 ->', (-2) ** 2)
print('2 ** 3 ** 2 ->', 2 ** 3 ** 2)
print('2 + 3 * 4 ->', 2 + 3 * 4)
print('(2 + 3) * 4 ->', (2 + 3) * 4)
print('10 - 4 - 3 ->', 10 - 4 - 3)
print('100 / 10 / 5 ->', 100 / 10 / 5)
print('7 // 2 % 3 ->', 7 // 2 % 3)
Multiplication, division and subtraction are all left-associative, which is why 100 / 10 / 5 is 2.0 and not 20. When you are not certain how a line will parse, add parentheses. They cost nothing, and they turn an expression you have to decode into one you can read at a glance.
Python also offers an augmented form for every arithmetic operator: +=, -=, *=, /=, //=, %= and **=. Each one rebinds the name to the result, and each binds with the precedence of the operator it wraps, so x **= 2 squares the value rather than doing something surprising. For a plain number these are convenient. For a string, += appends, which is the fastest way to build a long string in a loop.
What does / do differently from //?
/ always produces a float, even when the division is exact, because that is the honest answer to "how many times does this go into that". // produces a whole number by discarding the fractional part, and the direction of that discard is where the surprise lives: it always moves towards negative infinity, not towards zero.
print('7 / 2 ->', 7 / 2)
print('7 // 2 ->', 7 // 2)
print('-7 // 2 ->', -7 // 2)
print('-7 % 2 ->', -7 % 2)
print('7 % -2 ->', 7 % -2)
print('divmod(7, 2) ->', divmod(7, 2))
print('divmod(-7, 2) ->', divmod(-7, 2))
print('7.5 // 2 ->', 7.5 // 2)
print('10 // 3 ->', 10 // 3, '| 10 / 3 ->', 10 / 3)
try:
print(10 / 0)
except ZeroDivisionError as error:
print('10 / 0 ->', error)
So -7 // 2 is -4, because floor division asks "what is the largest integer not greater than -3.5", and the answer is -4. The remainder takes the sign of the divisor, so -7 % 2 is 1, which keeps the useful identity that a == (a // b) * b + (a % b) always holds. That invariant is what makes divmod safe for splitting amounts into whole units and leftovers.
Floor division also works on floats, where it returns a float: 7.5 // 2 is 3.0. It is not a rounding function, though. For rounding to a number of decimal places, use round().
What happens when arithmetic goes wrong?
Dividing by zero raises ZeroDivisionError for both integers and floats, and Python refuses to guess a result. Integer arithmetic, by contrast, has no ceiling at all: 10 ** 400 simply produces a number with 401 digits. Floats are the constrained ones. An operation beyond their range raises OverflowError, while one that merely overshoots returns infinity instead. These behaviours are worth knowing so a strange result does not look like a mystery.
try:
print(10.0 ** 400)
except OverflowError as error:
print('float overflow ->', error)
print('1e308 * 10 ->', 1e308 * 10)
print('int arithmetic has no ceiling ->', len(str(2 ** 100)), 'digits')
print('a huge float still converts ->', int(1e308) > 0)
There is a second category of wrongness that raises no exception at all: accumulated floating point error. Adding numbers one at a time rounds the running total after every step, so a small value can be swallowed entirely by a large one. The standard library offers a fully accurate alternative, and since Python 3.12 the built-in sum() has also improved, using compensated summation to reduce that drift.
import math
from decimal import Decimal
from fractions import Fraction
tenths = [0.1] * 10
print('sum() of ten tenths ->', sum(tenths))
print('math.fsum() ->', math.fsum(tenths))
# A tiny value between two enormous ones: sum() loses it, fsum() keeps it.
mixed = [1e-16, 1.0, 1e16]
print('sum() ->', sum(mixed))
print('math.fsum()->', math.fsum(mixed))
print('Fraction ->', Fraction(1, 3) + Fraction(1, 6))
print('Decimal ->', Decimal('0.1') * 3)
Use math.fsum() when you are adding many floats and want the best possible result. Use Fraction when the values are ratios you want held exactly. Use Decimal when the values are money or anything else a customer will read on an invoice.
Your turn: build a change calculator
- Write a script that takes a total price and the number of equal payments and prints how much each payment is. Run it with 100 and 3, and notice that
100 / 3is not exactly 33.33. - Now use
Decimalinstead, built from strings, and print the same division. Explain in a comment whyDecimal(100 / 3)is a worse choice thanDecimal('100') / Decimal('3'). - Add a line that splits a quantity into boxes and leftovers using
divmod(), for example 17 apples into boxes of 5. Confirm thatboxes * 5 + leftover == 17. - Print the value of
-7 // 2andint(-7 / 2)side by side. They differ, and the difference is floor division versus truncation towards zero. - Finish by summing a list of one hundred values of
0.1twice: once withsum()and once withmath.fsum(). Print both, and write one sentence explaining which one you would use in a reporting script.
Takeaways
/always returns a float;//returns a whole number rounded towards negative infinity.- The remainder takes the sign of the divisor, so
a == (a // b) * b + (a % b)always holds. -2 ** 2is-4, and2 ** 3 ** 2is 512, because exponentiation is right-associative and binds tighter than unary minus.- Integer arithmetic never overflows; float arithmetic either raises
OverflowErroror produces infinity. - Use
math.fsum()for float sums,Fractionfor exact ratios, andDecimalfor money.