Exercise 1: A class with state and behaviour¶
Define a Glacier class with name and area_km2 instance attributes, a describe() method returning a short string, and a __repr__. Create one and print both the object and its description.
# Your solution hereExercise 2: Instance versus class attributes¶
Define a Planet class with a shared class attribute g_earth = 9.81 and per-instance attributes name and surface_gravity. Create two planets and show that g_earth is shared while surface_gravity differs.
# Your solution hereExercise 3: A dataclass record¶
Use @dataclass to define a Reading with timestamp (str) and temp_celsius (float). Show that two readings with equal fields compare equal, and print one to see the generated repr.
# Your solution hereExercise 4: Read and mutate methods, with validation¶
Define a Series class holding a list of floats, with add(x) to append and mean() to return the average. mean() should raise ValueError if there are no values. Demonstrate both.
# Your solution hereExercise 5: Composition¶
Define a Catchment class that holds several Series objects keyed by name (composition). Add an add_series(name, series) method and a names() method returning the keys. Build one with two series.
# Your solution hereExercise 6: Fix the shared class-level list¶
The class below shares one list across all instances. Rewrite it so each instance has its own entries, then show two loggers do not contaminate each other.
class Logger:
entries = []
def __init__(self, name):
self.name = name
def log(self, msg):
self.entries.append(msg)# Your solution hereExercise 7: Inheritance and overriding¶
Define a base Station with a name attribute and a kind() method returning "station". Define RiverGauge(Station) that overrides kind() to return "river gauge". Show that a RiverGauge keeps the inherited name but reports the new kind.
# Your solution hereExercise 8: An invariant with assert¶
Write normalise(weights) that divides each weight by the total, and asserts the invariant that the result sums to 1 (within a small tolerance). Test it on [2.0, 2.0].
# Your solution hereExercise 9: A custom exception¶
Define NegativeDischargeError(ValueError) and a function check(q_m3s) that raises it when discharge is negative. Catch it and print the message.
# Your solution hereExercise 10: Try / except / else / finally¶
Write safe_divide(a, b) that returns a / b, catches ZeroDivisionError (returning None), prints a message in the else branch when it succeeds, and always prints “done” in finally. Call it with (6, 2) and (6, 0).
# Your solution hereExercise 11: Validate preconditions¶
Write validate(temp_celsius, rh_percent) that raises ValueError if the temperature is below absolute zero or the relative humidity is outside 0–100 %. Show it passing on valid input and raising on rh_percent = 150.
# Your solution hereExercise 12: Logging with levels¶
Configure logging to stdout at INFO level, get a logger, and emit a debug message (which should be suppressed), an info message, and a warning.
# Your solution hereExercise 13: Write and run a pytest suite¶
Write a module mymod.py with double(x) returning 2 * x, and a parametrised test file that checks three cases. Run pytest on it with a subprocess and print the output.
# Your solution hereExercise 14: Replace assert-based validation¶
The line assert rh_percent >= 0, "negative humidity" disappears under python -O. Rewrite the check as a function that raises ValueError, so it fires regardless of optimisation. Demonstrate on a valid value.
# Your solution hereExercise 15: Validated earthquake events — a real dataset¶
The USGS publishes a feed of every earthquake it records; this exercise reuses the archived one-month extract from 1.5’s pandas exercises.
Combine both halves of this subchapter: a class that carries state, and exceptions that refuse to let it be built from bad data.
# Pre-supplied: download and cache the earthquake data (same file used in 1.5).
import pooch
quakes_path = pooch.retrieve(
url="https://raw.githubusercontent.com/gse-unil/2026_MLEES_book/main/data/part-I/usgs_earthquakes_2023_01.csv",
known_hash="sha256:14e6100a41c55772f73a34daf1cd6ea38ba0fa49a86bb62a3930868614975df2",
fname="usgs_earthquakes_2023_01.csv",
path=pooch.os_cache("mlees"),
)Step 1. Read quakes_path with pandas, keeping only the place, mag, and depth columns. Print the resulting shape.
# Your solution here
# Hint: pd.read_csv(quakes_path)[["place", "mag", "depth"]]Step 2. Define an EarthquakeEvent class with instance attributes place: str, magnitude: float, and depth_km: float; a __repr__ showing all three; and a method is_shallow(self, threshold_km=70.0) -> bool returning whether depth_km is below the threshold. In __init__, raise ValueError if magnitude or depth_km is negative — a real event never has either, and this is external data, not your own arithmetic, so it is an exception, not an assert.
# Your solution here
# Hint: def __init__(self, place: str, magnitude: float, depth_km: float) -> None:
# if magnitude < 0.0 or depth_km < 0.0: raise ValueError(...)
# Hint: def is_shallow(self, threshold_km: float = 70.0) -> bool:
# return self.depth_km < threshold_kmStep 3. Confirm the validation works: build one valid EarthquakeEvent and print it, then show that EarthquakeEvent("test", -1.0, 10.0) raises ValueError.
# Your solution here
# Hint: wrap the invalid construction in try/except ValueErrorStep 4. Loop over the three columns from Step 1 with zip, building one EarthquakeEvent per row inside a try/except ValueError. Collect the successfully built events in a list, and count how many rows were rejected instead of stopping the loop.
# Your solution here
# Hint: events = []
# n_rejected = 0
# for place, mag, depth in zip(quakes["place"], quakes["mag"], quakes["depth"]):
# try:
# events.append(EarthquakeEvent(place, mag, depth))
# except ValueError:
# n_rejected += 1Step 5. Using is_shallow(), count how many events in your list are shallow. Separately, find the single deepest event with an explicit loop that tracks a running “deepest so far” variable — the same pattern StationNetwork.coldest_station uses in the lecture, not max().
# Your solution here
# Hint: n_shallow = 0
# for event in events:
# if event.is_shallow():
# n_shallow += 1
# Hint: deepest = None
# for event in events:
# if deepest is None or event.depth_km > deepest.depth_km:
# deepest = eventStep 6. Print a short summary: the number of events built, the number of rows rejected, the number of shallow events, and the deepest event’s repr.
# Your solution here
# Hint: print(len(events), "events,", n_rejected, "rejected,", n_shallow, "shallow")
# print(deepest)Exercise 16: The solar system, continued¶
Exercise 17 of 1.2 left the solar system
as a dictionary: planet_masses_1e24kg, mapping a name to a mass in units of 10^24 kg. A
dictionary keeps the name and the mass together only by convention — pass the mass somewhere on
its own and the name is gone, which is exactly what went wrong in that exercise’s bonus question.
Now that classes are available, finish the job: give a planet a type, so that its name and its mass travel as one object and the comparison you want to make becomes a method on it.
The dictionary is rebuilt below so this exercise stands on its own.
# Pre-supplied: the dictionary from Exercise 17 of 1.2, rebuilt here.
planet_masses_1e24kg = {
"mercury": 0.330,
"venus": 4.87,
"earth": 5.97,
"mars": 0.642,
"jupiter": 1898.0,
"saturn": 568.0,
"uranus": 86.8,
"neptune": 102.0,
}Q12) Write a class Planet with two attributes, name and mass_1e24kg.
Create one instance for Earth (5.97) and one for Jupiter (1898), then print each one’s name and mass to check they were stored.
Hint: give __init__ type annotations, as 1.7’s lecture does — name: str, mass_1e24kg: float.
They are not enforced at runtime, but they are the only place the unit is written down.
# Q12: define the Planet class here, then create earth and jupiterQ13) Add a method is_light that compares a planet against Jupiter.
It should report True when the planet is strictly lighter than Jupiter, False when it is
strictly heavier, and the string "same mass!" when the two are equal. Ask it whether Earth is
lighter than Jupiter, and whether Jupiter is lighter than itself.
# Q13: add is_light to the class, then ask it about earth and about jupiterQ14) Now fix what Q13 asked you to build.
The method you just wrote has two problems that 1.7’s own material names directly.
It prints its answer instead of returning it, so a caller cannot use the result —
if earth.is_light():will not do what you expect.It returns a
boolon two branches and astron the third, so the caller has to test the type of the answer before trusting it.
Rewrite it as compare_to_jupiter() returning one of three strings — "lighter", "heavier",
"same" — and have the caller do the printing. Then loop over every planet in
planet_masses_1e24kg, build a Planet for each, and print the verdict for all eight.
# Q14: rewrite it as compare_to_jupiter, then report all eight planetsQ15) Give the class the invariant it is missing.
A negative mass is not a planet. Raise a ValueError from __init__ when mass_1e24kg is not
strictly positive, and show both that a valid planet still constructs and that
Planet("nonsense", -1.0) fails. Add a __repr__ so a Planet prints readably.
This is the same shape as Exercise 15’s EarthquakeEvent: validate at construction, so that every
object that exists is one you can trust.
# Q15: validate in __init__ and add __repr__, then show both outcomes