Modules/Python/ Arithmetic

Numbers and Operators

Division splits into three different operators in Python. Get to know them and the surprises they hide.

Overview

The problem it solves

Older languages gave you one division that returned a whole number, quietly throwing away the fraction. Python 3 decided that was a silent bug factory: true division / keeps the fraction and answers in float; floor division // is the "whole-number division" people were actually reaching for — useful for splitting a list into equal chunks, or turning seconds into minutes.

The three divisions

/ true division, // floor division, % the remainder.

Python 3
Output

                            

Mixing ints and floats

An operation involving a float answers in floats. Watch the types change as you edit.

Python 3
Output

                            

The operator set

+ addition
- subtraction
* multiplication
/ true division — always answers in floats
// floor division — rounds down to a whole number
% modulo — the remainder left over
** exponent — 2 ** 10 is 2 raised to the 10th
Beware the negative trap: -7 // 2 is -4, not -3. Floor division always rounds down, toward minus infinity, not toward zero.

Numbers and Operators: A Practical Guide

Python 3 split division into two on purpose — each half is what a different kind of code actually wants.

The two kinds of number

count = 42          # int   - whole numbers, any size
price = 19.99       # float - decimals, with a small rounding error

Python integers have no size limit — 2 ** 1000 produces an exact 302-digit number — which is unusual among programming languages and occasionally very convenient.

Floats are the compromise every language makes for decimals: fast, fixed size, and approximate.

The arithmetic operators:

7 + 3      # 10
7 - 3      # 4
7 * 3      # 21
7 / 3      # 2.3333...  - true division, ALWAYS returns a float
7 // 3     # 2          - floor division, discards the remainder
7 % 3      # 1          - modulo, the remainder
7 ** 3     # 343        - power

The two to watch are / and //. In Python 3, 6 / 3 gives 2.0, not 2 — a float, even when it divides exactly. Use // when you want a whole number, such as an index or a count of complete groups.

What modulo is actually for

% returns the remainder, and it answers three questions that come up constantly:

n % 2 == 0            # is n even?
n % 15 == 0           # is n divisible by 15?
seconds % 60          # seconds left over after whole minutes
index % len(items)    # wrap around to the start of a list

The wrap-around use is the neatest: (i + 1) % len(colours) cycles through a list for ever without ever going out of bounds — the standard way to rotate through colours, players or servers.

Modulo with negative numbers follows the sign of the right operand in Python: -7 % 3 is 2, not -1. That differs from C and JavaScript and is worth remembering when porting code.

Floating point, and where it goes wrong

0.1 + 0.2            # 0.30000000000000004
0.1 + 0.2 == 0.3     # False

This is not a Python bug. Floats are stored in binary, and 0.1 has no exact binary representation, in the same way that 1/3 has no exact decimal one. The error is around 10⁻¹⁶ and it compounds across long calculations.

Two consequences to build into your habits:

Never compare floats with ==. Use a tolerance:

import math
math.isclose(0.1 + 0.2, 0.3)      # True

Never use floats for money. Use decimal.Decimal, or work in whole pence:

from decimal import Decimal
Decimal("0.1") + Decimal("0.2")   # Decimal('0.3')  - exact

Note the strings in that example: Decimal(0.1) from a float would inherit the error you were trying to escape.

Rounding, and the surprise in it

round(2.5)       # 2   -- not 3
round(3.5)       # 4
round(2.675, 2)  # 2.67 -- not 2.68

The first two use banker's rounding: exact halves round to the nearest even number. Over many values this avoids the upward bias that always-round-up introduces, which is why it is the standard in finance and statistics.

The third is the float problem again — 2.675 is really 2.67499999... in binary, so it rounds down. Decimal gives the answer you expected.

Related tools: math.floor and math.ceil always go down and up, int() truncates towards zero (so int(-2.7) is -2, not -3), and abs() drops the sign.

Guided experiments

  1. Read the first four lines. Line by line: 3.5, then 3 (fraction thrown away), then 1 (the leftover), then 1024. Note that 7 // 2 gives 3 while 7 / 2 gives 3.5 — same inputs, different promises.
  2. Check the types. In the second editor, 4 + 2.0 returns 6.0, a float, because one operand was a float. Every operator follows this rule: the float wins.
  3. Break on zero. Add print(1 / 0) to either editor and run. ZeroDivisionError is Python telling you the operation is undefined — the message names the exact line that caused it.
  4. Explore modulo yourself. Replace the numbers with a few of your own, one pair at a time, and check the pattern: x % y is always between 0 and y - 1. That boundedness is why it is so useful for wrapping.

