Python operators practice — a unit converter, the coffee card with IGIC in cents and a calculator with negative numbers (and the cent I kept losing)

Last term the uni cafeteria brought out a 10-coffee card with a student discount. I bought one and, since I was right in the middle of operators week in FP1, I wanted to work out with Python how much each coffee cost me. I did the sum with decimals, divided it by 10, multiplied by 10 to check… and it did not give what I had paid. Then I did it in cents and found out the problem was not Python: 1091 cents cannot be split exactly between 10 coffees. One is left over. The lesson it left me is the one you will see in today’s three programs: before you operate, think about what kind of number you have and what kind you want to get.

This is the practice for the Python operators theory. You will write three complete, intermediate-level programs that use everything you saw there: a unit converter (precedence and parentheses), a discount calculator with the coffee card, the Canary Islands IGIC tax and money in cents (//, % and round), and a full calculator that shows the seven arithmetic operations with positive and negative numbers (quotient, remainder and comparisons). The data look like real data: prices with cents, a tax that does not come out round and a negative number that makes // behave in a way you do not expect.

Before you start

You should be clear about:

  • / always returns a float and // returns the floor quotient, rounded down.
  • % is the remainder of the division, not a percentage, and b * (a // b) + a % b always gives a back.
  • Basic precedence: ** before *, /, // and %, and these before + and -.
  • float values are not compared with ==, and money is better handled in cents.

If any of these sounds shaky, go over the theory before carrying on.

Create a operators_practice folder with one file per program (converter.py, coffee_card.py, calculator.py). The programs do not use input(): the data are written at the top in variables, so when you want to try other values you only have to change that line and run it again. The outputs you will see here are the real ones from a run.

Program 1: unit converter

The same value (100) converted from kilometres to miles and back, from kilos to pounds and back, and from degrees Celsius to Fahrenheit and back. The first two conversions are a plain multiplication by a factor; temperature is the tricky one, because it mixes multiplication, division and addition or subtraction, and there precedence decides whether the result is right or nonsense.

The idea

Each conversion is a formula. Distance and weight are value * factor, and the factors (0.621371, 1.60934…) are constants: they never change, so we store them in capitalised variables at the top, so we do not repeat magic numbers all over the code. For temperature we use the two physics formulas: °F = °C × 9/5 + 32 and °C = (°F − 32) × 5/9. The second one needs parentheses, the first one does not. And to understand why, the program also works out the two badly written versions.

Before coding a formula, think about the order you would do it in by hand. If in that order there is an addition or subtraction before a multiplication, you need parentheses.

The code

# Unit converter: the same value in distance, weight and temperature
value = 100.0            # it used to come from input(); change it to try other values

# Conversion factors: constants, that is why they are in CAPITALS
KM_TO_MILES = 0.621371
MILES_TO_KM = 1.60934
KG_TO_LB = 2.20462
LB_TO_KG = 0.453592

print("=== UNIT CONVERTER ===")

print("--- Distance ---")
print(value, "km →", round(value * KM_TO_MILES, 4), "miles")
print(value, "miles →", round(value * MILES_TO_KM, 4), "km")

print("--- Weight ---")
print(value, "kg →", round(value * KG_TO_LB, 4), "pounds")
print(value, "pounds →", round(value * LB_TO_KG, 4), "kg")

print("--- Temperature ---")
c_to_f = value * 9 / 5 + 32       # * and / before +: no parentheses needed
f_to_c = (value - 32) * 5 / 9     # here they ARE: the subtraction has to go first
print(value, "°C →", round(c_to_f, 2), "°F")
print(value, "°F →", round(f_to_c, 2), "°C")

# The two typical mistakes, really calculated
no_parentheses = value - 32 * 5 / 9     # subtracts 17.78 instead of subtracting 32 first
with_floor_div = value * (9 // 5) + 32  # 9 // 5 is 1: the 0.8 is lost
print("Without parentheses it would be:", round(no_parentheses, 2), "°C")
print("With // it would be:", with_floor_div, "°F")

Output

=== UNIT CONVERTER ===
--- Distance ---
100.0 km → 62.1371 miles
100.0 miles → 160.934 km
--- Weight ---
100.0 kg → 220.462 pounds
100.0 pounds → 45.3592 kg
--- Temperature ---
100.0 °C → 212.0 °F
100.0 °F → 37.78 °C
Without parentheses it would be: 82.22 °C
With // it would be: 132.0 °F

Let’s check by hand: 100 × 0.621371 = 62.1371 miles, and 100 × 1.60934 = 160.934 km. For temperature, 100 × 9 / 5 = 180 and 180 + 32 = 212 °F (water boils at 212 °F, it adds up). The other way, (100 − 32) = 68; 68 × 5 = 340; 340 / 9 = 37.777…, which round(…, 2) leaves at 37.78 °C.

Now the two mistakes, which are not made up: the program really calculates them.

# The two typical mistakes, really calculated
no_parentheses = value - 32 * 5 / 9     # subtracts 17.78 instead of subtracting 32 first
with_floor_div = value * (9 // 5) + 32  # 9 // 5 is 1: the 0.8 is lost
  • Without parentheses, value - 32 * 5 / 9 first does 32 × 5 = 160, then 160 / 9 = 17.78 and finally 100 − 17.78 = 82.22 °C. Not even close: 100 °F is a very hot day, but not 82 °C.
  • With //, 9 // 5 is 1 (the 0.8 is lost), so the formula becomes 100 × 1 + 32 = 132.0 °F instead of 212.

Look at the two trees: each node shows which operation is done and what comes out, and they are solved from the bottom up.

Two ways of writing the °F to °C conversionTwo expression trees with value = 100. With parentheses, first 100 – 32 = 68, then 68 * 5 = 340 and 340 / 9 = 37.78. Without parentheses, first 32 * 5 = 160, then 160 / 9 = 17.78 and finally 100 – 17.78 = 82.22.With parentheses (right)Without parentheses (wrong)(value – 32) * 5 / 9value – 32 * 5 / 9/37.78*340.09–68.0510032–82.22100/17.78*1609325result: 37.78 °Cresult: 82.22 °C
A tree is solved from the bottom up: the parentheses decide which branch is worked out first.

Questions you might ask

Why does value * 9 / 5 + 32 not need parentheses? Because the natural order is already the right one: * and / go before + and, between themselves, from left to right. Python does (100 × 9) / 5 = 180 and then adds 32. Adding parentheses, (value * 9 / 5) + 32, changes nothing, but it is not wrong if it helps you read it.

Why do the results come out as 100.0 and not 100? Because value is 100.0, a float, and any operation with a float gives a float. In the old version of this program the value came from float(input(...)); here I kept the same type so the behaviour is the same.

Why round(…, 4) for distance and round(…, 2) for temperature? Because of what you need to show. A factor like 0.621371 gives lots of decimals and with 4 the conversion is clear; a temperature with 2 decimals is more than enough. round changes the value that is printed, but the variables c_to_f and f_to_c keep all their decimals.

Why 9 / 5 and not 1.8? It would work just the same (value * 1.8 + 32), but 9 / 5 looks like the textbook formula and you can check it at a glance. What matters is that it is / and not //.

Translating a formula “as it reads” without thinking about precedence: value - 32 * 5 / 9. Python does not read from left to right, it reads by steps. When in doubt, parentheses.

Try it yourself

  • Change value to -40.0 and check the fun fact: −40 °C is exactly −40 °F.
  • Add the conversion from metres to feet (1 m = 3.28084 feet) with its constant and its two lines.

Program 2: discount calculator with the coffee card

The 10-coffee card costs 12.00 €. Students get a 15 % discount, and on the discounted price you add IGIC, the Canary Islands sales tax (7 % general rate; on the Spanish mainland it would be VAT). The program has to show the breakdown in euros with two decimal places and also how much each coffee costs. What makes it tricky: the tax does not come out round (71.4 cents) and the total does not split exactly by 10.

The idea

The whole calculation is done in cents and with integers, as in the theory: 12.00 € is 1200 cents. That way every intermediate result is exact. The steps, in order:

  1. Discount = price × 15 // 100. Multiply first and divide after, so nothing is lost on the way.
  2. Discounted price = price − discount.
  3. IGIC = 7 % of the already discounted price, rounded to the cent with round.
  4. Total = discounted price + IGIC.
  5. To show it in euros: the euros are cents // 100 and the loose cents cents % 100.

At the end, for comparison, the same sum is done with float values.

Before coding, think about which step loses precision. With whole cents, only one: rounding the tax. With decimals, all of them.

The code

# 10-coffee card: student discount and IGIC, all in cents
product = "10-coffee card"
price_cents = 1200       # 12.00 € in cents: integers are exact
discount_pct = 15        # student discount (%)
IGIC_PCT = 7             # Canary Islands general IGIC (VAT on the mainland)

# 1. Discount: multiply first, divide after (// to stay with integers)
discount_cents = price_cents * discount_pct // 100
discounted = price_cents - discount_cents

# 2. Tax on the ALREADY discounted price; round() to the nearest cent
igic_cents = round(discounted * IGIC_PCT / 100)
total_cents = discounted + igic_cents

# 3. Show euros: // gives the euros and % the cents (:02d adds the zero: 05)
print("---", product, "---")
print(f"Price: {price_cents // 100}.{price_cents % 100:02d} €")
print(f"Discount ({discount_pct} %): -{discount_cents // 100}.{discount_cents % 100:02d} €")
print(f"Discounted: {discounted // 100}.{discounted % 100:02d} €")
print(f"IGIC ({IGIC_PCT} %): +{igic_cents // 100}.{igic_cents % 100:02d} €")
print(f"Total: {total_cents // 100}.{total_cents % 100:02d} €")

# 4. How much is each coffee? 1091 does not split exactly by 10
per_coffee = total_cents // 10
left_over = total_cents % 10
print("Each coffee:", per_coffee, "cents, left over:", left_over)
print("Does it add up?", per_coffee * 10 + left_over == total_cents)

# 5. The same sum with floats, to compare
with_floats = 12.00 * (1 - 0.15) * (1 + 0.07)
print("With floats:", with_floats)
print("Same as in cents?", with_floats == total_cents / 100)

Output

--- 10-coffee card ---
Price: 12.00 €
Discount (15 %): -1.80 €
Discounted: 10.20 €
IGIC (7 %): +0.71 €
Total: 10.91 €
Each coffee: 109 cents, left over: 1
Does it add up? True
With floats: 10.914
Same as in cents? False

Let’s check by hand:

StepSumResult (cents)
Discount1200 × 15 = 18000; 18000 // 100180
Discounted1200 − 1801020
IGIC1020 × 7 = 7140; 7140 / 100 = 71.4; round(71.4)71
Total1020 + 711091
Per coffee1091 // 10109
Left over1091 % 101

The drawing shows the whole route: from 1200 to 1091 with integers, and how // and % get both the euros and cents and the price per coffee out of 1091.

The coffee card worked out in cents1200 cents minus a 180 discount (1200 * 15 // 100) is 1020; plus 71 of IGIC (round(71.4)) is 1091. Below, 1091 // 100 = 10 euros and 1091 % 100 = 91 cents make 10.91 €; 1091 // 10 = 109 cents per coffee and 1091 % 10 = 1 cent left over.The 10-coffee card, all in centsprice1200discounted1020total1091−1801200*15//100+71round(71.4)1091 // 100 = 10euros1091 % 100 = 91cents→ 10.91 €1091 // 10 = 109per coffee1091 % 10 = 1left over109 * 10 + 1 == 1091
Every step is an exact integer; // and % turn the total into euros and cents, or into the price per coffee.

The part that formats the money deserves a zoom:

print(f"Price: {price_cents // 100}.{price_cents % 100:02d} €")
print(f"Discount ({discount_pct} %): -{discount_cents // 100}.{discount_cents % 100:02d} €")
print(f"Discounted: {discounted // 100}.{discounted % 100:02d} €")
print(f"IGIC ({IGIC_PCT} %): +{igic_cents // 100}.{igic_cents % 100:02d} €")
print(f"Total: {total_cents // 100}.{total_cents % 100:02d} €")

On each line, x // 100 are the whole euros and x % 100 what is left, the cents. The :02d inside the braces pads with a zero on the left until there are two digits: without it, 1200 cents would be shown as 12.0 € instead of 12.00 €, and 5 cents as 0.5 instead of 0.05.

And the ending, the one from my story: 1091 // 10 are 109 cents per coffee and 1 is left over. If you charge 109 for each coffee, you collect 1090: one cent is missing. That is why the program checks per_coffee * 10 + left_over == total_cents (109 × 10 + 1 = 1091): the sum only adds up if you do not forget the remainder.

Questions you might ask

Why price_cents * discount_pct // 100 and not price_cents * (discount_pct // 100)? Because 15 // 100 is 0: the discount would be 0. And even if you used /, 15 / 100 is 0.15, a float that brings decimals back in. Multiplying first (1200 × 15 = 18000) and dividing afterwards keeps everything in integers and is exact. As * and // are on the same step, Python already does it from left to right without parentheses.

Why do I use / and round for the IGIC, and not //? Because 71.4 cents are rounded to the nearest cent, which is 71, but 71.6 would round to 72. // always goes down, so with 71.6 it would give 71 and you would charge too little. round is what a real receipt asks for.

Why is the float result 10.914 and not 10.91? Because the decimal version never rounds the tax to the cent: it drags the 0.4 all the way to the end. That is why with_floats == total_cents / 100 gives False: it is not only the binary decimals problem, it is that they are different sums. The one in cents is the one that matches what you are charged.

Why is the tax applied to the discounted price? Because that is how a receipt works: first the discount, then the tax on what you actually pay. If you worked it out on 1200, the IGIC would be 84 cents instead of 71.

Working out the percentage as price * discount_pct // 100 with the price in euros as a float (12.0 × 15 // 100 = 1.0): // with a float also rounds down, and you lose the cents. If you use //, use it with integers.

The general IGIC rate in the Canary Islands is 7 % (on the mainland, hospitality has 10 % VAT). Change IGIC_PCT and you have the version for any tax.

Try it yourself

  • Change the discount to 20 % and the price to 1250 cents. Before running it, work out the total by hand and check whether anything is left over when splitting by 10.
  • Add a line that shows the total savings in euros (original price with IGIC minus total paid), with the same // and % format.

Program 3: full calculator

The old version of this program had a menu to choose the operation, but that needs conditionals, which we have not covered yet. This calculator does all the operations at once with the same pair of numbers a and b, and then checks three things with comparison and logical operators: whether a is divisible by b, whether a is even and whether the quotient-and-remainder equality holds. The interesting part is running it with three different pairs, one of them negative.

The idea

Each line is an f-string that shows the operation and its result. For the checks:

  • Divisible: a % b == 0 (if nothing is left over, it is divisible).
  • Even: a % 2 == 0.
  • The division equality: b * (a // b) + a % b == a. If // and % are consistent, this always gives True, with negative numbers too.
  • A protected check: b != 0 and a / b > 3. The b != 0 goes first so that, if b were ever 0, and stops before dividing.

Before running it with −17, write down on paper what you think -17 // 5 and -17 % 5 will give. Then compare.

The code

# Full calculator: every operation with the same pair of numbers
a = 17
b = 5

print(f"=== CALCULATOR: a = {a}, b = {b} ===")
print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
print(f"{a} / {b} = {a / b}")          # true division
print(f"{a} // {b} = {a // b}")        # floor division (rounds down)
print(f"{a} % {b} = {a % b}")          # remainder, with the sign of b
print(f"{a} ** {b} = {a ** b}")

print("--- Checks ---")
print("Is a divisible by b?", a % b == 0)
print("Is a even?", a % 2 == 0)
print("b * (a // b) + a % b == a?", b * (a // b) + a % b == a)   # always True
print("b not equal to 0 and a / b greater than 3?", b != 0 and a / b > 3)

Output

With a = 17 and b = 5:

=== CALCULATOR: a = 17, b = 5 ===
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3.4
17 // 5 = 3
17 % 5 = 2
17 ** 5 = 1419857
--- Checks ---
Is a divisible by b? False
Is a even? False
b * (a // b) + a % b == a? True
b not equal to 0 and a / b greater than 3? True

Check: 17 // 5 = 3 and 17 % 5 = 2, because 5 × 3 + 2 = 17. 17 ** 5 = 1419857 (17 × 17 = 289; × 17 = 4913; × 17 = 83521; × 17 = 1419857). 17 is not divisible by 5 (2 is left over) and it is not even (17 % 2 = 1). And 17 / 5 = 3.4 is greater than 3.

With a = 20 and b = 4 (changing only those two lines):

=== CALCULATOR: a = 20, b = 4 ===
20 + 4 = 24
20 - 4 = 16
20 * 4 = 80
20 / 4 = 5.0
20 // 4 = 5
20 % 4 = 0
20 ** 4 = 160000
--- Checks ---
Is a divisible by b? True
Is a even? True
b * (a // b) + a % b == a? True
b not equal to 0 and a / b greater than 3? True

Here the division is exact and even so 20 / 4 gives 5.0, a float, while 20 // 4 gives 5. The remainder is 0, so 20 is divisible by 4.

And with a = -17 and b = 5:

=== CALCULATOR: a = -17, b = 5 ===
-17 + 5 = -12
-17 - 5 = -22
-17 * 5 = -85
-17 / 5 = -3.4
-17 // 5 = -4
-17 % 5 = 3
-17 ** 5 = -1419857
--- Checks ---
Is a divisible by b? False
Is a even? False
b * (a // b) + a % b == a? True
b not equal to 0 and a / b greater than 3? False

This is the output that fools you. -17 / 5 is −3.4, but -17 // 5 is −4, not −3: // rounds down, and below −3.4 the next integer is −4. And the remainder is 3, not −2: it is what you have to add to 5 × (−4) = −20 to get back to −17. The equality still gives True, and the last check gives False because −3.4 is not greater than 3. The drawing shows it on the number line:

Floor division and remainder with a negative numberOn the number line, 3.4 goes down to 3 and -3.4 goes down to -4, because // always rounds to the left. That is why the remainder of 17 by 5 is 2 and that of -17 by 5 is 3: 5 * 3 + 2 = 17 and 5 * (-4) + 3 = -17.a = 17 and a = -17, with b = 5-5-4-3-2-10123453.4-3.4-17 / 5 = -3.4 → -17 // 5 = -417 / 5 = 3.4 → 17 // 5 = 317 // 5 = 3remainder: 17 – 5 * 3 = 2-17 // 5 = -4remainder: -17 – 5 * (-4) = 3b * (a // b) + a % b == a in both cases
// always goes down to the left; the remainder is what you add to get back to the original number.

Questions you might ask

Why does -17 // 5 not give −3 like the calculator on my phone? Because the calculator (and int()) cut off the decimals, which means going towards zero, and // rounds down, which means going towards minus infinity. With positive numbers they match; with negative numbers they do not. If you need to cut, use int(a / b).

Why does the line say -17 ** 5 = -1419857 if in the theory -2 ** 2 gave −4? Good catch. That line is text that shows a and b: the real calculation is a ** b with a = -17, which is (−17)⁵. As the exponent is odd, the result is negative and matches −(17⁵). If b were 2, a ** b would give 289, but typing -17 ** 2 by hand would give −289.

Why b != 0 and a / b > 3 and not the other way round? Because of short-circuiting. If b were 0, b != 0 would be False and and would never get to the division. The other way round, a / b would be worked out first and the program would stop with ZeroDivisionError.

What is the point of the equality b * (a // b) + a % b == a if it always gives True? To understand // and % as a pair: the quotient and the remainder have to rebuild the number. If one of your “how many fit and how many are left” calculations ever breaks it, you have a bug.

Using % to check whether a negative number is odd with n % 2 == 1. It works (-7 % 2 gives 1 in Python), but in other languages such as C or Java it gives −1. The version that always works is n % 2 != 0.

Try it yourself

  • Run it with a = 17 and b = -5. Predict the sign of a % b first (hint: the remainder takes the sign of the divisor).
  • Change b to 0 and see which line blows up first. Then think about how you would use and to protect the divisions (in the next topic you will do it with if).

Common mistakes in this practice

  • Forgetting the parentheses in a formula. value - 32 * 5 / 9 is not the conversion to Celsius. Write the formula in the order you would do it by hand.
  • Using // where you need /. 9 // 5 is 1 and wipes out the 0.8.
  • Doing money with float and comparing with ==. Use cents and, to show them, // 100 and % 100 with :02d.
  • Dividing the percentage before multiplying. 15 // 100 is 0; price * 15 // 100 is the discount.
  • Forgetting the remainder when sharing out. 1091 between 10 is 109 with 1 left over: if you do not count it, you lose a cent.
  • Expecting // to cut decimals with negative numbers. -17 // 5 is −4; to cut, int(-17 / 5).

Watch it step by step in Python Tutor

Copy this summary of program 2 into Python Tutor, choose Python 3 and step through it line by line:

total_cents = 1091

euros = total_cents // 100            # 10
cents = total_cents % 100             # 91
text = f"{euros}.{cents:02d} €"

per_coffee = total_cents // 10        # 109
left_over = total_cents % 10          # 1
adds_up = per_coffee * 10 + left_over == total_cents

with_floats = 12.00 * 0.85 * 1.07
same = with_floats == total_cents / 100

print(text, per_coffee, left_over, adds_up, same)

Output:

10.91 € 109 1 True False

What to look at step by step:

  1. euros appears with 10 and cents with 91: they are the quotient and the remainder of 1091 divided by 100, and both are int.
  2. In text you will see '10.91 €': the :02d changes nothing here, but if you change total_cents to 1005 you will see '10.05 €' instead of '10.5 €'.
  3. per_coffee is 109 and left_over is 1; adds_up is True because 109 × 10 + 1 = 1091.
  4. with_floats appears as 10.914: the decimal sum drags along the 0.4 from the tax. That is why same is False.

Summary

  • Formulas are written with precedence in mind: if an addition or subtraction goes first, it needs parentheses.
  • / for averages, percentages and conversions; // and % for sharing out and for splitting euros and cents.
  • Money in whole cents; round only in the step that needs it (the tax) and :02d to show two digits.
  • b * (a // b) + a % b == a always holds: it is the way to check a share-out.
  • With negative numbers, // rounds down (−17 // 5 = −4) and the remainder takes the sign of the divisor.
  • The protecting check goes first in an and (b != 0 and a / b > 3).

Checklist: have you practised it?

  • Have you run the three programs and checked the outputs?
  • Could you explain why value - 32 * 5 / 9 gives 82.22?
  • Can you turn 1091 cents into 10.91 € without looking at the code?
  • Do you know what -17 // 5 and -17 % 5 give, and why?
  • Have you tried the “Try it yourself” tasks without looking at the theory?

If you answered yes to almost everything, you are ready for the Python operators exercises, where I no longer give you the code: the gym fee, seconds to hours and the till change coin by coin. And after that, conditionals, where these comparisons will decide which path your program takes. See you there!

Next step: exercisesPython operators exercises — the gym fee, seconds to hours and the till change coin by coin (and an or that always said yes)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *