Exercise 1: Lists — build, modify, slice¶
Start from the daily mean temperatures [-2.3, -1.1, 0.4, 1.2, -0.8] (°C).
Append a sixth reading,
-3.1.Remove the value
0.4(the sensor flagged it as unreliable).Print the first reading, the last reading, and a slice containing the second and third.
Print how many readings remain.
# Your solution hereExercise 2: Dictionaries — build, look up, remove¶
You are given station records as a list of tuples (code, name, elevation_m):
[("JFJ", "Jungfraujoch", 3571), ("BAS", "Basel-Binningen", 316), ("LUG", "Lugano", 273)]Build a dict mapping each
codeto itselevation_m.Print the elevation for
"JFJ".Look up
"ZRH"with.get, returning"unknown"if it is absent.Delete the entry for
"LUG", then print each remaining code and its elevation using.items().
# Your solution hereExercise 3: A pure function¶
Write a function mean(values) (no type hints) that returns the average of a list of numbers. Test it on [6.1, 7.4, 5.9, 8.2, 6.8], printing the result rounded to two decimals.
# Your solution hereExercise 4: A function with a default argument¶
Write a function is_warm(temp_celsius, threshold=10.0) (no type hints) that returns True when the temperature is at or above the threshold and False otherwise, using if/else. Test it on 6.1 °C with the default threshold, and again on 6.1 °C with a threshold of 5.0 °C passed by keyword.
# Your solution hereExercise 5: Pair two stations with zip¶
Two stations recorded the same five days:
dates = ["Mon", "Tue", "Wed", "Thu", "Fri"]
jfj_celsius = [-2.3, -1.1, 0.4, 1.2, -0.8]
bas_celsius = [6.1, 7.4, 5.9, 8.2, 6.8]Using zip, print one line per day showing the date and the temperature difference
(Basel minus Jungfraujoch), rounded to two decimals.
# Your solution hereExercise 6: Number the readings with enumerate¶
A station wrote one reading per day for a week:
week_celsius = [6.1, 7.4, 5.9, 8.2, 11.3, 9.8, 6.8]Using
enumerate, print one line per reading: the day number and the temperature. Count days from 1, not 0 —enumeratestarts at 0, so the day number is the index plus one.In the same loop, keep track of the warmest reading seen so far and the day it fell on, in two variables defined before the loop. Do not use
max.After the loop, print the warmest reading and its day number.
# Your solution hereExercise 7: while, break, and continue¶
A logger returned six readings, two of which are missing:
mixed_celsius = [6.1, None, 5.9, None, 9.5, 6.8]Using a while loop over the indices:
Skip any missing value with
continue.Stop at the first reading above 9.0 °C with
break, storing it infirst_hot.Print
first_hotafter the loop. Define it before the loop so the code is safe even if no reading exceeds the threshold.
# Your solution here
# Hint: continue jumps straight back to the while line, skipping everything below it in the
# loop body, including an index update at the bottom of the loopExercise 8: Default arguments and returning two values¶
Write a function summarise(values, scale=1.0) that multiplies every value by scale and
returns two things: the total and the mean.
Call it on
[6.1, 7.4, 5.9, 8.2, 6.8]with the default scale, unpacking the result intototal_celsiusandmean_celsius.Call it again with
scale=2.0passed by keyword, and print only the mean.Call it once more and store the result in a single name. Print its
type— what did you get?
# Your solution hereExercise 9: A variable number of arguments¶
Write a function warmest(*temps_celsius) (no type hints) that returns the highest value among
any number of positional arguments, using a loop and a running maximum (not max). Test it on
three individual readings passed directly, and again by unpacking the list
[6.1, 7.4, 5.9, 8.2, 6.8] with *.
# Your solution hereExercise 10: Fix the shared-default bug¶
An assistant wrote this function; it is correct the first time but wrong on later calls:
def collect_above(values, threshold, out=[]):
for v in values:
if v > threshold:
out.append(v)
return outIn one comment, explain the bug, then rewrite it so each call starts fresh. Demonstrate with two separate calls, assigning each result to its own variable.
# Your solution hereGoing deeper (optional)¶
Exercise 11: Document your function with a docstring¶
Take the mean(values) you wrote in Exercise 3 and give it a docstring — a one-line summary in
triple quotes on the first line of the body. State what it returns and the unit it assumes, so
the function carries information the name alone cannot.
Then print mean.__doc__, and call help(mean) to see what a reader of your code would see.
# Your solution hereExercise 12: Tuples — unpacking and immutability¶
A station’s position is stored as a tuple, station_coords = (46.5475, 7.9853).
Unpack it into
latandlonin one line, and print both.Try to change the latitude to
47.0. Catch the resulting error withtry/except TypeErrorand print the message.In a comment, say why a coordinate pair is better stored as a tuple than as a list.
# Your solution hereExercise 13: Means and a filter, with comprehensions¶
Recreate a stations dict where each code maps to a dict containing readings_celsius:
{"JFJ": {"readings_celsius": [-2.3, -1.1, 0.4, 1.2, -0.8]},
"BAS": {"readings_celsius": [6.1, 7.4, 5.9, 8.2, 6.8]},
"LUG": {"readings_celsius": [9.4, 10.2, 8.8, 11.1, 9.9]}}With a dict comprehension, build means = {code: mean_temp}, calling the mean function you
wrote in Exercise 3 rather than summing inline. Then, with a list comprehension, build the list
of codes whose mean is below 0 °C.
# Your solution hereExercise 14: Spot the impure function¶
This function is meant to return the readings without the last one:
def drop_last(readings):
readings.pop()
return readingsCall it on
original = [6.1, 7.4, 5.9], then printoriginal. What happened?Rewrite it as a pure function that leaves the caller’s list untouched, and demonstrate that
originalis unchanged after the call.In one comment, connect this to the mutable-default bug from the lecture.
# Your solution hereExercise 15: Reject impossible temperatures¶
Write to_kelvin(temp_celsius) that raises a ValueError when the input is below absolute zero
(−273.15 °C) and otherwise returns the temperature in kelvin. Give it a docstring and a type hint.
Loop over [20.0, -300.0, 0.0], calling it inside a try/except so that the invalid value is
reported without stopping the loop.
# Your solution hereExercise 16: A year of station data, continued¶
The optional section ends with Exercise 15. Exercises 16 and 17 are the main exercises of this subchapter.
In subchapter 1.1 you fetched this file, worked out its structure, and converted a single record to °C. You did not yet have the tools to handle all of it at once — now you do.
The goal: turn a year of daily maximum temperatures into a monthly summary, and write that
summary out as a new file. The input file has the same three columns as in Exercise 9 of the
previous subchapter: the station code, the day as dd.mm.yy, and the daily maximum temperature
in degrees Fahrenheit. Your monthly summary will have its own columns: month, n_obs
(the number of observations), and mean_temp_celsius.
# Pre-supplied: download the data file and cache it locally.
# You do not need to understand this cell yet — fetching data is covered in the
# reproducible-data-pipelines bonus subchapter.
import pooch
data_file = pooch.retrieve(
url="https://raw.githubusercontent.com/gse-unil/2026_MLEES_book/main/data/part-I/station_iib_daily_max_temp_2022.csv",
known_hash="sha256:034755fb289b4e157e5d029995481786a359eb25a80df62c09ee83d063013144",
fname="station_iib_daily_max_temp_2022.csv",
path=pooch.os_cache("mlees"),
)
print("data cached at:", data_file)data cached at: /home/runner/.cache/mlees/station_iib_daily_max_temp_2022.csv
Step 1. Write a function fahrenheit_to_celsius(temp_fahrenheit) that returns the
temperature in °C. Test it on the freezing point of water (32 °F should give 0.0 °C) and on
212 °F. A one-line docstring is optional here (see Exercise 11).
You wrote this conversion inline in 1.1. Steps 2 and 4 both need it again; with a function, they call the same tested code instead of repeating the arithmetic.
# Your solution here
# Hint: °C = (°F - 32) * 5 / 9
# Hint (optional, see Exercise 11): a docstring goes on the first line of the function body,
# in triple quotesStep 2. Open the file, read all lines, and loop over the records (skipping the header). For each one, extract the month and the temperature in °C. Print the first three as a check.
Sixteen days in this file have an empty temperature field — a real gap in the station’s record,
late October to mid-November. float("") raises ValueError, so your loop has to decide what to
do with those days before it can get to the end of the file. Skip them, and count how many you
skipped. After the loop, print how many records you converted and how many you skipped; the two
should add up to 365.
# Your solution here
# Hint: open it as in 1.1, with Path(data_file).open("r", encoding="utf-8") in a with block
# Hint: lines[1:] skips the header row
# Hint: .strip().split(",") gives you the three fields
# Hint: the month is the middle piece of the day field — split it on "."
# Hint: use a counter that you update with += inside the loop
# Hint: a day with no reading has fields[2] == "" — `continue` moves on to the next lineStep 3. Build a dictionary mapping each month to the number of observations in it.
You do not know in advance which months are present, so the dictionary has to grow as you go. Keep skipping the days with no reading: a day the sensor did not record is not an observation, so October and November should come out short of their full length.
# Your solution here
# Hint: start with an empty dict, counts = {}
# Hint: counts.get(month, 0) returns 0 for a month you have not seen yet
# Hint: counts[month] = counts.get(month, 0) + 1Step 4. Build a second dictionary mapping each month to its mean daily maximum temperature in °C. Print it with each mean rounded to two decimals.
# Your solution here
# Hint: accumulate a running total per month in the same loop as the counts
# Hint: a second loop over the totals turns each total into a mean
# Hint: reuse your function from step 1 — do not repeat the conversion arithmeticStep 5. Find the warmest month and print it with its mean temperature.
Do this with a loop and a comparison, keeping track of the best value seen so far, as you did for
the warmest day in Exercise 6. A dictionary is looked up by key, not by position, so there is no
[0] to start from: start from None instead.
# Your solution here
# Hint: initialise warmest_month = None and warmest_mean = None before the loop
# Hint: if warmest_mean is None, take this month; elif its mean is larger, take it insteadStep 6. Write your monthly summary to _files/monthly_means.csv, with the header
month,n_obs,mean_temp_celsius and one line per month, creating the _files folder first as you
did in 1.1. Then read the file back and print it, to confirm it says what you meant it to say.
# Your solution here
# Hint: out_path = Path("_files") / "monthly_means.csv" builds the path, and
# out_path.parent.mkdir(exist_ok=True) creates the _files folder if it is missing
# Hint: open with mode "w" inside a with block, and write the header first
# Hint: build each line with an f-string, and remember the "\n"
# Hint: f"{value:.2f}" keeps the file tidyStep 7 (optional). This step uses try/except from the going-deeper box in the lecture. Real files contain junk, and you have already met one kind of it — the empty
temperature field. Suppose some lines are truncated as well, or hold a temperature that is not a
number at all. Modify your reading loop so that it skips any line that does not have three fields
or whose temperature will not convert to a number, counting how many lines it skipped.
Test it on this list before applying it to the real file:
test_lines = [
"IIB,01.01.22,30.2",
"IIB,02.01.22",
"IIB,03.01.22,notanumber",
"IIB,04.01.22,33.4",
]You should keep 2 and skip 2.
# Your solution here
# Hint: len(fields) != 3 catches the truncated line — use continue
# Hint: wrap the float() call in try/except ValueError to catch the bad number
# Hint: continue inside a for loop moves straight to the next lineExercise 17: The solar system¶

