Open the notebook 21-dataclasses-pattern-matching-starter in your workspace and run it once. It holds a hand-written Flight class and four made-up flights, and a comment marks where the code of each part goes. Steps 1 to 3 are the exercise. Pattern matching is optional and sits under If time permits.
Do one part at a time, 1a, 1b and so on, and compare with the expected output before you go on. The solution is in 21-dataclasses-pattern-matching-solution, under every part on the exercise site, and at the back of the exercises PDF.
Step 1Data class
1a
Run the Flight cell and read its output. Which field is missing from the printed flights? Are the two identical flights equal?
Turn Flight into a data class. Remove __init__ and __repr__, keep the four fields, and run the cell again. Compare with 1a : does the printed form now show every field, and are the two identical flights equal?
Hint 1
The slides Declaring a Data Class and Automatic Methods show the decorator and the fields.
Hint 2
A data class lists its fields as annotated names in the class body, and the decorator writes __init__, __repr__ and __eq__ from them.
Add a __post_init__ method that raises a ValueError when origin or dest is not three uppercase letters, such as "ams" or "LONDON". Put the flight number and the bad code in the message.
Hint 1
The slide Post-Initialization shows when the decorator calls __post_init__.
Hint 2
str has len, isalpha and isupper. A loop over (self.origin, self.dest) checks both airports with one piece of code.
Show solutionHide solution
python
def __post_init__(self) -> None: for code in (self.origin, self.dest): if not (len(code) == 3 and code.isalpha() and code.isupper()): raise ValueError(f"{self.flight}: {code!r} is not an IATA airport code")
3b
Make __post_init__ raise a ValueError as well when origin and dest are the same airport.
Show solutionHide solution
python
if self.origin == self.dest: raise ValueError(f"{self.flight}: origin and destination are both {self.origin}")
3c
Create these three flights, each inside a try, and print the error : ("KL1005", "ams", "LHR"), ("KL1006", "AMS", "AMS") and ("KL1007", "AMS", "LONDON"). Then write a comment that says where the error appears : when the object is created, or when it is used?
Hint 1
The three records go through the same try, so a loop over them saves two copies of the code.
Check your output
ValueError: KL1005: 'ams' is not an IATA airport codeValueError: KL1006: origin and destination are both AMSValueError: KL1007: 'LONDON' is not an IATA airport code
Your wording of the messages may differ from these.
Show solutionHide solution
python
for bad in (("KL1005", "ams", "LHR"), ("KL1006", "AMS", "AMS"), ("KL1007", "AMS", "LONDON")): try: Flight(*bad) except ValueError as err: print("ValueError:", err)# The error appears when the object is created, inside __post_init__.# A bad record never exists, so no later code can use it.
If time permits
Steps 4 to 6 are optional. They use the Flight data class from Steps 1 to 3, so finish those first. Run the starter's last cell, optional_pattern_matching, before you start. It holds commands and messages, and a board that has a copy of every flight by flight number.
4a
The commands list holds operator commands as lists of words. Write a function run(command) that uses match. Give it a case for ["list"] that prints one line per flight on the board, with flight number, route and delay. Call run(["list"]).
Hint 1
The slide Matching Sequences shows a match on a list of words.
def run(command: list[str]) -> None: match command: case ["list"]: for f in board.values(): print(f" {f.flight} {f.origin}-{f.dest} delay {f.dep_delay}")run(["list"])
4b
Add three cases :
["delay", flight, minutes] adds the minutes to the dep_delay of that flight
["cancel", flight] appends "cancelled" to the remarks of that flight
["divert", flight, airport] prints that the flight is diverted
Hint 1
The slide Capture Patterns explains how a plain name in a pattern picks up the value at its position.
Hint 2
minutes is text. It has to go through int() before it can be added.
Show solutionHide solution
python
case ["delay", flight, minutes]: board[flight].dep_delay += int(minutes) print(f"{flight} delayed to {board[flight].dep_delay} minutes") case ["cancel", flight]: board[flight].remarks.append("cancelled") print(f"{flight} cancelled") case ["divert", flight, airport]: print(f"{flight} diverted to {airport}")
4c
Add a case for [] that does nothing, and a last case for anything else that prints the verb and the arguments it did not understand. Run every command in commands, and print the command before each run. What does ["delay", "KL1001"] match, and why not the delay case?
Hint 1
The slide Extended Sequence Matching shows how a starred name collects the rest of a sequence.
Hint 2
The order of the cases matters : the first pattern that matches wins.
Check your output
> list KL1001 AMS-LHR delay 12 KL1002 AMS-CDG delay -3 DL0045 JFK-AMS delay 47 BA0431 LHR-AMS delay 95> delay KL1002 25KL1002 delayed to 22 minutes> cancel BA0431BA0431 cancelled> divert DL0045 RTMDL0045 diverted to RTM> delay KL1001Unknown or incomplete command 'delay' with arguments ['KL1001']>
Show solutionHide solution
python
case []: pass case [verb, *rest]: print(f"Unknown or incomplete command {verb!r} with arguments {rest}")for command in commands: print(">", " ".join(command)) run(command)
4d
Try run(["delay", "XX9999", "10"]) and read the error. Then add a guard, if flight in board, to the delay and cancel cases, and try it again.
Hint 1
The slide Guards shows the if after a pattern. A case whose guard fails moves on to the next case.
Check your output
Unknown or incomplete command 'delay' with arguments ['XX9999', '10']
Before the guard, the call fails with a KeyError on 'XX9999'.
Show solutionHide solution
python
case ["delay", flight, minutes] if flight in board: board[flight].dep_delay += int(minutes) print(f"{flight} delayed to {board[flight].dep_delay} minutes") case ["cancel", flight] if flight in board: board[flight].remarks.append("cancelled") print(f"{flight} cancelled")run(["delay", "XX9999", "10"])
4e
Let the verb "scrap" do the same as "cancel". Try run(["scrap", "KL1001"]).
Hint 1
The slide OR Patterns shows alternatives separated by |.
Check your output
KL1001 cancelled
Show solutionHide solution
python
case ["cancel" | "scrap", flight] if flight in board: board[flight].remarks.append("cancelled") print(f"{flight} cancelled")run(["scrap", "KL1001"])
5a
Write a function classify(flight: Flight) -> str that uses class patterns on your data class. A flight whose last remark is "cancelled" is "cancelled", and any other flight is "on time". Print the class of every flight in flights, and of these two extra flights : Flight("KL1009", "AMS", "OSL", 5, ["cancelled"]) and Flight("KL1010", "CDG", "AMS", 4).
Hint 1
The slide Matching Objects shows a class pattern with named attributes.
Hint 2
The remarks are a list of any length. The slide Extended Sequence Matching shows how *_ accepts a remainder without binding it, so [*_, "cancelled"] matches a list that ends in "cancelled".
Check your output
KL1001: on timeKL1002: on timeDL0045: on timeBA0431: on timeKL1009: cancelledKL1010: on time
Show solutionHide solution
python
def classify(flight: Flight) -> str: match flight: case Flight(remarks=[*_, "cancelled"]): return "cancelled" case Flight(): return "on time"extra = [Flight("KL1009", "AMS", "OSL", 5, ["cancelled"]), Flight("KL1010", "CDG", "AMS", 4)]for f in flights + extra: print(f"{f.flight}: {classify(f)}")
5b
Add two cases before the last one : a flight with a dep_delay above 60 is "severely late", and a flight with a dep_delay above 15 is "late". Put the delay in the text, for example "late, 47 minutes". Run the loop again. Then move the > 15 case above the > 60 case and run it once more : what happens to BA0431?
Hint 1
A class pattern can capture an attribute, Flight(dep_delay=delay), and take a guard.
Hint 2
The first matching pattern wins, as in a SQL CASE WHEN.
Check your output
KL1001: on timeKL1002: on timeDL0045: late, 47 minutesBA0431: severely late, 95 minutesKL1009: cancelledKL1010: on time
Show solutionHide solution
python
case Flight(dep_delay=delay) if delay > 60: return f"severely late, {delay} minutes" case Flight(dep_delay=delay) if delay > 15: return f"late, {delay} minutes"
5c
Add a case for a flight to AMS that is not late : "on time, inbound from" and its origin. Put it before the last case, and run the loop again.
Hint 1
A class pattern can match an attribute on a literal, Flight(dest="AMS"), and capture another one in the same pattern.
Check your output
KL1001: on timeKL1002: on timeDL0045: late, 47 minutesBA0431: severely late, 95 minutesKL1009: cancelledKL1010: on time, inbound from CDG
Show solutionHide solution
python
case Flight(dest="AMS", origin=origin): return f"on time, inbound from {origin}"
6a
The messages list is shaped like the messages a live flight feed sends. Write a function handle(message) that uses mapping patterns. A position message with "on_ground": True prints that the aircraft is on the ground, and any other message prints its type. Run every message in messages.
Hint 1
The slide Matching Dictionaries shows a mapping pattern that names some keys and ignores the rest.
Hint 2
Use {"type": kind} for the last case : it matches any message that has a type key and binds it.
Check your output
ignored message of type 'position'ignored message of type 'position'KLM45 is on the groundignored message of type 'landed'ignored message of type 'error'ignored message of type 'heartbeat'
Show solutionHide solution
python
def handle(message: dict) -> None: match message: case {"type": "position", "on_ground": True, "callsign": callsign}: print(f"{callsign} is on the ground") case {"type": kind}: print(f"ignored message of type {kind!r}")for message in messages: handle(message)
6b
Add two more cases before the last one. A position above 9000 metres prints that the aircraft is cruising, and any other position prints that it is climbing or descending. Run every message again.
Hint 1
The cruising case needs a guard on the altitude. The slide Guards shows the syntax.
Hint 2
Put the cruising case before the general position case, or it never runs.
Check your output
KLM1001 cruising at 11278 mEIN602 climbing or descending at 1150 mKLM45 is on the groundignored message of type 'landed'ignored message of type 'error'ignored message of type 'heartbeat'
Show solutionHide solution
python
case {"type": "position", "callsign": callsign, "altitude": altitude} if altitude > 9000: print(f"{callsign} cruising at {altitude} m") case {"type": "position", "callsign": callsign, "altitude": altitude}: print(f"{callsign} climbing or descending at {altitude} m")
6c
Add a case for a landed message before the last one. Print the callsign and the airport, and collect the other keys of the message in rest and print those as well.
Hint 1
The slide Matching Dictionaries shows **rest at the end of a mapping pattern.
Check your output
DAL45 landed at AMS, also {'runway': '18R'}
This is the line for the landed message. Your wording may differ.
Show solutionHide solution
python
case {"type": "landed", "callsign": callsign, "airport": airport, **rest}: print(f"{callsign} landed at {airport}, also {rest}")
6d
Add a case before the last one for an error message with code 429 or 503 that prints a retry message with the code. Run every message once more. Which keys of the messages does no pattern mention?
Hint 1
The slide OR Patterns shows a choice inside a mapping pattern. 429 | 503 as code binds the value that matched.
Check your output
KLM1001 cruising at 11278 mEIN602 climbing or descending at 1150 mKLM45 is on the groundDAL45 landed at AMS, also {'runway': '18R'}retry later, status 429ignored message of type 'heartbeat'
Your wording of the lines may differ.
Show solutionHide solution
python
case {"type": "error", "code": 429 | 503 as code}: print(f"retry later, status {code}")
Extras
Write an Airport data class with frozen=True, order=True and the fields code and city. Use instances as dictionary keys, try to change a city, and make a changed copy with dataclasses.replace. Sort a list of airports and check which field decides the order.
Build a pandas DataFrame from [asdict(f) for f in flights] and sort it by dep_delay.
Print [f.name for f in fields(Flight)], and explain why astuple with those names is the safer way to hand the records to Spark.