A Match Made in Heaven: Structural Pattern Matching in Python

Bertrand Chardon
Inside Doctrine
Published in
6 min readMar 31, 2023

In October of 2020, python 3.9 was released. It was an important event, with two important PEPs tipping the scale for the language:

  • the adoption of an annual release cycle through PEP 602, with a new version coming out every 12 months in October, accelerating the iterations on the language’s features and capabilities
  • the adoption of a new PEG-based parser for the language through PEP 617, which promised to “lift the LL(1) restriction on the current Python grammar”, allowing for a richer set of features for end users in the subsequent releases

A year later, in October of 2021, right on cue, version 3.10 of python was released, which featured a boon of type hinting improvements (PEP 604, PEP 612 and PEP 613) and multiple PEPs centered around a brand-new feature of the language made possible by the improvements we mentioned as part of the 3.9 release: structural pattern matching (PEP 634, PEP 635 and PEP 636).

In this article, we’ll try our best to communicate just how big of a deal that is, what the feature has to offer and how this seemingly inconsequential change can go towards changing your whole approach to python programming in the future.

Structural… pattern… matching???

First of all, let’s begin with the obvious question: what exactly is structural pattern matching? There are three words in there, so the concept must be quite complicated, right? Well… Turns out it’s not really… It’s actually something you’ve being doing for quite some time…

the classic “pegs into holes” kiddie game, a wooden box with holes having different shapes and different colored pegs, some of them already fitted into some of the holes
structural pattern matching, early days

Of course there’s more to this, but if you should take ONE THING away from this article, it’s that structural pattern matching really boils down to matching the shapes of objects and their associated structure to known patterns.

While this seems simple, it is a powerful concept, in that it allows developers to tailor behaviour to what the objects look like instead of tailoring behaviour to their internal state and values.

Also, a quick note: even though we’ll be focusing on python here, it should be noted that the feature wasn’t created out of thin air a year and half ago and has existed in the wild in other languages (such as Objective Caml, Rust or Scala to name a few) for quite some time now.

There are even proposals to bring the feature to ECMAScript in the future.

At this point, you should be painfully aware that this article has yet to provide a single line of code, so here goes:

Let’s zoom in on the analyze_person function where all the fun happens:

It takes an instance of Person as an argument and uses structural pattern matching through the matchcase construct on that object (going back to the analogy exhibited in the image right above, the match is the peg the baby is holding in his hand, the cases are the different holes he’s trying to make it fit into.)

The different cases are called the “arms” of the match and they’re evaluated one after another. Once an arm matches, the other ones are not evaluated (the patterns are not “fall-through”).

Also and this is extremely powerful, each case allows the developer to destructure the object being matched, as exhibited by case Person(age=20, height=h, name=n), which really means:

if the person passed as a parameter is 20, then destructure it and bind its height and name to components of the pattern (here, h and n)

As a result the logic of the function goes as follows:

  • in the first arm: if the person is 20, destructure it and capture its name (in variable n), its height (in variable h)
  • in the second arm, the logic is more or less the same but we’re making use of another feature of the construct, guards, which allows us to match conditionally (here, if and only if the person is more than 200cm tall)
  • in the third and final arm, we’re using _ (called the wildcard pattern) to match everything without capture, which serves as a catch-all for the match statement

Now that we’ve shown what structural pattern matching looks like, we should take time to reflect on what it is, what it is not and how you could make use of it next.

First, for the more practical: structural pattern matching is a convenient way to recognise objects based on shape and capabilities but most importantly, it allows developers to navigate those shapes in depth to programmatically react to matches.

Note, and this bears repeating, that structural pattern matching is not python’s version of the switch...case statement, nor is it a switch...case statement “on steroids”.

Structural pattern matching is different in that it is not intended to deal with “values” through “exact match” like switch...case (though it can), but rather rather to deal with “shapes” and be capable of “partial matches”.

As such, it can express everything a switch...case statement can and then some.

What can be matched?

Structural pattern matching can match any kind of object and it can do so in many different ways:

  • Literal matching (i.e. matching a string, an integer, a float, a bool etc)
  • Sequence matching (i.e. matching things such as lists, sets, tuples)
  • Mapping matching (i.e. matching things such as dicts, etc)
  • Class matching ((i.e. matching the shapes of instances of classes)

Customising matches

Note that matching works for any user-defined class and that the matching behaviour itself is customisable through the user-provided definition of class attribute __match_args__ which allows developers to specify which fields should be used when matching positional arguments in class patterns (see PEP 634 for reference)

A quick example:

When should structural pattern matching be used?

When the strategy to use when processing objects depends on inner characteristics (or “structure”) of objects rather than on their precise type or values.

This is something that we’ve somewhat been able to do in python since 3.8 and the introduction of structural subtyping through protocols, but while structural subtyping is all about being able to deal with objects that present the same façade to the outside world (i.e. they adhere to a common interface) in a uniform way, structural pattern matching is all about being able to interact with groups of objects that present the same shape or inner configuration in a uniform way.

On the one hand you have “all the objects on which you can call method drive()" and the other hand you have the subtly different “all the objects with a metal hood, a combustion engine and four wheels”.

Those two powerful way of dealing with categories of objects are not orthogonal and can be conveniently combined in your designs to create extremely rich, powerful and highly reusable blocks of logic.

Another common use case for structural pattern matching is nested class-based properties matching where you would have had to rely heavily on nested isinstance calls, such as:

This is more elegantly expressed using structural pattern matching as:

Note that the deeper the nesting goes, the more structural pattern matching will shine in its expressiveness, so any kind of task involving processing nested structures such as (but not restricted to) AST / CST manipulation, JSON / XML parsing, running algorithms on trees, etc, though it should be noted that the power of structural pattern matching goes beyond those use cases.

To further illustrate the capabilities it offers, let’s consider a different way of handling errors in a python program (à la rust or golang):

Wrapping up

As we have seen, structural pattern matching is a great and expressive addition to the python language, which allows us to look at the data we manipulate daily under a different light and allows for extremely convenient and elegant designs.

It is, incidentally, a good example of cross-pollination between different worlds (in this case, the influence of functional programming on the python language), something that takes the cream of the crop from the different paradigms and allows us to look at problems from different and often better vantages.

Other such constructs might end up being added to the core of the language in the future, making it more expressive and powerful along the way.

After going through this article, I hope you leave with a better understanding of the feature itself and the affordances it grants developers in a modern codebase and feel comfortable making good use of it for your next project.

--

--