Skip to content

Numbers and arithmetic

docs.scrimba.com

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 integers have no overflow (the wrap-around or error you hit in languages with fixed-size integers): they grow as large as memory allows. Floats are the usual double-precision decimals, fine for most work but never exact for money. The arithmetic operators follow mathematical definitions rather than the C convention many languages copy: // is floor division (it rounds toward negative infinity) and % carries the sign of the divisor, not the dividend. Both choices keep the same result whether your inputs are positive or negative, which is the behaviour you want for cycling and wrapping.

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.

Each operator is wired to a dunder method (a method named with double underscores that Python calls behind the scenes): + to __add__, // to __floordiv__, % to __mod__, ** to __pow__, and so on. The practical payoff is that your own classes can define those methods and then work with + or * directly, the same way a built-in number does. Mixing an int and a float in one operation always gives a float, and / returns a float no matter what you feed it.

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
JunoThe operators+, -, *, / behave like paper maths. The three extras are worth learning early: // divides down to a whole number, % hands you the remainder, and ** raises to a power. I reach for % more than I ever expected to.
JunoThe operators The four basics work as expected, with one rule to bank: / always returns a float. The extras are // for integer division, % for the remainder, and ** for exponentiation. Each one saves you writing a helper.
JunoThe operators Every operator routes to a dunder, so a class that defines __add__ or __mul__ plugs straight into + and *. Mixed int and float always widens to float, and / returns a float regardless of what you give it.

Division: / vs //

/ always gives you the exact decimal result, even if the answer is a whole number. // drops the fractional part and floors toward negative infinity. For positive numbers that means cutting the decimal, but for negatives it steps one further from zero:

/ 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 and always returns a float. // is floor division: it rounds the true quotient toward negative infinity rather than chopping toward zero, which is what many other languages do. The reason that matters in practice: Python's // and % satisfy a == (a // b) * b + (a % b) for every integer input, negatives included. Truncating division breaks that identity for negatives, so any cycling or wrapping logic you write stays correct here without special-casing the sign.

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)

You will mostly use // with positive numbers. Keep the negative behavior in mind for when they show up.

JunoDivision: / vs /// always hands back a decimal, even when it divides evenly: 4 / 2 is 2.0. // drops the fractional part, but watch the negatives: -7 // 2 is -4, not -3, because it floors instead of chopping. Caught me out the first time I did it.

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

JunoDivision: / vs /// always returns a float, even 4 / 2 gives 2.0. // floors rather than truncating, so -7 // 2 is -4. For positive numbers the two look the same, the gap only opens on negatives.

// computes floor(a / b), not truncation. It works on floats too: 7.5 // 2 is 3.0, the floored quotient handed back as a float rather than an int.

JunoDivision: / vs //// floors the quotient, so it satisfies a == (a // b) * b + (a % b) on every input, negatives and all. That is the property to lean on for cycling logic. Note that a float operand makes // return a float, like 7.5 // 2 giving 3.0.

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 most common 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. Many other languages take the sign of the dividend instead, so they would give -1 where Python gives 2 for -7 % 3. Python's answer falls out of defining modulo as a - (a // b) * b with // flooring toward negative infinity. The payoff is that value % limit stays inside 0 to limit - 1 even when value goes negative, so an index that wraps around the end of a list lands in range without a guard clause.

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

6 % 2     # 0  (even)
7 % 2     # 1  (odd)
JunoThe remainder operator %% is the leftover after integer division. 10 % 3 is 1 because 3 fits into 10 three times with 1 to spare. You will mostly meet it as the even/odd check: n % 2 is 0 for even, 1 for odd.
JunoThe remainder operator %% is modulo, and it goes well past even/odd: any time you need to keep a value inside a range, value % limit wraps it to 0 through limit - 1. Counters, group assignment, repeating cycles, all the same shape.
JunoThe remainder operator %% follows the sign of the divisor, so -7 % 3 is 2, not -1. That is what lets value % limit stay in range with negative inputs, no guard clause for a wrapping index.

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:

** raises to a power, and the fractional-power form is handy: n ** 0.5 is a square root without importing anything. Two int operands give an int, any float operand gives a float. One thing to know before you reach for it on large or precise work: 9 ** 0.5 goes through float maths, so it can land on 2.9999999999999996 for some inputs. When exactness matters, math.isqrt() for integer roots and math.pow() are the tools to weigh, covered in the Modules chapter.

