Skip to content

Numbers and arithmetic

Numbers show up in almost every program you write. A shopping cart totals a price. A game updates a score. A script counts how many times something happened. Python gives you arithmetic operators that work like paper maths, plus a few more that are worth knowing from the start.

Python's arithmetic operators cover the standard set plus integer division, modulo, and exponentiation. A few behaviors differ from other languages in ways that matter in practice: / always returns a float, floor division rounds toward negative infinity, and modulo follows true modulo semantics.

Python's numeric tower: int (arbitrary precision), float (IEEE 754 binary64), complex (not covered here). The arithmetic operators follow mathematical definitions rather than C convention: // is floor division (toward negative infinity), % carries the sign of the divisor, and both together satisfy the identity a == (a // b) * b + (a % b) for all integer inputs.

The operators

The four operators from maths (+, -, *, /) work exactly as you would expect. Python adds three more that you will use constantly: integer division, remainder, and exponentiation.

The standard four operators behave as expected, with one notable rule: / always returns a float, even when the result is a whole number. The three additional operators extend what you can express without any extra work.

All seven operators map to dunder methods: + to __add__, // to __floordiv__, % to __mod__, ** to __pow__, and so on. Mixed int/float operations widen to float. / always returns float regardless of operand types.

python
price    = 12.99
quantity = 3

print(price * quantity)   # 38.97
print(price + 2)          # 14.99
print(price - 1.00)       # 11.99
OperatorNameExampleResult
+Addition5 + 38
-Subtraction5 - 32
*Multiplication5 * 315
/Division5 / 31.6666...
//Integer division5 // 31
%Remainder5 % 32
**Exponentiation5 ** 3125

Division: / vs //

/ always gives you the exact decimal result, even if the answer is a whole number. // gives you only the whole-number part, cutting off everything after the decimal point. It does not round; it cuts:

/ always returns a float, regardless of whether the inputs are integers. // returns the floor of the result: the largest integer that is less than or equal to the true result. For positive numbers that is the same as truncation. For negatives, it is not:

/ is true division: always returns float. // is floor division: it applies math.floor() to the true quotient, always rounding toward negative infinity rather than toward zero. This differs from C and Java, where integer division truncates. The mathematical benefit: Python's // and % satisfy the identity a == (a // b) * b + (a % b) for all integer inputs including negatives. C's truncating division breaks this identity for negatives.

python
10 / 2     # 5.0   (always float, even when it divides evenly)
10 / 3     # 3.3333333333333335

10 // 3    # 3
7  // 2    # 3
-7 // 2    # -4    (floors toward negative infinity, not toward zero)

The -7 // 2 result surprises people. You will mostly use // with positive numbers where this does not come up. Keep it in the back of your mind for when negatives show up.

Python calls this floor division because it applies the mathematical floor function. Other languages truncate toward zero instead, giving a different result for negatives. The name // is a hint: divide, then floor.

// implements floor(a / b), not truncation. The identity a == (a // b) * b + (a % b) holds for all integer inputs in Python. In C and Java, where / truncates toward zero, this identity fails for negative values and % acts as remainder (takes sign of the dividend) rather than true modulo (takes sign of the divisor).

The remainder operator %

% gives you what is left over after integer division. If 10 // 3 is 3 (because 3 goes into 10 three times), then 10 % 3 is 1 (because 3 × 3 = 9, and 10 - 9 = 1). The most common use is checking whether a number is even or odd.

% is the modulo operator. Even/odd checking is the obvious use, but it generalises to any cycling or wrapping problem: keeping a counter within a range, distributing items across groups, repeating a sequence. The pattern is always value % limit, which returns something between 0 and limit - 1.

Python's % is true modulo: the result always carries the sign of the divisor. This differs from C and Java where % is a remainder operator and takes the sign of the dividend. In Python, -7 % 3 is 2, not -1, because modulo is defined as a - (a // b) * b and // floors toward negative infinity. This consistent sign behavior is what makes % reliable for cycling and wrapping with negative inputs.

python
10 % 3    # 1
10 % 2    # 0  (divides evenly)
10 % 7    # 3

6 % 2     # 0  (even)
7 % 2     # 1  (odd)

Exponentiation **

** raises a number to a power. Use two asterisks, not the ^ symbol (which means something else in Python):

** is exponentiation. It works with floats too, which lets you express roots as fractional powers rather than a separate function call:

