90.5 Python 3.10: match/case, Better Error Messages
Alright, let’s talk about Python 3.10. This is where things started to get genuinely fun again. For years, we’d been dutifully writing if/elif/elif chains that looked like a sad, tangled string of Christmas lights. Then, the language designers finally gave in and gave us what every other self-respecting modern language had: structural pattern matching, or as we call it, match/case. It’s not a switch statement. Don’t call it that. It’s so much more, and also, in some ways, a little less. Let’s dive in.
The match/case Statement: Finally, A Proper Switch
The match statement is your new best friend for untangling complex conditional logic. You take a value and then match it against a series of case patterns. The first one that fits, wins. It’s like an if/elif/elif chain on steroids.
def describe_entity(entity):
match entity:
case []:
return "An empty list. How profound."
case [x]:
return f"A list with one item: {x}"
case [x, y]:
return f"A pair: ({x}, {y})"
case _:
return "A list with, like, a bunch of stuff."
print(describe_entity([])) # An empty list. How profound.
print(describe_entity([42])) # A list with one item: 42
print(describe_entity([1, 2])) # A pair: (1, 2)
See? Clean. Readable. The _ is the wildcard, the default case that catches everything you haven’t handled. You should always include it, unless you’re absolutely positive you’ve covered all possibilities, which you almost never are.
Beyond Simple Matching: Unpacking Like a Pro
This is where it gets powerful. You can unpack values while you match. It’s like if and tuple unpacking had a beautiful, highly logical baby.
def handle_command(command):
match command.split():
case ["quit"]:
print("Goodbye!")
exit()
case ["load", filename]:
print(f"Loading {filename}...")
case ["save", filename]:
print(f"Saving to {filename}...")
case _:
print(f"Unknown command: '{command}'")
handle_command("load my_file.txt") # Loading my_file.txt...
handle_command("nonsense") # Unknown command: 'nonsense'
Notice how we’re matching on the structure of the list created by .split(). The first element must be the string “load”, and the second element gets captured into the variable filename. This is the killer feature. It makes parsing text-based commands or data structures an absolute breeze.
Matching on Types and Guards: Getting Fancy
You’re not limited to sequences. You can match on types and add conditional checks, known as “guards,” for when a simple pattern isn’t specific enough.
def process_data(data):
match data:
case str() as text if text.isnumeric():
print(f"It's a string that's all numbers: {text}")
case str() as text:
print(f"It's a string: {text}")
case int() | float() as number:
print(f"It's a number: {number}")
case _:
print("I don't know what this is.")
process_data("hello") # It's a string: hello
process_data("123") # It's a string that's all numbers: 123
process_data(45.2) # It's a number: 45.2
The as keyword captures the whole matched value into a variable. The | allows you to match against multiple patterns (here, int or float). The if guard after the pattern adds an extra condition that must be true for the case to execute. This is incredibly expressive and lets you replace truly gnarly nested if statements with clean, linear code.
The Obvious Pitfall: Variable Capture
Here’s the first thing that will bite you. In a case statement, what looks like a literal is a literal, and what looks like a variable name is a variable that will capture whatever is in that position. This leads to the most common rookie mistake.
points = 100
value_to_match = 100
match value_to_match:
case points: # This is NOT comparing to the variable `points` above!
print(f"You got {points} points!") # This prints: You got 100 points!
# The case `points:` is a capture pattern. It always matches and assigns the value of `value_to_match` to a new variable named `points`, shadowing the outer one.
To match against a value stored in a variable, you must use a dotted name (like an attribute). The standard workaround is to use a class or an enum, but a quick-and-dirty fix is this:
match value_to_match:
case int() as points if points == points: # Compare to the outer `points`
print(f"You got the high score of {points}!")
Yeah, it’s a bit of a clunker. The language designers knew this would be a footgun, but the syntax for making a true value pattern was too ambiguous. So we live with it. Always use literals (42, "hello") or constants (Color.RED) in your patterns, not variable names you expect to be compared.
Matching on Custom Classes
This is the real showstopper. You can match based on the type of an object and its attributes.
class Point:
__match_args__ = ('x', 'y') # This defines the order for positional matching
def __init__(self, x, y):
self.x = x
self.y = y
def location_description(point):
match point:
case Point(x=0, y=0):
return "Origin"
case Point(x=0) as p: # Any point where x is 0
return f"On the Y-axis at {p.y}"
case Point(y=0) as p:
return f"On the X-axis at {p.x}"
case Point(x=x, y=y):
return f"At ({x}, {y})"
case _:
return "Not a point"
print(location_description(Point(0, 0))) # Origin
print(location_description(Point(0, 7))) # On the Y-axis at 7
The __match_args__ dunder is crucial here. It tells the match statement what order to expect attributes in if you want to use a positional style, like case Point(0, 7): instead of case Point(x=0, y=7):. It’s a bit of boilerplate, but it makes the patterns beautifully concise.
Better Error Messages: The Unsung Hero
While match/case is the flashy new feature, the improved error messages in 3.10 are the workhorse that will save you hours of debugging. The designers finally realized that SyntaxError: invalid syntax is a useless thing to say to a human being.
Before 3.10, if you forgot a colon, you’d get:
SyntaxError: invalid syntax
In 3.10 and later, you get:
SyntaxError: expected ':'
It points directly to the place where it expected the colon. It’s the same for missing commas in collections, mismatched brackets, and a host of other common errors. The parser now does a much better job of guessing what you meant to type instead of just giving up and calling you invalid. It’s a small change that makes the learning curve for Python significantly less steep. It’s not witty or clever, it’s just good, thoughtful design—and we love to see it.