Figure 1:The eight planets and the Moon, as photographed by Mariner 10, Magellan, Galileo, Mars Global Surveyor, Cassini and the Voyager spacecraft. The inner bodies are roughly to scale with each other, as are the outer ones, but the two groups are not to scale with one another. Image NASA/JPL (PIA03153), public domain.
This is the long exercise for this subchapter, and the only one in it whose data is not environmental. Eight planets and their masses are few enough to type by hand, and familiar enough that a wrong answer is easy to spot, which makes them a good first test of lists, dictionaries and functions together.
Work through sections A to C in order: section C writes functions against the dictionary you
build in section B. You will pick the exercise up again in 1.7, once classes are available, and
turn the dictionary into a Planet type.
The masses come from NASA’s
planetary fact sheet. Use units of
10^24 kg throughout, so that Earth is 5.97 and Jupiter is 1898 — the variable names below carry
that unit, which is what makes a bare number like 1898 readable three cells later.
A: lists and loops¶
Q1) Create a list with the names of every planet in the solar system, in order.
Name it planets and use lowercase names, so it starts ["mercury", "venus", ...].
# Q1: create your list hereQ2) Have Python tell you how many planets there are by examining your list.
You should get 8. Do not type the number — read it off the list.
# Q2: count the planets
# Hint: len() reads the length off the list itselfQ3) Use slicing to display the first four planets — the rocky ones.
# Q3: the rocky planetsQ4) Iterate through your planets and print the planet name only if it ends in “s”.
You should see venus, mars and uranus, and nothing else.
Hint: the last letter of a name is planet[-1]. Use continue for the names that do not match,
so the loop body says explicitly what it skips.
# Q4: print the planets whose name ends in sB: dictionary¶
Q5) Create a dictionary that maps each planet name to its mass.
Call it planet_masses_1e24kg, and read the values off the
NASA fact sheet in units of 10^24 kg. Every
value has to be in that one unit: entered as 1.898 (in 10^27 kg), Jupiter would come out 1000
times too light in every comparison and ratio below.
# Q5: build the dictionary hereQ6) Use your dictionary to look up Earth’s mass.
# Q6: look up EarthQ7) Loop through the dictionary and build a list of every planet heavier than 100 x 10^24 kg.
You should end up with ["jupiter", "saturn", "neptune"] — uranus, at 86.8, does not make it.
Hint: .items() gives you the name and the mass together on each pass, as in 1.2’s lecture.
# Q7: the planets heavier than 100e24 kgQ8) Add Pluto to your dictionary.
Its mass is 0.0130 x 10^24 kg. Print the names afterwards, looping with .items(), to confirm it
landed.
Pluto has not been a planet since 2006. Adding it still takes one assignment, and none of the code that reads the other eight entries has to change.
# Q8: add Pluto, then print the namesC: functions¶
Q9) Write a function that converts a planetary mass to Earth masses.
Jupiter is about 1898 x 10^24 kg, which is about 318 Earth masses:
Call it to_earth_masses(mass_1e24kg) and check it against Jupiter — you should get
317.92294807370183.
# Q9: write to_earth_masses here, then test it on JupiterQ10) Now write a single function that converts to either Earth masses or Jupiter masses, depending on a keyword argument.
Call it to_planet_masses(mass_1e24kg, reference="jupiter"). Converting Jupiter’s own mass should
give 317.923 with reference="earth" and exactly 1.0 with reference="jupiter".
With the default argument, one function covers both conversions. Jupiter in Jupiter masses has to come out as exactly 1.0; any other value means the function divides by the wrong mass.
# Q10: write to_planet_masses here, then test both references on JupiterQ11) Write a function that returns two values.
mass_in_two_units(mass_1e24kg, reference) should return the mass in Earth masses and in masses
of whatever reference planet you pass. Test it on Jupiter with reference="mars".
Hint: return a, b returns a tuple, and first, second = f(...) unpacks it — both from 1.2’s
lecture.
# Q11: write mass_in_two_units here, then test it on Jupiter against marsBonus. Convert Neptune’s mass to Jupiter masses with your Q10 function. Then pass that result
to your Q11 function with reference="jupiter", and compare the second value it returns with the
Q10 result. Are they the same? If not, work out which function is being given a number in the
wrong unit, and what it should have been given instead — this is the mistake the _1e24kg suffix
exists to prevent.
# Bonus: Neptune, out and back