** calls __pow__. With two int operands it returns int; with any float operand it returns float. One precedence trap: -2 ** 2 parses as -(2 ** 2) because ** binds tighter than unary minus, giving -4, not 4. Use parentheses: (-2) ** 2.

python
2 ** 10    # 1024
3 ** 3     # 27
9 ** 0.5   # 3.0  (square root: raise to the power of 0.5)

Operator precedence

Python follows the standard maths order: exponentiation first, then multiplication and division, then addition and subtraction. When you are unsure, use parentheses. They make the intention clear and cost nothing:

Python follows the standard PEMDAS/BODMAS order. The part that trips people up: /, //, and % all share the same precedence level and evaluate left to right when mixed. Parentheses are free; use them whenever the order is not obvious at a glance:

Precedence from highest to lowest among arithmetic operators: **, then unary -, then * / // % (left-to-right at equal precedence), then + -. The unary minus interaction with ** is a subtle trap: -2 ** 2 is -(2 ** 2) = -4 because unary minus binds lower than **. Always parenthesise when combining negation with exponentiation.

python
2 + 3 * 4      # 14, not 20
2 ** 3 + 1     # 9,  not 512
10 - 4 / 2     # 8.0, not 3.0

(2 + 3) * 4    # 20
10 / (2 + 3)   # 2.0

How int and float interact

Python has a consistent rule: / always returns a decimal (even 4 / 2 gives 2.0), and any operation mixing an integer and a decimal gives a decimal. When you need a whole number, use // or convert with int().

Type rules are predictable: / always returns float. // and % with two integers return int. Any operation mixing int and float returns float. This means 4 / 2 is 2.0, not 2, which matters when you need an integer (for example, to use as an index).

Type coercion follows a fixed hierarchy: int widens to float in mixed operations. / maps to __truediv__, which always returns float. // maps to __floordiv__: with two int operands it returns int; with any float operand it returns float. These rules are consistent and predictable; the only surprise is that / never returns int even for 4 / 2.

python
4 / 2      # 2.0   (float, always)
4 // 2     # 2     (int)
4 + 2      # 6     (int)
4 + 2.0    # 6.0   (float)
4 * 0.5    # 2.0   (float)

Float precision

There is a gotcha that surprises almost everyone at some point:

python
0.1 + 0.2   # 0.30000000000000004

That tiny error is not a Python bug. Computers store decimal numbers in binary, and some values like 0.1 cannot be represented exactly. It is similar to how 1/3 cannot be written exactly in decimal. For most everyday calculations it does not matter. For displaying money, round() or the :.2f format specifier will keep the output tidy.

Python floats are IEEE 754 binary64: 64 bits with roughly 15-16 significant decimal digits of precision. The imprecision surfaces because some fractions cannot be represented exactly in binary. 0.1 + 0.2 produces 0.30000000000000004. The drift only shows up when you inspect the raw value; formatting with :.2f or round() hides it in output.

For financial work where fractions of a cent accumulate, Python provides decimal.Decimal in the standard library with exact base-10 arithmetic. That is covered in the Modules chapter.

float is IEEE 754 binary64: sign × mantissa × 2^exponent with 53 bits of mantissa, giving relative precision of 2^-52 ≈ 2.2e-16. Any fraction whose denominator has prime factors other than 2 (like 1/10 = 1/(2×5)) is a non-terminating binary fraction and cannot be stored exactly. The error is small but accumulates in repeated arithmetic.

For exact decimal arithmetic, Python's decimal.Decimal uses arbitrary-precision base-10 internally. For exact rational arithmetic with no rounding at all, fractions.Fraction stores numerator/denominator pairs. Both are in the standard library, covered in the Modules chapter.

Readable number literals

Python lets you put underscores in number literals to make large numbers easier to read. Python ignores them completely; they are just for you:

Underscores are valid anywhere in a numeric literal and are stripped during parsing with no effect on the value. Useful for thousands separators in constants and for grouping digits in binary or hex literals:

Underscores in numeric literals are a tokeniser feature: stripped during lexing with zero effect on the resulting value. Valid in integers, floats, and based literals (0xFF_FF, 0b1010_0001, 1_234.567_890). The only restrictions: cannot appear at the start, end, or adjacent to a decimal point or exponent marker.

