When Python 3.10 introduced structural pattern matching (match/case syntax), there was an excitement and confusion in the community. In first days, I was one of those confused people (confused of other people's excitement). I was looking at match/case in Python as something similar to switch/case in other languages: a glorified if-elif-else chain. But soon I realized it as a much more powerful feature than simple switch/case.
Now that we have reached a point where all minimum Python versions with active support is 3.10, I thought I can give it a try to use match/case in my codes. But as soon as I started using this syntax, I started realising it has a much deeper links to the language syntax than I thought. In this post, I want to tell you about these findings.
Literal patterns – the first taste
Let’s start with a trivial match on a constant values:
x = 1
match x:
case 1:
print("one")
This is equivalent to if x == 1: ... syntax. Easy!
And if we want to execute same code block for multiple values, or have a default block:
x = 1
match x:
case 1:
print("one")
case 2 | 3:
print("two or three")
case _:
print("other")
The _ symbol is part of syntax (some kind of wildcard for Any Values), but I can rewrite the above code like this:
x = 1
match x:
case 1:
print("one")
case 2 | 3:
print("two or three")
case n:
print(f"other: {n}")
Here n is just a simple variable which matches the condition as "it possible to do n <- x? if yes, then set n = x, and execute the code block".
So far, so good. But, this still is not much more than Python's if/elif/else syntax. Python's match/case will shine when you use it for more complex objects. For example you can use these patterns in match expression when working with tuples, lists and dictioanries:
event = {
"type": "purchase",
"user": 42,
"amount": 250,
"currency": "USD",
"items_count": 5,
},
match event:
# Login or signup events
case {
"type": "login" | "signup", # Match any of "login" or "signup" strings
"user": user_id,
"ip": ip,
} as evt: # get a variable pointing to the event variable
print(f"User {user_id} login: {evt}", user_id, evt)
# Failed authentication from external IPs
case {
"type": "login_failed",
"ip": ip,
"error": {"code": code, "message": message} as error # you can also use `as` to capture a subpattern
} if not ip.startswith("10."):
print(f"Suspicious login from IP address {ip}. Info: {rest!r}")
# Premium purchases
case {
"type": "purchase",
"user": _, # event must have a key user with any values (_ is a special keyword inside match syntax and you cannot treat it as a variable in case blocks)
"amount": amount,
"currency": "USD",
**rest,
} if amount >= 100: # Define comlex conditions in if caluse
print(f"Large purchase with amount {amount} placed")
# Normal purchases
case {
"type": "purchase",
"amount": amount,
}:
print(f"Normal purchase: {event}")
case _: ##
print(f"Ignoring event: {event}")
There are some caveats on using lists and dicys for structural matching:
- The
_wildcard is a special case inmatchclause which tells Python to ignore the value, you can't access the value using_variable inmatchblocks. - Dictionary patterns don't have to list all keys in the object, so
{"a": 1, "b": 2}matches pattern{"a": 1}. - List patterns matches same size of elements, unless you use a unpacking pattern (either
*_if you don't need these arbitrary length values, or*xto access them in case block). - As per above points, there is no
**_pattern in dicts and Python would not ercognize**_inside patterns. - You can't use wildcard variable as dictionary key. This is a wrong pattern:
{x: "value"}❌.
Pattern matching on Sets
A Confusing point: You can't use pattern matching capabilities available for dict and list in set! What? Python doesn't support patttern matching for sets? Yes, unfortunately😞. But you can check these conditions:
data = {1,2,3}
match data:
case set() as s if {1, 2}.issubset(s): # Check if s is a subclass of type `set` and `{1,2}` is a subset of variable s
print("Contains 1 and 2")
case set(): # Just check for s being a set
print("Some other set")
There is a small point in case set() usage. Python interprets this as if isinstance(data, set). And this is how it handles patterns for custom classes.
Class based structural patterns
So far, we've discussed matching built-in data structures. But structural pattern matching becomes more interesting when we start matching objects created from our own classes.
Consider this example:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(0, 20)
match p:
case Point(x=0, y=0):
print("This point is the origin")
case Point(x=0):
print(f"Point is on x=0 line")
case Point(x=x1, y=x2):
print(f"Point is at ({x1}, {x2})")
The important point here is that Point(x=..., y=...) doesn't create a new Point object. It is a class pattern that asks whether the object is an instance of Point and, if so, checks its attributes.
In other words, case Point(x=x, y=y) roughly means: Does this object instance of Point, and does it have an x and a y attribute whose values I can bind to x1 and x2 variables?
As you can see in case Point(x=0), we have omitted y attribute, because we don't care about the value, even though, Point class cannot be instanciated with only x argument. In fact, Python treats class objects like dict objects.
This is very interesting. But how does that work? Actually, the syntax is a little misleading at the first glance. Python doesn't care about arguments we pass to __init__() of this class. Python interprets case Point(x=0, y=0) as "object p is instance of type Point and point.x == 0 and p.y == 0". Let's check another example:
class Rectangle:
def __init__(self, w, h):
self.x = x
self.y = y
@property
def s(self):
return self.x * self.y
shape = Rectangle(0, 20)
match shape:
case Rectangle(s=0): # Rectangle.s is a property!
print(f"Rectangle size is Zero!")
case Rectangle(w=w, h=h) if w == h:
print(f"It's a Square with side size {w}")
case Rectangle(w=w, h=h):
print(f"Rectangle with({w=}, {h=})")
Positional Attributes and __match_args__
Because of attributes normally having no order, we define them like named kwargs. Python also allows us to define patterns in positional order, but first we have to define the order using __match_args__:
class User:
__match_args__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Jack", 25)
match user:
case User("Jack", 25):
print("Hello Jacky.")
case User():
print("Do we know each other?")
Dataclasses automatyically define __match_args__
Yes, dataclass types have no logic and they are supposed to eork like a simple container for data, so they can automatically define the order:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
point = Point(10, 20)
match point:
case Point(x, y):
print(f"Point: ({x}, {y})")
The bigger picture
At this point, we can see that Python's patterns aren't limited to simple value comparisons and value checking. We can describe:
- Mapping structure:
case {"type": "login", "user": user_id}: - Sequence structure:
case [0, x, *_]: - Class structure:
case User(name=name, age=age): - Nested structure:
case User(
address=Address(city=city)
):
- Structure + condition:
case Circle(radius) if radius > 100:
The common idea is the same in all of them: describe the structure you're interested in, extract the pieces you need, and optionally add a condition.
This fantastic feature has a lot of capabilities. I'm bery keen to see how the community will make use of this feature.
Top comments (0)