In one line

Python has three division-shaped operators for three different jobs: / when you want the exact answer, // when you want whole-number chunks, and % when you want the remainder that chunking throws away. Getting the right one wrong is a classic source of bugs that produce no error at all — the program just silently rounds. When numbers misbehave, check your operators before your logic.

The operators that shorten common code

x = 5
x += 3       # x = x + 3, and the same for -= *= /= //= %= **=

a, b = b, a  # swap, with no temporary variable

7 < x < 20   # chained comparison, evaluated once

divmod(17, 5)          # (3, 2) - quotient and remainder together
sum([1, 2, 3])         # 6
min(values), max(values)
abs(-4)                # 4

The math module carries the rest of the standard toolkit:

import math

math.sqrt(16)        # 4.0
math.floor(3.7)      # 3
math.ceil(3.2)       # 4
math.pi              # 3.141592653589793
math.log(100, 10)    # 2.0
math.inf             # infinity, useful as a starting "worst value"

math.inf deserves a mention: initialising best = math.inf before a loop looking for a minimum is cleaner and safer than picking an arbitrary large number.

Common mistakes

  • Expecting / to give a whole number. It returns a float in Python 3; use //.
  • Comparing floats with ==. Use math.isclose.
  • Using floats for money. Use Decimal or integer pence.
  • Assuming round always rounds halves up. It rounds to even.
  • Forgetting input() returns a string. input("n: ") * 3 repeats the text; wrap it in int().
  • Integer division on negatives. -7 // 2 is -4, because it floors rather than truncating.

A worked example: divmod and the units it gives you

// and % are almost always wanted together, and divmod returns both in one call:

minutes, seconds = divmod(3725, 60)
hours, minutes = divmod(minutes, 60)

print(f"{hours}h {minutes}m {seconds}s")
1h 2m 5s

Each divmod peels off one unit: how many whole minutes, and what is left over. Applying it again to the minutes gives hours. The same shape converts bytes to kilobytes, pence to pounds, or an item count to pages and a remainder.

Doing it with two operators means writing the division twice, and the two can drift apart when someone edits one and not the other. divmod computes them together, so they cannot disagree.

The general pattern is worth recognising: any time you have a total and a unit size, // is "how many whole units" and % is "what did not fit". Chunking a list, paginating results, laying out a grid and formatting a duration are all the same arithmetic.

Comparing floats without lying to yourself

"Never compare floats with ==" is easy to say and needs one more sentence to be useful, because math.isclose has two tolerances and the default only covers one case.

import math

print(math.isclose(0.1 + 0.2, 0.3))
print(math.isclose(1e-9, 0.0))
print(math.isclose(1e-9, 0.0, abs_tol=1e-6))
True False True

By default isclose uses a relative tolerance: the two values must be within a small proportion of each other. That is the right test for ordinary magnitudes, and it is why the first line works.

It fails against zero, and the second line shows it. Nothing is within a proportion of zero except zero itself, so a comparison to zero always needs an absolute tolerance, chosen from what "negligible" means in your problem. That is what abs_tol is for, and forgetting it is the most common way this function surprises people.

The related habit is to compare rounded values only when rounding is the question. round(a, 2) == round(b, 2) is a legitimate test for "do these agree to the penny", and a poor substitute for a tolerance when the question is "are these the same number".

The operators that surprise people on negatives

Three operations disagree with C, JavaScript and most people's intuition, all for the same underlying reason: Python floors rather than truncating.