python
2 ** 10    # 1024
3 ** 3     # 27
9 ** 0.5   # 3.0  (square root: raise to the power of 0.5)
JunoExponentiation **** raises a number to a power, so 2 ** 10 is 1024. A fractional power gives you a root: 9 ** 0.5 is 3.0. The one trap is the symbol, use **, not ^, which does something unrelated in Python.
JunoExponentiation **** is exponentiation, and it takes floats, so 9 ** 0.5 gives you a square root with no separate function call. Reach for the fractional-power form when you want a quick root inline.
JunoExponentiation ** Two ints give an int, any float gives a float. The catch with the ** 0.5 root trick is that it runs through float maths and can drift, so reach for math.isqrt() or math.pow() when exactness counts.

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 isn't clear at a glance:

Precedence from highest to lowest among arithmetic operators: **, then unary - (a minus sign in front of a single value), then * / // % (left-to-right when they share a level), then + -. The one trap that bites in real code: -2 ** 2 is -(2 ** 2), which is -4, because ** binds tighter than the leading minus. If you ever square a value that might be negative, parenthesise it as (-2) ** 2 or the sign will be wrong without any error to warn you.

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
JunoOperator precedence Same order as paper maths: powers first, then multiply and divide, then add and subtract. When the order isn't clear at a glance, add parentheses. They cost nothing and they spell out exactly what you meant.
JunoOperator precedence Standard PEMDAS order, with one snag: /, // and % all sit at the same level and run left to right when mixed. That left-to-right rule is the part people misread, so parenthesise whenever the grouping isn't clear.
JunoOperator precedence The trap is unary minus binding looser than **, so -2 ** 2 is -4. Square anything that might be negative as (-2) ** 2, or the sign flips with no error to flag it.

How int and float interact

Python has a consistent rule: / always returns a decimal, even 4 / 2 gives 2.0. Any operation mixing an integer and a decimal also 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).

The rule is fixed and worth internalising: int widens to float whenever the two mix, / always returns a float, and // returns an int only when both operands are int. The bug this prevents is a float sneaking into something that needs a whole number, like a list index or a dictionary key, where 2.0 and 2 are not interchangeable. When a calculation might produce a float and you need an int, convert with int() at that point rather than hoping the division came out whole.

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)
JunoHow int and float interact/ always gives a decimal, so 4 / 2 is 2.0, not 2. Mix a whole number with a decimal anywhere and you get a decimal out. When you need a whole number, reach for // or wrap it in int().
JunoHow int and float interact/ is always float, // and % on two ints stay int, and any int mixed with a float comes out float. The one that catches people is 4 / 2 being 2.0 when you wanted an index.
JunoHow int and float interact A stray float from / breaks anything that needs an exact int, like an index or a dict key where 2.0 isn't 2. Convert with int() at that boundary instead of trusting the division to come out whole.

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.

Floats give you roughly 15 to 16 significant decimal digits of precision. The imprecision surfaces because some fractions cannot be stored exactly in binary, so 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.

A float stores numbers in binary (base 2), and any fraction whose denominator isn't a power of two (like 1/10) has no exact binary form, the same way 1/3 has no exact decimal form. That is the whole reason 0.1 + 0.2 lands on 0.30000000000000004. The error per operation is tiny, but it accumulates across a long chain of arithmetic, so two ways of computing the same total can disagree in the last few digits.

The rule that prevents the bug: never use a float for money, and never test floats for exact equality with ==. Reach for decimal.Decimal when you need exact base-10 arithmetic (billing, accounting), or fractions.Fraction when you need exact ratios with no rounding at all. Both ship in Python's standard library, covered in the Modules chapter.

JunoFloat precision Computers store decimals in binary, and some values like 0.1 have no exact binary form, so 0.1 + 0.2 comes out as 0.30000000000000004. It isn't a Python bug, every language does this. For everyday output, round() or :.2f keeps it tidy.
JunoFloat precision Binary can't hold every decimal exactly, so 0.1 + 0.2 drifts to 0.30000000000000004. Fine for most maths, and :.2f or round() tidies the display. When the cents have to add up, switch to decimal.Decimal.
JunoFloat precision The rule that saves you: no floats for money, and never == on floats, since the error accumulates across a chain of operations. Use decimal.Decimal for exact base-10 and fractions.Fraction for exact ratios.

