Controversy – Python Adding Everything But the Kitchen Sink?
Is Python piling in too many new features taken from other languages? Is Pattern Matching yet another way of doing things for a language which has prided itself on there being one obvious way of doing things? In short, is Python being ruined by people who don’t appreciate the benefits of Less Is More?
Short answer: No ;-). Long answer: see below. Changed answer: see Second Impressions of Python Pattern Matching
I appreciate arguments for simplicity, and I want the bar for new features to be high, but I am glad Pattern Matching made its way in. It will be the One Obvious Way for doing, errr, pattern matching. Pattern matching may not be as crucial in a dynamically typed language like Python but it is still useful. And the syntax is nice too. match
and case
are pretty self-explanatory. Of course, there are some gotchas to watch out for but pattern matching is arguably one of the most interesting additions to Python since f-strings in Python 3.6. So let’s have a look and see what we can do with it.
What is Pattern Matching?
Not having used pattern matching before in other languages I wasn’t quite sure how to think about it. According to Tomáš Karabela I’ve been using something similar without realising it (Python 3.10 Pattern Matching in Action). But what is Pattern Matching? And why would I use it in Python?
I have found it useful to think of Pattern Matching as a Switch statement on steroids.
Pattern Matching is a Switch Statement on Steroids
The switch aspect is the way the code passes through various expressions until it matches. That makes total sense – one of the earliest things we need to do in programming is respond according the value / nature of something. Even SQL has CASE WHEN
statements.
The steroids aspect has two parts:
Unpacking
Unpacking is beautiful and elegant so it is a real pleasure to find it built into Python’s Pattern Matching. case (x, y):
looks for a two-tuple and unpacks into x
and y
names ready to use in the code under the case condition. case str(x):
looks for a string and assigns the name x
to it. Handy.
Type Matching
Duck typing can be a wonderful thing but sometimes it is useful to match on type. case Point(x=x, y=y):
only matches if an instance of the Point
class. case int(x):
only matches if an integer. Note – case Point(x, y): doesn’t work in a case condition because positional sub-patterns aren’t allowed. Confused? More detail on possible gotchas below:
Gotchas
Sometimes you think exactly the same as the language feature, sometimes not. Here are some mistakes I made straight away. Typically they were caused by a basic misunderstanding of how Pattern Matching “thinks”.
Patterns aren’t Objects
case Point(x, y):
seems to me to be an obvious way of looking for a Point
object and unpacking its values into x
and y
but it isn’t allowed. It is the correct syntax for instantiating a Point
object but we are not instantiating an object and supplying the object the case condition – instead we are supply a pattern to be matched and unpacked. We have to have a firm grasp on the notion that Python patterns are not objects.
Patterns Ain’t Objects
If we forget we get a TypeError:
case Point(0, 999):
TypeError: Point() accepts 0 positional sub-patterns (2 given)
Note, you must match the parameter names (the left side) but can unpack to any variable names you like (the right side). It may feel a bit odd being forced to use what feel like keyword arguments when the original class definition is positional but we must remember that we aren’t making an object – we are designing a pattern and collecting variables / names.
case Point(x=0, y=y):
the x=
and y=
are the required syntax for a pattern. We insist on x
being 0
but y
can be anything (which we add the name y
to). We could equally have written case Point(x=0, y=y_val):
or case Point(x=0, y=spam):
.
case Point:
, case int:
, case str:
, case float:
don’t work as you might expect. They match anything and assign it to the name Point
or int
or str
etc. Definitely NOT what you want. The only protection is when you accidentally do this before other case conditions – e.g.
case int:
^
SyntaxError: name capture 'int' makes remaining patterns unreachable
This might become a common error because of our experience with isinstance
where we supply the type e.g. isinstance(x, int)
. Remember:
case Patterns have to be Patterns
Instead, using the example of integer patterns, we need case int():
, or, if we want to “capture” the value into, say, x
, case int(x):
.
Guards and Traditional Switch Statements
It is very common in switch / case when statements to have conditions. Sometimes it is the whole point of the construct – we supply one value and respond differently according to its values / attributes. E.g. in rough pseudocode if temp < 0 freezing, if > 100 boiling, otherwise normal. In Pattern Matching value conditions are secondary. We match on a pattern first and then, perhaps evaluating the unpacked variables in an expression, we apply a guard condition.
case float(x) if abs(x) < 100:
...
case float(x) if abs(x) < 200:
etc
Depending on how it’s used we could think of “Pattern Matching” as “Pattern and Guard Condition Matching”.
The most similar to a classic switch construct would be:
match:
case val if val < 10:
...
case val if val < 20:
...
etc
One final thought: there seems to be nothing special about the “default” option (using switch language) – namely, case _:
. It merely captures anything that hasn’t already been matched and puts _
as the name i.e. it is a throwaway variable. We could capture and use that value with a normal variable name although that is optional because there’s nothing stopping us from referencing the original name fed into match. But, for example, case mopup:
would work.
How to play with it on Linux
Make image using Dockerfile e.g. the following based on Install Python3 in Ubuntu Docker (I added vim and a newer Ubuntu image plus changed apt-get to apt (even though it allegedly has an unstable cli interface):
FROM ubuntu:20.04
RUN apt update && apt install -y software-properties-common gcc && \
add-apt-repository -y ppa:deadsnakes/ppa
RUN apt update && apt install -y python3.10 python3-distutils python3-pip python3-apt vim
docker build --tag pyexp .
(don’t forget the dot at the end – that’s a reference to the path to find Dockerfile)
Then make container:
docker create --name pyexp_cont pyexp
and run it with access to bash command line
docker container run -it pyexp /bin/bash
Useful Links
Pattern matching tutorial for Pythonic code | Pydon’t
Python 3.10 Pattern Matching in Action
PEP 622