-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
61: Structural Pattern Matching with Classes and Sequences
Up until now, you've probably relied on if/elif/else chains to handle different types of data. They work, but once you start dealing with complex nested structures—like a mix of custom objects and lists—your code starts looking like a pyramid of indentation. That's where structural pattern matching comes in. It's not just a "switch" statement; it's a way to describe the shape of the data you're looking for.
Defining our Game Events
To see this in action, let's build a simple event processor for a 2D game. We have different types of actions: some are structured as classes (for complex state), and others are simple sequences (for quick commands). Let's start by defining a couple of classes to represent movement and combat.
class Move:
__match_args__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
class Attack:
__match_args__ = ("target", "damage")
def __init__(self, target, damage):
self.target = target
self.damage = damage
Notice those __match_args__ tuples. I'm adding those because they tell Python, "When someone matches this class positionally, here is the order of the attributes." Without them, you'd have to use keyword arguments in your patterns, which is fine, but positional matching is much cleaner.
Handling the Simple Stuff with Classes
Now, let's write a function to process these events. Instead of checking isinstance() over and over, we use match. This allows us to "unpack" the attributes of the class directly into variables we can use inside the case block.
def handle_event(event):
match event:
case Move(x, y):
print(f"Character moving to coordinates: {x}, {y}")
case Attack(target, damage):
print(f"Dealing {damage} damage to {target}!")
case _:
print("Unknown event received.")
I love this because the pattern Move(x, y) does two things at once: it checks if event is an instance of Move AND it assigns the values of event.x and event.y to local variables. It's incredibly concise.
Tackling Sequence-based Commands
In many games, you might have a console where you type commands. These usually come in as lists or tuples. Let's say we want to support commands like ["teleport", 10, 20] or ["wait"]. We can mix sequence matching right into the same match block.
def handle_event(event):
match event:
case Move(x, y):
print(f"Character moving to coordinates: {x}, {y}")
case Attack(target, damage):
print(f"Dealing {damage} damage to {target}!")
case ["teleport", x, y]:
print(f"Magic teleport to {x}, {y}!")
case ["wait"]:
print("Character is idling...")
case _:
print("Unknown event received.")
Python is now looking at the structure. If it sees a list with exactly three elements and the first one is the string "teleport", it triggers that block. If it's a list with one element "wait", it takes that path.
The Order Problem (and how I messed it up)
Here is where I usually trip up when I'm coding this in a hurry. Let's say I want to add a "catch-all" for any list command that I haven't specifically defined yet, maybe just to log it. I might do this:
# WARNING: This version has a bug
def handle_event(event):
match event:
case Move(x, y):
print(f"Moving to {x}, {y}")
case [command, *args]: # Catch-all for any list
print(f"Executing generic command {command} with args {args}")
case ["wait"]:
print("Character is idling...")
case _:
print("Unknown event.")
If I pass ["wait"] into this, it will never hit the ["wait"] case. Why? Because ["wait"] matches the pattern [command, *args] perfectly (command becomes "wait" and args becomes an empty list). Pattern matching is sequential. I effectively put a giant net in front of a small hole.
The fix is simple: always put your most specific patterns at the top and your most general ones at the bottom. I'll move the generic list handler below the specific "wait" and "teleport" commands.
Adding a bit of logic with Guards
Sometimes, matching the structure isn't enough. You might need to match the structure and check a condition. We do this with "guards" using the if keyword inside the case.
def handle_event(event):
match event:
case Move(x, y) if x < 0 or y < 0:
print("Can't move into negative coordinates!")
case Move(x, y):
print(f"Moving to {x}, {y}")
case ["teleport", x, y] if x > 100 or y > 100:
print("Teleport destination is out of bounds!")
case ["teleport", x, y]:
print(f"Teleporting to {x}, {y}")
case ["wait"]:
print("Idling...")
case [command, *args]:
print(f"Generic command {command} with {args}")
case _:
print("Unknown event.")
Now the logic is read like a story: "If it's a Move and the coordinates are negative, do this. Otherwise, if it's just a Move, do that." It keeps the validation logic tightly coupled with the data structure it's validating.
📋 Practical Task
Build a Combat Log Parser
You are building a system that parses a combat log from a RPG. The log is a list containing different types of events. Some events are Damage objects, and some are lists representing "Status Effect" changes.
Your task:
- Create a
Damageclass with__match_args__foramountandtype(e.g., "Fire", "Physical"). - Write a function
parse_log_entry(entry)that uses structural pattern matching to:- Handle
Damageobjects: If the type is "Critical", print "CRITICAL HIT for [amount]!". Otherwise, print "Took [amount] [type] damage." - Handle lists with the format
["buff", "strength", duration]: Print "Strength increased for [duration] seconds." - Handle lists with the format
["debuff", "poison", duration]: Print "Poisoned for [duration] seconds." - Handle any other list starting with "buff" or "debuff" as a "Generic status change."
- Handle everything else as "Unrecognized log entry."
- Handle
Test your function with a list of entries like: Damage(50, "Physical"), Damage(100, "Critical"), ["buff", "strength", 10], ["debuff", "slow", 5], and "Random noise".
There are no comments for now.