Readable number literals

Python lets you put underscores in number literals to make large numbers more readable. Python ignores them completely; they are there 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 are stripped before Python sees the value, so they have zero effect on it. They work in integers, floats, and based literals alike (0xFF_FF, 0b1010_0001, 1_234.567_890), which makes them handy for grouping bytes in a hex constant, not only thousands in a decimal one. The only restrictions: an underscore cannot sit at the start, at the end, or next to a decimal point or exponent marker.

python
population = 8_100_000_000
distance_km = 384_400
pi_approx = 3.141_592_653
JunoReadable number literals Drop underscores into a long number to make it readable: 8_100_000_000 is the same value as 8100000000. Python ignores them entirely, they're there for your eyes, not the interpreter.
JunoReadable number literals Underscores are valid anywhere inside a numeric literal and vanish at parse time, so the value is unchanged. Good for thousands separators in constants, and equally useful for grouping digits in binary or hex.
JunoReadable number literals Underscores are cosmetic across integers, floats and based literals, so 0xFF_FF and 0b1010_0001 read cleaner. The only rules: not at the start, the end, or beside a decimal point or exponent marker.

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() returns the magnitude of a number, with the return type matching the input: an int in gives an int out, a float gives a float. The everyday use is comparing how far two values sit from a target without caring which side they fall on, for example abs(measured - expected) < tolerance as a tolerance check, which reads more clearly than two comparisons against a signed range.

python
abs(-5)     # 5
abs(3.7)    # 3.7
abs(-0.5)   # 0.5
Junoabs()abs() strips the sign and hands back the positive size: abs(-5) is 5, abs(3.7) is 3.7. Reach for it when you care how far a number is from zero, not which direction it leans.
Junoabs()abs() gives the magnitude of an int or float, direction discarded. It's the clean way to write distance and error-margin checks, anywhere the sign isn't part of the question.
Junoabs()abs() keeps the input type, an int stays an int. The pattern worth keeping is abs(measured - expected) < tolerance: one tolerance check instead of two comparisons around a signed range.

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() uses round-half-to-even (banker's rounding): an exact tie goes to the nearest even integer, not always up. That keeps rounding errors from biasing a long column of figures in one direction, which is why it's the default for statistical work. Two practical cautions. First, a float tie often isn't an exact tie once it's in binary, so round(2.675, 2) gives 2.67, not 2.68, because 2.675 can't be stored exactly. Second, when you need predictable currency rounding, do it with decimal.Decimal and an explicit rounding mode rather than relying on round() over floats.

python
round(2.5)   # 2
round(3.5)   # 4
round(4.5)   # 4
Junoround()round(x) goes to the nearest whole number, round(x, n) keeps n decimal places. The surprise: a value sitting exactly halfway rounds to the nearest even number, so round(2.5) is 2, not 3.
Junoround()round() uses banker's rounding, so exact halves go to the nearest even number: round(2.5) is 2, round(4.5) is 4. It cuts bias in aggregate work, but expect it when you assumed halves always round up.
Junoround() Banker's rounding plus binary means round(2.675, 2) is 2.67, since 2.675 isn't an exact tie under the hood. For currency, round with decimal.Decimal and a chosen mode rather than trusting round() over floats.

divmod()

divmod() gives you both the quotient and the remainder in a single call. It returns a pair of values, a tuple (covered in the Tuples and sets chapter), that 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(a, b) does the division once and returns both the floor quotient and the modulo remainder as a pair, so you avoid recomputing the same division through // and then %. It reads cleanest in the places you need both halves anyway: time conversion (divmod(seconds, 60)), pagination, or laying items out across fixed-size rows.

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
Junodivmod()divmod(a, b) hands back the quotient and the remainder together: divmod(10, 3) is (3, 1). You can unpack both at once with quotient, remainder = divmod(10, 3).
Junodivmod()divmod(a, b) is (a // b, a % b) in one step. Reach for it when you need both pieces, like time conversion or pagination, rather than computing the division twice.
Junodivmod() One division, both results: divmod() skips the redundant work of separate // and %. It reads best where you want both halves anyway, like divmod(seconds, 60) or row layout.

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.