Skip to content

Python lesson 3 of 9

Python Arithmetic and Numbers

How Python does maths - why division always gives a float, what // and % really answer, powers, int versus float, and why 0.1 + 0.2 is not 0.3.

Published · Every example on this page was run before it was published.

Printing text is how a program talks. Arithmetic is how it works something out before it speaks. A checkout page totals a bill, a timer turns a pile of seconds into a clock reading, a scoreboard adds one more point — all of that is numbers being combined, with a handful of operators you can learn in a single sitting.

Python's arithmetic looks like the sums you did on paper, and most of the time it behaves that way. Two plus three really is five. But a few details are genuinely different, for good reasons rather than out of awkwardness. Division hands you a decimal even when it did not have to. Two extra division operators answer a question plain division cannot. And numbers with a decimal point carry a small permanent inaccuracy that will turn up in your output whether you expect it or not.

This lesson takes the operators in the order you meet them, then spends real time on the three things that catch people out: what / actually gives you, what // and % are for, and why 0.1 + 0.2 refuses to equal 0.3. Nothing here goes beyond the variables, types and printing from the two lessons before it.

The Four Operators You Write Every Day

Addition, subtraction, multiplication and division are written +, -, * and /. Python works the answer out the instant it reads the line, and variables serve just as well as plain numbers:

Python
apples = 12
oranges = 5

print(apples + oranges)
print(apples - oranges)
print(apples * oranges)
print(apples / oranges)
Output
17
7
60
2.4

Multiplication is the one that looks unfamiliar. On paper you would write a small x, or a dot, or nothing at all beside a bracket. Python accepts none of those: the letter x is a perfectly good variable name, so it cannot double as an operator. Multiplication is always a star:

Python
width = 3
height = 4

print(width * height)
print(2 * (width + height))
Output
12
14

That second line is the perimeter of a rectangle, and the star sits before the bracket even though a textbook would leave it out. There is no implied multiplication in Python.

Division Always Hands Back a Decimal

Here is the most surprising rule in Python arithmetic, and the one worth committing to memory today: / always produces a number with a decimal point, even when the division comes out perfectly even.

Python
print(10 / 2)
print(9 / 3)
print(100 / 4)
print(type(10 / 2))
Output
5.0
3.0
25.0
<class 'float'>

Ten divided by two is five, and Python prints 5.0. Not a mistake, not a rounding artefact — a deliberate rule. A number with a decimal point is called a float, short for "floating point", and / is defined to produce one every single time. The .0 tells you what kind of number you are holding, not that something went wrong.

The rule exists so that / is predictable. If division sometimes gave a whole number and sometimes a decimal, you could never be sure what type your result was without knowing the exact values involved.

Where it bites is the moment you print a result for a human. "You have 5.0 messages" reads badly. Once a stray .0 appears in your output, a / upstream is almost always the cause, and the fix is either a different operator or a conversion — both are below.

Whole Groups and What Is Left Over

Plain division is not the only useful answer to "how does this number split up". Picture a hundred eggs going into cartons that hold a dozen. Two answers are true at once here, and neither of them is 8.33. The first is how many cartons you can close and stack: eight. The second is how many eggs are still sitting on the counter with nowhere to go: four.

These are two halves of one question, and Python gives each half its own operator. // is floor division, which reports how many whole groups fit. % is the modulo operator, which reports what is left over:

Python
print(100 // 12)
print(100 % 12)
Output
8
4

The % symbol will look familiar from percentages, and that reading is no help at all here. Python borrowed the symbol for something unrelated: the remainder, the part that did not fit. Feed both operators plain whole numbers and plain whole numbers come back, which is what counting needs when the things being counted cannot be cut in half.

The pair earns its keep as soon as the numbers are too big to hold in your head. A school trip takes a hundred and thirty-seven students, and each coach seats forty-five:

Python
students = 137
seats_per_coach = 45

full_coaches = students // seats_per_coach
still_waiting = students % seats_per_coach

print("Students:", students)
print("Full coaches:", full_coaches)
print("Still waiting:", still_waiting)
print("Back to the total:", full_coaches * seats_per_coach + still_waiting)
Output
Students: 137
Full coaches: 3
Still waiting: 2
Back to the total: 137

That last line is the reason to trust the pair. Multiply the whole groups back up, add the remainder, and you land exactly on the number you started with. The relationship always holds, so it is the quickest way to check you have the operators the right way round.

One more property turns % from a curiosity into a reliable tool: divide by a positive number and the remainder is always smaller than it. Split anything into groups of eight and what is left over can only be 0 through 7, because 8 would be another whole group:

Python
print(15 // 8, 15 % 8)
print(16 // 8, 16 % 8)
print(23 // 8, 23 % 8)
print(7 // 8, 7 % 8)
Output
1 7
2 0
2 7
0 7

A remainder of 0, as on the second line, means the division was exact. And when the total is smaller than the group size, as on the last line, you get zero whole groups and the entire amount left over. Neither case needs special handling — the operators simply tell the truth.

That zero-remainder fact gives you the standard way to ask whether a number divides evenly, which in practice usually means asking whether it is even:

Python
print(10 % 2)
print(11 % 2)
print(2506 % 10)
Output
0
1
6

An even number leaves nothing when divided by two, so number % 2 is 0 for evens and 1 for odds. The third line applies the same idea to digits: % 10 peels off the final digit of a number.

Powers with the ** Operator

Two stars mean "raised to the power of". 2 ** 3 is three twos multiplied together:

Python
print(2 ** 3)
print(5 ** 2)
print(10 ** 6)
print(9 ** 0.5)
print(2 ** 0.5)
Output
8
25
1000000
3.0
1.4142135623730951

10 ** 6 is a million, which is tidier than counting zeros by hand. A fractional power is a root: raising to the power of 0.5 is the square root, so 9 ** 0.5 is three. It comes back as 3.0 rather than 3 because a float went into the calculation, and once a float is involved the answer stays a float. The square root of two on the last line is an endless decimal, and 1.4142135623730951 is as close as Python can store — the first sight of a limit two sections below.

int and float: Two Kinds of Number

Python keeps whole numbers and decimal numbers as two separate types. An int is a whole number with no decimal point: 7, 0, -40. A float is a number that has one: 7.0, 2.5, -0.001. The built-in type() function tells you which one you are holding:

Python
whole = 7
decimal = 7.0

print(whole, decimal)
print(type(whole))
print(type(decimal))
print(whole == decimal)
print(3 + 0.5)
print(type(4 * 2.0))
Output
7 7.0
<class 'int'>
<class 'float'>
True
3.5
<class 'float'>

Two results deserve attention. whole == decimal is True: 7 and 7.0 are different types but the same quantity, and Python compares them by value. Yet type(4 * 2.0) is a float, which shows the rule when the types meet in one sum: mix an int with a float and the answer is a float. Python widens the int rather than chopping the float, because widening loses nothing.

To move between the types deliberately, int() and float() build a new value of the type you name:

Python
print(int(4.9))
print(int(-4.9))
print(round(4.9))
print(round(-4.9))
print(round(2.5))
print(round(3.5))
print(float(7))
print(int("25") + 5)
Output
4
-4
5
-5
2
4
7.0
30

int() does not round. It truncates — it throws away everything after the decimal point and moves toward zero, which is why 4.9 becomes 4 and -4.9 becomes -4. When you want rounding, round() is the function for it, and it gives 5 and -5 instead. Reaching for int() when you meant round() is a quiet way to be wrong by one.

round() has one habit of its own that surprises almost everybody. On an exact half it does not always go upwards; it goes to the even neighbour, which is why round(2.5) is 2 while round(3.5) is 4. That keeps a long column of rounded numbers from creeping upwards overall, and it is worth knowing about before you meet it in a total that is a penny off.

The last line is the conversion you will use most often. Text that looks like a number is not a number, and int("25") builds the int 25 from the text "25" so that arithmetic can happen — the bridge between the outside world, which hands you text, and your sums.

Why 0.1 + 0.2 Is Not 0.3

This one deserves to be shown before it is explained:

Python
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.1 + 0.2 - 0.3)
Output
0.30000000000000004
False
5.551115123125783e-17

That is not a typo, a bug, or something wrong with your machine. It is how floats work, and those same three lines give the same strange answer in JavaScript, Java, C, Ruby and essentially every other mainstream language, because they nearly all store decimal numbers the same way.

The reason, in plain terms: a computer stores numbers in binary, as sums of halves, quarters, eighths and so on, and some decimal fractions have no exact form in that system. One tenth is one of them, in exactly the way one third has no exact form in ordinary decimal notation — writing 0.3333 and adding threes forever never quite reaches a third. So Python stores the closest value to 0.1 that it can, which is a hair off, and adding two hairs-off numbers can produce an error big enough to see. That last line, 5.551115123125783e-17, is shorthand for a number with sixteen zeros after the decimal point before the digits begin: a vanishingly small gap, but not zero, and == does not forgive gaps.

The trouble is that it only shows up sometimes, which is what makes it a trap:

Python
print(0.1 + 0.2 + 0.3)
print(1.1 * 3)
print(2.675 * 100)
Output
0.6000000000000001
3.3000000000000003
267.5

Two of those are visibly off and the third is exact. Since you cannot tell by looking which sums will drift, the practical rule has to apply everywhere: never compare two floats with ==. Ask instead whether they are close enough:

Python
import math

total = 0.1 + 0.2

print(abs(total - 0.3) < 0.000001)
print(math.isclose(total, 0.3))
print(round(total, 2) == 0.3)
Output
True
True
True

The first line subtracts one number from the other, takes the size of the difference with abs() so the sign cannot matter, and checks that it is tiny. The second does the same job properly with math.isclose() from the standard library, which chooses a sensible tolerance for you. The third rounds before comparing, which is often right when the numbers have a natural precision.

A second strategy is stronger, and it is the one to reach for with money: work in whole units. Store paise rather than rupees, or cents rather than dollars, keep the arithmetic in ints where nothing can drift, and split the result only for display:

Python
price_paise = 1075
quantity = 3

total_paise = price_paise * quantity
print("Total in paise:", total_paise)
print("Rupees:", total_paise // 100)
print("Paise:", total_paise % 100)
Output
Total in paise: 3225
Rupees: 32
Paise: 25

Notice // and % reappearing to do the same job as before: how many whole rupees, and what is left over. This arithmetic is exact because ints are exact. Python also ships a decimal module built for exact decimal arithmetic, which is what serious financial code uses — worth knowing the name now and reading about when you need it.

The Order Python Works In

When several operators appear in one expression, Python does not simply work left to right. It follows precedence rules, the same ones you were taught for paper arithmetic: powers first, then multiplication and division, then addition and subtraction, with brackets overriding everything.

Python
print(2 + 3 * 4)
print((2 + 3) * 4)
print(10 - 2 - 3)
print(2 ** 3 ** 2)
print((2 ** 3) ** 2)
Output
14
20
5
512
64

The multiplication on the first line happens before the addition, giving 14 rather than 20. Operators of equal standing do run left to right, so 10 - 2 - 3 is 8 minus 3. Powers are the exception: they group right to left, so 2 ** 3 ** 2 is two raised to the ninth, not eight squared. That is the mathematical convention, and also a good argument for brackets — nobody should have to remember that rule to read your code.

Precedence mistakes are dangerous precisely because nothing goes wrong. The program runs, prints a number, and the number is simply not the one you wanted:

Python
maths = 78
science = 85
english = 91

print((maths + science + english) / 3)
print(maths + science + english / 3)
Output
84.66666666666667
193.33333333333334

The second line divides only the English mark by three, then adds it to the other two. No error, no warning, just an average of 193 for three subjects that are all under 100. The brackets on the first line are not decoration; they are the entire calculation.

Unary minus carries a trap of its own:

Python
print(-3 ** 2)
print((-3) ** 2)
print(-(3 ** 2))
Output
-9
9
-9

** binds more tightly than the minus sign, so -3 ** 2 squares the three and then negates the result. If you meant minus three, squared, the brackets are compulsory. The habit worth forming is to add brackets whenever an expression would make a reader pause, even where Python does not require them: (base * rate) + fee and base * rate + fee give the same answer, and only the first cannot be misread.

Negative Numbers Divide Downwards

// is often described as "divide and throw away the decimal part", and for positive numbers that description works fine. For negative numbers it is wrong, and if you have used another language you may have learned the wrong rule without knowing it:

Python
print(7 // 2)
print(-7 // 2)
print(7 % 2)
print(-7 % 2)
print(-7 / 2)
Output
3
-4
1
1
-3.5

Minus seven divided by two is -3.5. Throwing away the decimal part would give -3, but Python gives -4. That is what floor means in "floor division": round down toward the smaller number, not toward zero. Down from -3.5 is -4.

The remainder follows from that choice, because Python keeps the promise that multiplying back up and adding the remainder returns the original number:

Python
print((-7 // 2) * 2 + (-7 % 2))
print(-13 % 5)
print(13 % -5)
print(int(-7 / 2))
Output
-7
2
-2
-3

-4 times 2 is -8, and a remainder of 1 brings it back to -7. For that identity to hold the remainder had to come out positive, which is why -7 % 2 is 1 and not -1. In general the remainder takes the sign of the right-hand number: -13 % 5 is positive because 5 is, and 13 % -5 is negative because -5 is. That is also the qualifier on the earlier rule about leftovers being small. With a negative number on the right the remainder is negative too, so what always holds is that its size stays under the size of the number you divided by.

The final line shows the alternative when you genuinely want truncation toward zero: divide with / and convert with int(), which gives -3. Both behaviours are available; they answer different questions. In everyday counting your numbers are positive and the distinction never arises — but when it does, guessing will cost you an hour.

Updating a Number You Already Have

Adding to a running total is common enough that Python has shorthand for it. total += 5 means exactly total = total + 5: work the sum out from the current value, then rebind the name to the result. The same shorthand exists for the other operators, and the whole family is called augmented assignment.

Python
score = 10
print(score)

score += 5
print(score)

score -= 3
print(score)

score *= 2
print(score)

score /= 4
print(score)
Output
10
15
12
24
6.0

Follow the value down: 10, add 5 to get 15, subtract 3 to get 12, double it to 24, then divide by 4. That last result is 6.0 rather than 6, because /= is ordinary division underneath and division always produces a float. //= exists too, and would have kept it an int.

A Worked Example

Here is a small complete program that turns a count of study sessions into a readable summary, using multiplication for the total, // and % to split minutes into hours, plain / for an average, and an f-string to keep the decimal under control.

Python
minutes_in_hour = 60
minutes_per_session = 25
sessions = 20

total_minutes = minutes_per_session * sessions
hours = total_minutes // minutes_in_hour
spare_minutes = total_minutes % minutes_in_hour
rebuilt = hours * minutes_in_hour + spare_minutes
average_per_day = total_minutes / 7

print("==============================")
print("       STUDY SUMMARY")
print("==============================")
print("Sessions .........", sessions)
print("Minutes each .....", minutes_per_session)
print("Total minutes ....", total_minutes)
print("------------------------------")
print("Whole hours ......", hours)
print("Spare minutes ....", spare_minutes)
print("Checks back to ...", rebuilt)
print(f"Average per day ... {average_per_day:.1f} minutes")
print("==============================")
Output
==============================
       STUDY SUMMARY
==============================
Sessions ......... 20
Minutes each ..... 25
Total minutes .... 500
------------------------------
Whole hours ...... 8
Spare minutes .... 20
Checks back to ... 500
Average per day ... 71.4 minutes
==============================

The first three lines name the facts the program is built on, so changing the number of sessions means editing one line rather than hunting through the calculations. minutes_in_hour is a fixed 60, written as a name because total_minutes // 60 would tell a reader nothing about why 60.

Then the arithmetic. minutes_per_session * sessions gives 500 total minutes. 500 // 60 is 8, the whole hours, because eight sixties fit into 500 with room to spare. 500 % 60 is 20, the minutes that did not make up a ninth hour. rebuilt is the check from earlier: 8 hours of 60 minutes is 480, plus the 20 spare, is 500 again. average_per_day uses plain / because a daily average genuinely can be fractional.

The printing block is presentation only. The rows of = and - are text inside quotes doing the job of ruled lines on a paper form, the dots pad each label so the numbers line up, and the commas inside print() place a single space between label and value. The last calculated line is the only f-string: 500 / 7 is 71.42857142857143, which would be noise in a summary, so :.1f displays it as 71.4. The stored value is untouched — keep full precision in the number, and choose the precision at the point a human reads it.

Common Mistakes

Using / when you wanted whole groups. This is the most common arithmetic slip, and the give-away is a decimal point turning up where a count belongs:

Python
eggs = 100
per_carton = 12

print(eggs / per_carton)
print(eggs // per_carton)
Output
8.333333333333334
8

The first answer is a correct piece of division and a useless answer to the question that was asked, because a carton is either full or it is not. A decimal point sitting where a plain count belongs is nearly always a missing second slash.

Comparing floats with ==. Whole-number arithmetic is exact, so == is safe there. Float arithmetic is not:

Python
print(1 + 2 == 3)
print(0.1 + 0.2 == 0.3)
print(1 / 3 * 3 == 1)
Output
True
False
True

The middle line is false and the last is true, and there is no pattern you can rely on. Use math.isclose(), round both sides before comparing, or keep the values in whole units.

Reading % as "percent". The operator has nothing to do with percentages, and using it that way produces a number plausible enough to slip through:

Python
correct = 18
questions = 24

print(correct % questions)
print(correct / questions * 100)
Output
18
75.0

18 % 24 is 18, because 24 does not fit into 18 even once, so the whole 18 is left over. A real percentage needs a division and a multiplication, and comes out at 75.

Dividing by zero. Every division operator refuses, because there is no answer to give: Python raises a ZeroDivisionError and stops the program unless you catch it.

Python
students = 0

try:
    print(120 / students)
except ZeroDivisionError as error:
    print("ZeroDivisionError:", error)

print("The program carried on")
Output
ZeroDivisionError: division by zero
The program carried on

This turns up whenever you average something that might be empty. Check for zero before dividing, or catch the error as above.

Expecting a tidy number on screen. Division prints everything it has, which is often more than you meant to show:

Python
bill = 100
people = 3

print(bill / people)
print(round(bill / people, 2))
print(f"Each pays {bill / people:.2f}")
Output
33.333333333333336
33.33
Each pays 33.33

The first line is the honest full answer. round(value, 2) produces a genuinely shorter number, while the f-string specification :.2f formats for display and leaves the underlying value alone. Prefer the f-string when the goal is presentation, and keep the full-precision value for further arithmetic.

Next Steps

Try the Party Planner exercise if you have not already — it hands you a guest list, an appetite and a pizza that arrives cut into eight, then leaves the two // and % lines for you to write.

Of the linked practice problems, two-number-sum puts plain addition to work while you keep track of which values are ints. binary-search is where floor division earns its keep: finding the middle of a range is (low + high) // 2, and it has to be // rather than / because a list position must be a whole number — a float index is an error, not a rounded guess.

Before either, spend a few minutes in the Python playground. Print 10 / 2 and 10 // 2 side by side until the difference is boring rather than surprising. Try % with a few totals and group sizes and confirm for yourself that the leftover is never as large as the group. Then type 0.1 + 0.2 once more, so that when it appears in your own output six months from now you recognise it immediately instead of losing an afternoon to it.

Write it yourself

Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.