print(-7 // 2, -7 % 3, int(-2.7))
-4 2 -2

-7 // 2 is -4, not -3, because floor division rounds towards negative infinity rather than towards zero. -7 % 3 is 2, not -1, because the result takes the sign of the right-hand operand. And int(-2.7) is -2 because int truncates, which is the one that does *not* floor — making // and int() disagree on negatives.

The modulo behaviour is the useful one. Because the result always has the sign of the divisor, i % len(items) is a valid index for any i, positive or negative, which is exactly what makes the wrap-around idiom safe. In languages where modulo can return a negative, that idiom needs a correction term.

The rule to carry: // and % are defined so that a == (a // b) * b + a % b holds for every pair, and everything else follows from keeping that identity true.

Choosing the right numeric type

Four types cover numeric work in Python, and picking the wrong one is the cause of most "the arithmetic is wrong" reports.

**int** for anything countable: quantities, indices, ids, pence. It is exact and unbounded, so there is no overflow to plan around and no rounding to worry about. When a value will only ever be whole, an int removes an entire class of problem.

**float** for measurement and anything scientific: lengths, temperatures, probabilities, timings. It is fast, fixed-size and approximate, and the approximation is acceptable because the input was approximate too. A sensor reading accurate to three decimal places loses nothing to a representation error at the sixteenth.

**decimal.Decimal** for money and anything a person will check by hand. It works in base ten, so the values you write are the values it holds, and it carries an explicit precision and rounding mode. The cost is speed, which almost never matters next to producing a total that matches the invoice.

**fractions.Fraction** for exact ratios, where a third really must be a third rather than 0.333…. Rare, and the right answer when it applies.

The decision is easier than it looks: if the number counts something, int. If it measures something, float. If somebody will audit it, Decimal. Mixing a Decimal with a float in one expression converts back to float and discards the exactness you paid for, so the choice has to hold across the whole calculation rather than at the end of it.

The bitwise operators, briefly

Six operators work on the binary representation of integers, and while most Python never touches them, they turn up often enough to be worth recognising.

&, | and ^ are and, or and exclusive-or applied bit by bit. ~ inverts every bit. << and >> shift left and right, which multiply and divide by powers of two — n << 3 is n * 8, and n >> 1 is n // 2 for positive numbers.

Where you will meet them is flags. A single integer can carry many independent on-off settings, one per bit, which is how file permissions, regular-expression options and many C-derived APIs are expressed. re.IGNORECASE | re.MULTILINE is exactly this: two flags combined into one integer with |, and the library tests each with &.

Two cautions. These are the same characters as the set operators, and the meaning is entirely different — & on two sets is intersection, on two integers it is bitwise and. And and/or are not the same as &/|: the word forms short-circuit and return one of their operands, the symbols always evaluate both sides and work bit by bit. Using & where and was meant is a real bug that often produces the right answer on the values you tested.

For readable flags in your own code, enum.Flag gives the same combining behaviour with names attached, which is easier to debug than an integer whose meaning is spread across five bit positions.

Questions people ask

Is there a maximum integer? No. Python integers grow to whatever memory allows.

How do I round to two decimal places for display? f"{value:.2f}", which formats without changing the stored value.

What is the difference between // and int()? // floors towards negative infinity; int() truncates towards zero. They differ on negative numbers.

How do I generate a random number? random.randint(1, 6) for integers, random.random() for a float between 0 and 1. For anything security-related use the secrets module instead.

Why is 0.1 + 0.2 not 0.3? Binary floating point cannot represent 0.1 exactly. Every language with IEEE 754 floats behaves the same way.

Is ** the same as math.pow? Nearly — ** keeps integers exact where it can, while math.pow always returns a float.

Why does 2 ** 0.5 return a float? Because the result is not a whole number. ** keeps integers exact only when both operands are integers and the exponent is not negative.

How do I check whether a float is a whole number? x.is_integer(), which is a method on floats and reads better than comparing against int(x).

Recap in one screen

  • int is unbounded and exact; float is fast and approximate.
  • / always returns a float; // floors; % gives the remainder and is how you wrap around a list.
  • Floats cannot represent decimals exactly — compare with math.isclose, and use Decimal for money.
  • round uses banker's rounding, so round(2.5) is 2.
  • input() returns text; convert it before doing arithmetic.

Check yourself

0 of 3

Answer without scrolling back up.

  1. 7 / 2 in Python 3 gives:

  2. 7 % 3 evaluates to:

  3. 2 ** 10 evaluates to:

Cheat sheet

Numbers and Operators

Older languages gave you one division that returned a whole number, quietly throwing away the fraction. Python 3 decided that was a silent bug factory: true division / keeps the fraction and answers in float; floor division // is the "whole-number division" people were actually reaching for — useful for splitting a list into equal chunks, or turning seconds into minutes.

PYTHON · vizlearn.in/python/numbers_and_operators.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.