python
population  = 8_100_000_000
distance_km = 384_400
pi_approx   = 3.141_592_653

Useful built-in functions

abs()

abs() returns the absolute value: always positive, regardless of the sign of the input. Use it when you care about how far a number is from zero, not which direction.

abs() returns the magnitude of a number. Works on integers and floats. Useful for distance calculations, error margins, and any situation where direction is irrelevant and you only need the size of the value.

abs() calls __abs__ on the operand. For int and float it returns the same type. For complex it returns the magnitude (Euclidean distance from the origin) as a float. Return type matches input type for real numbers.

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5

round()

round() rounds to the nearest integer by default. Pass a second argument to keep a specific number of decimal places:

python
round(3.7)          # 4
round(3.2)          # 3
round(3.14159, 2)   # 3.14

One thing worth knowing: round(2.5) gives 2, not 3. Python rounds to the nearest even number when a value is exactly halfway between two options.

round() uses banker's rounding: when the value is exactly halfway, it rounds to the nearest even number rather than always rounding up. This minimises accumulated error in statistical work but can surprise you if you expect 0.5 to always round up:

python
round(2.5)   # 2  (rounds to nearest even)
round(3.5)   # 4
round(4.5)   # 4  (not 5)
round(3.14159, 2)   # 3.14

round() implements IEEE 754 round-half-to-even (banker's rounding): ties round to the nearest even integer. This differs from the "round half up" convention. With a ndigits argument, round() calls __round__ on the object; custom types can override rounding behavior. Note: because floats are not exact, "ties" like round(2.5) may not actually land at exactly 0.5 in binary, giving results that look inconsistent.

python
round(2.5)   # 2
round(3.5)   # 4
round(4.5)   # 4

divmod()

divmod() gives you both the quotient and the remainder in a single call. It returns a pair of values you can assign to two names at once:

divmod(a, b) is equivalent to (a // b, a % b) but computed in a single step. Use it when you need both values anyway: pagination, time conversion, or distributing items into groups.

divmod() calls __divmod__ on the left operand. It performs the division once and returns both floor quotient and modulo remainder, avoiding redundant computation from calling // and % separately. The result satisfies a == divmod(a, b)[0] * b + divmod(a, b)[1] with Python's floor-division semantics for all integer inputs.

python
divmod(10, 3)   # (3, 1): quotient 3, remainder 1
divmod(7, 2)    # (3, 1)
divmod(9, 3)    # (3, 0)

quotient, remainder = divmod(10, 3)
print(quotient)    # 3
print(remainder)   # 1

What's the (3, 1) thing?

That is a tuple: a fixed pair of values returned together. Tuples get their own chapter. For now, pull the two values apart by assigning to two names at once, as shown above.

In practice

A tip calculator:

python
bill     = 45.50
tip_rate = 0.18
tip      = round(bill * tip_rate, 2)
total    = round(bill + tip, 2)

print(f"Bill:  ${bill}")
print(f"Tip:   ${tip}")
print(f"Total: ${total}")

round() keeps the output looking like money rather than a long run of decimal places.

Counting pages for pagination and tracking progress as a percentage:

python
total_items    = 153
items_per_page = 10

full_pages, leftover = divmod(total_items, items_per_page)
total_pages = (total_items + items_per_page - 1) // items_per_page

print(f"Full pages: {full_pages}, leftover: {leftover}")
print(f"Total pages needed: {total_pages}")   # 16
python
total_files     = 847
processed_files = 312

percent = round(processed_files / total_files * 100, 1)
print(f"Progress: {processed_files}/{total_files} ({percent}%)")

The ceiling division formula (n + d - 1) // d is a standard integer trick for rounding up without converting to float.

Min-max normalisation and percentage change: two patterns that appear constantly in data work:

python
# min-max normalisation: scale a value into the 0.0 to 1.0 range
value   = 75
minimum = 0
maximum = 100

normalised = (value - minimum) / (maximum - minimum)
print(f"Normalised: {normalised:.2f}")   # 0.75

# percentage change between two measurements
before = 1_200
after  = 1_380

change = (after - before) / before * 100
print(f"Change: {change:.1f}%")          # 15.0%

Both patterns reduce to a ratio: a value relative to a reference range or a reference magnitude. The precision of float is sufficient for most analytical work; the accumulated error only matters when the computation chains dozens of operations or involves values that differ by many orders of magnitude.