Warm-up · Activity 1 of 7
Warm-up from lesson B3.4: the sort is stable. What does this print?
units = ["km", "m", "cm"]
print(sorted(units, key=len))// B3.5 · ~40 min · Beginner
In this build you write a small unit converter library: documented functions for lengths and temperatures, ValueError for bad input, and one convert() with a keyword-only rounding option.
Lesson 5 of 5 in B3 Functions
You will be able to
Warm-up · Activity 1 of 7
units = ["km", "m", "cm"]
print(sorted(units, key=len))Predict · Activity 2 of 7
def check_age(age):
if age < 0:
raise ValueError("age cannot be negative")
return age
print(check_age(30))
print(check_age(-1))
print("done")Practice · Activity 3 of 7
def metres_per(unit):
if unit == "m":
return 1.0
____ ValueError("unknown unit: " + unit)Practice · Activity 4 of 7
Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
print(0.1 + 0.2 == 0.3, round(0.1 + 0.2, 2) == 0.3)Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
This is the pattern the build follows, for durations. seconds_per knows the units and raises ValueError for any other. convert_time validates the value, goes through seconds, and rounds with a keyword-only digits option that defaults to 2. The def line is split over three lines to stay under 79 characters, which PEP 8 allows inside brackets.
main.py
def seconds_per(unit: str) -> int:
"""Return how many seconds one unit is."""
if unit == "s":
return 1
elif unit == "min":
return 60
elif unit == "h":
return 3600
elif unit == "d":
return 86400
raise ValueError("unknown time unit: " + unit)
def convert_time(
value: float, from_unit: str, to_unit: str, *, digits: int = 2
) -> float:
"""Convert a duration between units, rounded to digits places."""
if value < 0:
raise ValueError("a duration cannot be negative")
seconds = value * seconds_per(from_unit)
return round(seconds / seconds_per(to_unit), digits)
print(convert_time(90, "min", "h"))
print(convert_time(1, "d", "min"))
print(convert_time(100, "s", "min"))
print(convert_time(100, "s", "min", digits=4))
print(convert_time.__doc__)
Run it with
python main.pyOutput
1.5
1440.0
1.67
1.6667
Convert a duration between units, rounded to digits places.Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercise 1 of 3
Start the library with lengths. metres_per(unit) returns how many metres one unit is: mm 0.001, cm 0.01, m 1.0, km 1000.0, and raises ValueError("unknown length unit: " + unit) for anything else. convert_length(value, from_unit, to_unit) raises ValueError for a negative value, then converts through metres. The tests compare results rounded to 6 places.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Finish metres_per as an if and elif chain with one return per unit, and put raise ValueError(…) after the chain.
In convert_length, check if value < 0: and raise first.
Then return value * metres_per(from_unit) / metres_per(to_unit).
One way to solve it. Yours can look different and still pass the checks.
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "mm":
return 0.001
elif unit == "cm":
return 0.01
elif unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
if value < 0:
raise ValueError("a length cannot be negative")
return value * metres_per(from_unit) / metres_per(to_unit)
print(convert_length(2.5, "km", "m"))
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "m":
return 1.0
# Add mm, cm and km, and raise ValueError for any other unit.
return 1.0
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
# Raise ValueError for a negative value, then convert via metres.
return value
print(convert_length(2.5, "km", "m"))
test_main.py
from main import convert_length
def test_km_to_m():
"""2.5 km is 2500 m"""
got = convert_length(2.5, "km", "m")
assert round(got, 6) == 2500.0, f"convert_length(2.5, 'km', 'm') returned {got!r}, expected 2500.0"
def test_cm_to_m():
"""250 cm is 2.5 m"""
got = convert_length(250, "cm", "m")
assert round(got, 6) == 2.5, f"convert_length(250, 'cm', 'm') returned {got!r}, expected 2.5"
def test_mm_to_cm():
"""5 mm is 0.5 cm"""
got = convert_length(5, "mm", "cm")
assert round(got, 6) == 0.5, f"convert_length(5, 'mm', 'cm') returned {got!r}, expected 0.5"
def test_same_unit():
"""7 m is 7 m"""
got = convert_length(7, "m", "m")
assert round(got, 6) == 7.0, f"convert_length(7, 'm', 'm') returned {got!r}, expected 7.0"
def test_unknown_unit():
"""An unknown unit raises ValueError"""
try:
got = convert_length(3, "m", "miles")
except ValueError:
return
raise AssertionError(f"convert_length(3, 'm', 'miles') returned {got!r} instead of raising ValueError")
def test_negative_length():
"""A negative length raises ValueError"""
try:
got = convert_length(-1, "m", "cm")
except ValueError:
return
raise AssertionError(f"convert_length(-1, 'm', 'cm') returned {got!r} instead of raising ValueError")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 3
Add temperatures in "C", "F" and "K". Write to_kelvin(value, unit) and from_kelvin(kelvin, unit): K = C + 273.15 and C = (F - 32) * 5 / 9. Both raise ValueError for an unknown unit. convert_temperature(value, from_unit, to_unit) converts to kelvin, raises ValueError below 0 K, and converts from kelvin to the target unit. Step 1 is already in main.py.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
to_kelvin: K returns value, C returns value + 273.15, F returns (value - 32) * 5 / 9 + 273.15, anything else raises.
from_kelvin reverses it: C is kelvin - 273.15, F is (kelvin - 273.15) * 9 / 5 + 32.
convert_temperature: kelvin = to_kelvin(value, from_unit); if kelvin < 0: raise ValueError(…); return from_kelvin(kelvin, to_unit).
One way to solve it. Yours can look different and still pass the checks.
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "mm":
return 0.001
elif unit == "cm":
return 0.01
elif unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
if value < 0:
raise ValueError("a length cannot be negative")
return value * metres_per(from_unit) / metres_per(to_unit)
TEMPERATURE_UNITS = ["C", "F", "K"]
def to_kelvin(value: float, unit: str) -> float:
"""Return the temperature in kelvin."""
if unit == "K":
return value
elif unit == "C":
return value + 273.15
elif unit == "F":
return (value - 32) * 5 / 9 + 273.15
raise ValueError("unknown temperature unit: " + unit)
def from_kelvin(kelvin: float, unit: str) -> float:
"""Return a temperature given in kelvin in another unit."""
if unit == "K":
return kelvin
elif unit == "C":
return kelvin - 273.15
elif unit == "F":
return (kelvin - 273.15) * 9 / 5 + 32
raise ValueError("unknown temperature unit: " + unit)
def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a temperature between C, F and K."""
kelvin = to_kelvin(value, from_unit)
if kelvin < 0:
raise ValueError("below absolute zero")
return from_kelvin(kelvin, to_unit)
print(convert_temperature(100, "C", "F"))
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "mm":
return 0.001
elif unit == "cm":
return 0.01
elif unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
if value < 0:
raise ValueError("a length cannot be negative")
return value * metres_per(from_unit) / metres_per(to_unit)
TEMPERATURE_UNITS = ["C", "F", "K"]
def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a temperature between C, F and K."""
# Step 2: convert to kelvin first, refuse anything below 0 K,
# then convert from kelvin to the target unit.
return value
print(convert_temperature(100, "C", "F"))
test_main.py
from main import convert_temperature
def test_c_to_f():
"""100 C is 212 F"""
got = convert_temperature(100, "C", "F")
assert round(got, 6) == 212.0, f"convert_temperature(100, 'C', 'F') returned {got!r}, expected 212.0"
def test_f_to_c():
"""32 F is 0 C"""
got = convert_temperature(32, "F", "C")
assert round(got, 6) == 0.0, f"convert_temperature(32, 'F', 'C') returned {got!r}, expected 0.0"
def test_kelvin():
"""0 C is 273.15 K, and 0 K is -273.15 C"""
got = convert_temperature(0, "C", "K"), convert_temperature(0, "K", "C")
assert (round(got[0], 6), round(got[1], 6)) == (273.15, -273.15), f"convert_temperature(0, 'C', 'K') and convert_temperature(0, 'K', 'C') returned {got!r}"
def test_minus_forty():
"""-40 C is -40 F"""
got = convert_temperature(-40, "C", "F")
assert round(got, 6) == -40.0, f"convert_temperature(-40, 'C', 'F') returned {got!r}, expected -40.0"
def test_below_absolute_zero():
"""-300 C and -1 K raise ValueError"""
for value, unit in [(-300, "C"), (-1, "K")]:
try:
got = convert_temperature(value, unit, "F")
except ValueError:
continue
raise AssertionError(f"convert_temperature({value}, {unit!r}, 'F') returned {got!r} instead of raising ValueError")
def test_unknown_unit():
"""An unknown unit raises ValueError"""
try:
got = convert_temperature(20, "C", "X")
except ValueError:
return
raise AssertionError(f"convert_temperature(20, 'C', 'X') returned {got!r} instead of raising ValueError")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 3 of 3
Finish the library with convert(value, from_unit, to_unit, *, digits=None). If both units are in LENGTH_UNITS, use convert_length; if both are in TEMPERATURE_UNITS, use convert_temperature; otherwise raise ValueError("cannot convert " + from_unit + " to " + to_unit). When digits is not None, round the result. Annotate digits as int | None, and give convert a docstring.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
from_unit in LENGTH_UNITS is True when the unit is in that list; combine two such tests with and.
Store the result of convert_length or convert_temperature in a variable, and raise ValueError in the else branch.
Add *, digits: int | None = None to the parameters, then: if digits is not None: result = round(result, digits).
One way to solve it. Yours can look different and still pass the checks.
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "mm":
return 0.001
elif unit == "cm":
return 0.01
elif unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
if value < 0:
raise ValueError("a length cannot be negative")
return value * metres_per(from_unit) / metres_per(to_unit)
TEMPERATURE_UNITS = ["C", "F", "K"]
def to_kelvin(value: float, unit: str) -> float:
"""Return the temperature in kelvin."""
if unit == "K":
return value
elif unit == "C":
return value + 273.15
elif unit == "F":
return (value - 32) * 5 / 9 + 273.15
raise ValueError("unknown temperature unit: " + unit)
def from_kelvin(kelvin: float, unit: str) -> float:
"""Return a temperature given in kelvin in another unit."""
if unit == "K":
return kelvin
elif unit == "C":
return kelvin - 273.15
elif unit == "F":
return (kelvin - 273.15) * 9 / 5 + 32
raise ValueError("unknown temperature unit: " + unit)
def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a temperature between C, F and K."""
kelvin = to_kelvin(value, from_unit)
if kelvin < 0:
raise ValueError("below absolute zero")
return from_kelvin(kelvin, to_unit)
def convert(
value: float, from_unit: str, to_unit: str, *, digits: int | None = None
) -> float:
"""Convert value between two units of the same kind.
Lengths: mm, cm, m, km. Temperatures: C, F, K. With digits,
the result is rounded to that many decimal places.
"""
if from_unit in LENGTH_UNITS and to_unit in LENGTH_UNITS:
result = convert_length(value, from_unit, to_unit)
elif from_unit in TEMPERATURE_UNITS and to_unit in TEMPERATURE_UNITS:
result = convert_temperature(value, from_unit, to_unit)
else:
raise ValueError("cannot convert " + from_unit + " to " + to_unit)
if digits is not None:
result = round(result, digits)
return result
print(convert(1, "km", "m"))
print(convert(100, "C", "F", digits=1))
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
LENGTH_UNITS = ["mm", "cm", "m", "km"]
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "mm":
return 0.001
elif unit == "cm":
return 0.01
elif unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
if value < 0:
raise ValueError("a length cannot be negative")
return value * metres_per(from_unit) / metres_per(to_unit)
TEMPERATURE_UNITS = ["C", "F", "K"]
def to_kelvin(value: float, unit: str) -> float:
"""Return the temperature in kelvin."""
if unit == "K":
return value
elif unit == "C":
return value + 273.15
elif unit == "F":
return (value - 32) * 5 / 9 + 273.15
raise ValueError("unknown temperature unit: " + unit)
def from_kelvin(kelvin: float, unit: str) -> float:
"""Return a temperature given in kelvin in another unit."""
if unit == "K":
return kelvin
elif unit == "C":
return kelvin - 273.15
elif unit == "F":
return (kelvin - 273.15) * 9 / 5 + 32
raise ValueError("unknown temperature unit: " + unit)
def convert_temperature(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a temperature between C, F and K."""
kelvin = to_kelvin(value, from_unit)
if kelvin < 0:
raise ValueError("below absolute zero")
return from_kelvin(kelvin, to_unit)
def convert(value: float, from_unit: str, to_unit: str) -> float:
"""Convert value between two units of the same kind."""
# Step 3: pick convert_length or convert_temperature, refuse mixed
# kinds, and add a keyword-only option digits for rounding.
return value
print(convert(1, "km", "m"))
test_main.py
from main import convert
def test_length():
"""convert(1, "km", "m") is 1000"""
got = convert(1, "km", "m")
assert round(got, 6) == 1000.0, f"convert(1, 'km', 'm') returned {got!r}, expected 1000.0"
def test_temperature():
"""convert(100, "C", "F") is 212"""
got = convert(100, "C", "F")
assert round(got, 6) == 212.0, f"convert(100, 'C', 'F') returned {got!r}, expected 212.0"
def test_digits():
"""digits=1 rounds 123.456 cm to 123.5"""
got = convert(1.23456, "m", "cm", digits=1)
assert got == 123.5, f"convert(1.23456, 'm', 'cm', digits=1) returned {got!r}, expected 123.5"
def test_mixed_kinds():
"""A length cannot become a temperature"""
try:
got = convert(1, "m", "C")
except ValueError:
return
raise AssertionError(f"convert(1, 'm', 'C') returned {got!r} instead of raising ValueError")
def test_digits_keyword_only():
"""digits must be passed by keyword"""
try:
got = convert(1.23456, "m", "cm", 1)
except TypeError:
return
raise AssertionError(f"convert(1.23456, 'm', 'cm', 1) returned {got!r}; digits should be keyword-only")
def test_docstring():
"""convert has a docstring"""
assert convert.__doc__, "convert has no docstring"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pydef metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "m":
return 1.0
elif unit == "km":
return 1000.0
raise ValueError("unknown length unit: " + unit)
def convert_length(value: float, from_unit: str, to_unit: str) -> float:
"""Convert a length from one unit to another."""
return value * metres_per(from_unit) / metres_per(to_unit)
print(convert_length(3, "m", "miles"))
What Python prints
ValueError: unknown length unit: milesWhy, and the fix
This is the validation working as intended: the traceback runs from the call down to the raise line in metres_per, and the last line carries your message. Fix the call, or add the unit to metres_per. A message that names the bad value makes this quick to find.
def convert(value: float, factor: float, *, digits: int = 2) -> float:
"""Multiply value by factor and round the result."""
return round(value * factor, digits)
print(convert(1.23456, 100, 0))
What Python prints
TypeError: convert() takes 2 positional arguments but 3 were givenWhy, and the fix
digits is after the *, so it must be named: convert(1.23456, 100, digits=0). That is the point of keyword-only options: a bare 0 in a call says nothing about what it controls.
def metres_per(unit: str) -> float:
"""Return how many metres one unit is."""
if unit == "m":
return 1.0
raise "unknown length unit: " + unit
print(metres_per("ft"))
What Python prints
TypeError: exceptions must derive from BaseExceptionWhy, and the fix
raise needs an exception, not a plain string. Wrap the message in one: raise ValueError("unknown length unit: " + unit). mypy reports this mistake before the program runs.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.