Read Structured Data with match and case
Choose the first matching structural case and capture values from small event dictionaries.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Read Structured Data with match and case
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Programs often receive small dictionaries that represent different kinds of events. A message has a type and some text. A movement has a type and coordinates. A third event might not fit either form. Python's match statement lets us describe those shapes directly and capture the values we need. It is more than a switch on a single constant: a pattern can inspect a mapping, a sequence, or an object. Python checks cases from top to bottom and runs the first one that matches. That ordering gives us a clear place for specific cases and a final fallback. In this video we will build an event formatter, deliberately write a case that captures everything by mistake, and fix it with quoted literals and a guard.
- 00:46
We will also see why matching a shape is not always the same as validating content. A text field can be a string yet still contain only spaces. The companion lesson on PythonLessonLab includes runnable examples, a browser console, a checked task, typical errors, and a short quiz. This syntax requires Python 3.10 or newer. Before we type code, predict the desired results: a nonblank message should show its trimmed text, a movement should display two integers, and anything else should become Unknown event. That fallback is useful because malformed input remains visible instead of silently producing None.
- 01:27
Both inputs enter the message branch. That is not because move equals message; it is because a bare name in a pattern is usually a capture. The pattern assigns the subject to a new name called message and succeeds. It is a powerful feature when we want to extract data, but a bug when we intended a fixed value. To compare with the literal word message, put it in quotes. This is one of the most important syntax differences to remember when learning match. In an if statement, writing `kind == message` would compare with an existing variable. In a case pattern, an unqualified bare name does something else.
- 02:07
Python will even reject a later case after an unconditional capture because it would be unreachable. That compiler error is helpful, but if the broad case is your only case, the wrong behavior can still look plausible. Let's repair the code by using `case "message":` and then a wildcard `case _:` for other kinds. The wildcard is also broad, so it belongs last. We will apply the same discipline to dictionary event patterns: quote the exact type string, capture only the fields that should vary, and make a final fallback explicit. A pattern match is not a conversion. The string "3" will not turn into integer 3 just because the move pattern expects an integer coordinate.
- 02:52
The first event matches even though it has an extra id key. A mapping pattern requires the keys it lists, but additional keys are permitted. The type value must equal the quoted string message. The text value must match the str class pattern, and the captured value becomes the local name text. Then the guard checks that some content remains after stripping spaces. The first event passes every step and returns Hi. The second has the right keys and a string value, but the guard fails because the string is blank after stripping. The third has an integer text field, so it fails the str pattern before the guard even runs.
- 03:32
This layered reading is useful: shape first, content rule second, result last. Do not confuse a guard with a general exception handler. If your case body contains another operation that can fail, matching does not magically catch it. The final wildcard gives us a deliberate result for the shapes we do not support. In production code you might raise a domain-specific error instead, especially if malformed input should stop processing. For this teaching exercise, Unknown event makes every path easy to see in the console and easy to verify. Try removing the guard mentally. Whitespace-only text would then become a visible but empty message line, which is not the behavior we promised.
- 04:18
The first case describes a message and trims its text. The second describes a movement and captures two integer coordinates. The third case catches whatever did not fit. Notice that the pattern does not modify the dictionary. It reads values and returns a new string. The string coordinate "3" does not match `int(x)`, so the fallback handles it. We could choose to convert numeric text, but that would be a separate policy, not an automatic behavior of match. Cases are checked in order, and the first match wins. A general two-key mapping pattern placed before a more specific one might take an event too early.
- 04:58
Keep the shapes narrow and place any broad fallback last. There is also a subtle Python detail: bool is considered a subclass of int, so an int class pattern can match True. If a real event API must reject booleans for coordinates, add a guard such as `type(x) is int and type(y) is int`. The exercise's contract uses normal integer coordinates, so we do not complicate the first version with that rule. Pattern matching is powerful, but it should make your intent clearer, not encourage clever cases that are hard to debug. If a simple if statement is clearer for a one-condition decision, use it.
- 05:38
Here the two dictionary shapes make match a readable fit. These checks cover the promise we made before writing code. A message with an extra id field still matches; its original dictionary remains unchanged. Blank text is rejected by the guard. A movement with a negative coordinate is valid, but a string coordinate is not silently converted. An unknown event reaches the fallback. The assertions do not prove that every possible mapping is safe for every real application. They do show that our two supported shapes and failure path behave consistently. Open the companion lesson and fill in the match cases yourself.
- 06:19
Start with quoted type literals, then capture the fields, then add the message guard, and keep the wildcard last. Run the practical checker and inspect any failing case before moving to the quiz. If you see a surprising branch, ask whether you wrote a literal or a capture pattern. If a dictionary with extra keys succeeds, remember that mapping patterns specify required keys rather than an exact complete schema. If blank text succeeds, the structure matched but the content rule is missing. This distinction between shape and guard is a useful way to read more complex match statements later. You can extend the function with another event type once these basics are solid, but keep branch order and fallback behavior explicit.
- 07:05
The goal is not to use match everywhere. It is to make structured alternatives easy for the next reader to understand.
Understand the concept
Python's match statement compares one subject against case patterns from top to bottom and runs only the first matching branch. It can compare literals, unpack sequences, and inspect the shape of mappings. A wildcard case _ handles anything left over. Unlike a chain of equality checks, a structural pattern can also capture values from inside the data. The syntax is available in Python 3.10 and later.
In a mapping pattern such as case {'type': 'message', 'text': str(text)}, the literal type must match and the text value must be a string. The name text captures that value for the case body. Extra mapping keys are allowed, so a timestamp does not make an otherwise valid message fail. A bare name in a pattern is normally a capture, not a comparison with an existing variable. Put quoted literals in cases when you mean exact text.
A guard follows a successful structural match: case ... if condition. This is useful when a string must also be nonblank after strip. Cases should be ordered from specific to general; an early broad capture or wildcard will make later cases unreachable. Pattern matching does not validate every possible malformed value on its own, so keep a final fallback with an explicit result or error.
The exercise formats two event shapes without mutating them. A nonblank message event becomes Message: followed by stripped text. A move event with integer x and y becomes Move to (x, y). Anything else returns Unknown event. The checker covers extra keys, wrong types, blank messages, and unknown kinds. That makes branch order and guards observable, not just syntax decoration.
- Cases are tried top to bottom
- Mapping patterns inspect required keys and allow extra keys
- A name pattern captures a value
- A guard adds a condition after a structural match
See it step by step
Read the code, predict the output, then compare it with the result.
01. Choose the first matching literal
def status_text(code):
match code:
case 200: return "OK"
case 401 | 403: return "Access denied"
case _: return "Other"
print(status_text(200))
print(status_text(403))
print(status_text(500))OK
Access denied
OtherOnly the first matching case runs; | combines literal alternatives and _ is the fallback.
02. Capture parts of a sequence
def point_text(point):
match point:
case (0, 0): return "origin"
case (x, y): return f"{x}, {y}"
case _: return "invalid"
print(point_text((0, 0)))
print(point_text((4, -1)))
print(point_text((1, 2, 3)))origin
4, -1
invalidThe two-item sequence pattern captures x and y; three items do not match that shape.
03. Guard a mapping pattern
def message(event):
match event:
case {"type": "message", "text": str(text)} if text.strip():
return text.strip()
case _:
return "no message"
print(message({"type": "message", "text": " Hi ", "id": 7}))
print(message({"type": "message", "text": " "}))Hi
no messageThe mapping may have extra keys. The guard rejects blank text after the structural match.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Start with literal alternatives and a fallback
A match statement evaluates its subject once and checks cases in source order. A literal case is the simplest: case 200 matches the integer 200. The vertical bar combines alternative literal patterns, so one branch can handle several related codes. A final wildcard case _ catches everything else. Without a matching case or a fallback, the match statement simply finishes and the surrounding function may return None.
Do not confuse the vertical bar inside a pattern with Python's boolean or operator. Here 401 | 403 is a pattern alternative, not an arithmetic expression evaluated to a single status. Cases run only once, even if more than one pattern could fit. Put specific cases before broad patterns. Run the example, then move the fallback to the top mentally: it would absorb every value and make the later branches useless.
def status_text(status):
match status:
case 200:
return "success"
case 401 | 403:
return "denied"
case _:
return "other"
print(status_text(200))
print(status_text(403))
print(status_text(500))success
denied
other- The 200 subject matches the first literal case.
- 403 matches the second branch's alternatives.
- 500 falls through to the wildcard.
Capture values only after matching a shape
A structural pattern can inspect the keys and value types in a dictionary. The message case below requires type to be the literal string message and text to be a string. The name text captures that string for the body. Extra keys such as id are allowed. If the text is an integer, the str(text) class pattern fails, and the fallback runs instead. This avoids calling strip on a non-string value.
The case includes an if guard because a string can still be blank. The guard runs only after the pattern has matched and text exists. `text.strip()` is truthy only when some non-space content remains. Be precise about bare names: a pattern such as case message captures almost any value rather than comparing with a variable named message. Use quoted literals for exact text.
def message_text(event):
match event:
case {"type": "message", "text": str(text)} if text.strip():
return text.strip()
case _:
return "no message"
print(message_text({"type": "message", "text": " Hi ", "id": 7}))
print(message_text({"type": "message", "text": " "}))
print(message_text({"type": "message", "text": 42}))Hi
no message
no message- The first mapping has both required keys and a string text value.
- A blank string matches the shape but fails the guard.
- An integer text value fails the str class pattern.
Combine event cases without hiding malformed data
A useful dispatcher can have several narrow shapes and one explicit unknown result. Our exercise accepts a message with nonblank text or a move event with two integer coordinates. The mapping patterns inspect only the listed keys, so an event can carry additional metadata without breaking its essential shape. Because the move pattern uses int(x) and int(y), the string '3' does not silently become an integer coordinate.
The final wildcard makes malformed input visible as Unknown event instead of an accidental None. This is a teaching contract, not a complete schema validator: Python bool values are also instances of int for class patterns. If a real API must reject booleans as coordinates, add an explicit guard checking exact types. Complete the practice task, then try moving the wildcard above the other cases; Python will reject unreachable cases, which shows why order matters.
def describe_event(event):
match event:
case {"type": "message", "text": str(text)} if text.strip():
return "Message: " + text.strip()
case {"type": "move", "x": int(x), "y": int(y)}:
return f"Move to ({x}, {y})"
case _:
return "Unknown event"
print(describe_event({"type": "message", "text": " Ready "}))
print(describe_event({"type": "move", "x": 3, "y": -2}))
print(describe_event({"type": "move", "x": "3", "y": -2}))Message: Ready
Move to (3, -2)
Unknown event- The nonblank message is stripped after its shape matches.
- Integer coordinates are captured and formatted.
- The string coordinate fails the move shape and reaches the fallback.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Put the message case before the wildcard and add `if text.strip()` as a guard. Use `case {'type': 'move', 'x': int(x), 'y': int(y)}` for coordinates. Keep `case _` last.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Using a capture where a literal is required
match event_type:
case message:
return 'message'match event_type:
case 'message':
return 'message'What happens: message captures any value, so this branch matches everything.
Quoted text is a literal pattern; an unqualified bare name captures.
Placing a broad branch too early
match point:
case (x, y): return 'point'
case (0, 0): return 'origin'match point:
case (0, 0): return 'origin'
case (x, y): return 'point'What happens: The origin is handled by the first two-item pattern and never reaches its specific branch.
Order specific shapes before general ones.
Assuming a shape also means valid content
case {'type': 'message', 'text': str(text)}:
return 'Message: ' + textcase {'type': 'message', 'text': str(text)} if text.strip():
return 'Message: ' + text.strip()What happens: Whitespace-only messages are treated as meaningful content.
A guard expresses conditions that a shape alone does not capture.