When I switched from Scala to Python, the most annoying thing was the lack of pattern matching.
Table of Contents
- Constant matching
- Sequence matching
- Matching object fields
- Matching object fields without dataclasses
- Pattern guards
- Mapping patterns
- Matching alternatives
- No matching found
- The hardest thing to remember
In Scala, we tend to solve multiple problems using the matching operator because such notation makes it easier to work with types such as Option
, Either
, or Try
when we must handle both possible values.
Thankfully, since Python 3.10, we finally have structural pattern matching, which works almost the same as the Scala implementation! Let’s take a look at the similarities and differences.
Constant matching
In both languages, we can use pattern matching to verify whether a variable has the specified value. Of course, Python doesn’t have strict typing. Therefore, we can use values of different types, and the automatic type conversion sometimes gets in the way.
In Scala, I could write the following code to check the value of the x
variable:
x match {
case 0 => "zero"
case true => "true"
case "hello" => "'hello'"
case _ => "whatever"
}
The equivalent Python code looks like this:
match x:
case 0: print("zero")
case True: print("true")
case "hello": print("'hello'")
case _: print("whatever")
Beware of a trap!!! If the x
variable has the value False
, 0
will get matched. To solve the problem, we must verify the variable type using a guard expression. I will show the notation in one of the following examples.
Sequence matching
Scala offers a few ways to match sequence content. For example, we can check the exact length of a sequence and bind the variables by element position:
val ints = Seq(1, 2)
ints match {
case Seq() => "The Seq is empty!"
case Seq(first) => s"The seq has one element : $first"
case Seq(first, second) => s"The seq has two elements : $first, $second"
}
To achieve the same result in Python, we need very similar code:
ints = [1, 2]
match ints:
case []: print("The Seq is empty!")
case [first]: print(f"The seq has one element: {first}")
case [first, second]: print(f"The seq has two elements: {first}, {second}")
Of course, a common use case of retrieving the first value from a long sequence is also trivial to implement. Let’s start with the Scala code:
seq match {
case Seq(first, rest@_*) => s"First: $first, rest: $rest"
}
and here is the Python code:
match ints:
case [first, *rest]: print(f"First: {first}, rest: {rest}")
Matching object fields
Using pattern matching to match constants and sequence elements would be pretty useless. In Scala, we can use it also to match the fields (and their values) of case classes.
case class Person(firstName: String, lastName: String)
author = Person("Bartosz", "Mikulski")
author match {
case Person("Bartosz", last_name) => s"$last_name"
}
To get something similar to a Scala case class in Python, we have to use dataclasses and define the following class:
from dataclasses import dataclass
@dataclass
class Person:
first_name: str
last_name: str
Now, we can instantiate it and try matching the content:
author = Person("Bartosz", "Mikulski")
match author:
case Person("Bartosz", last_name): print(f"Hi, Bartosz {last_name}")
Matching object fields without dataclasses
What if we had a standard Python class instead of a dataclass? Would it work?
class NotADataClass:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
another_author = NotADataClass("Bartosz", "Mikulski")
match another_author:
case NotADataClass("Bartosz", _): print("Hi, Bartosz")
Unfortunately, such notation isn’t supported, and when we try running the code, we will get an error: TypeError: NotADataClass() accepts 0 positional sub-patterns (2 given)
.
However, if we can modify the class definition by adding a special __match_args__
field to tell Python which arguments to use during pattern matching:
class NotADataClass:
__match_args__ = ("first_name", "last_name")
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
another_author = NotADataClass("Bartosz", "Mikulski")
match another_author:
case NotADataClass("Bartosz", _): print("Hi, Bartosz")
Of course, if we want to match a class imported from someone else’s library, the only thing we can do is wait until they include the __match_args__
in their code, or we can try patching the class definition:
NotADataClass.__match_args__ = ("first_name", "last_name")
It will work too. Nevertheless, such patching is quite error-prone, and I don’t recommend doing it.
Pattern guards
Similarly to Scala, we can use the if
statement in the pattern definition to limit the matching:
match another_author:
case NotADataClass(first_name, _) if first_name == "Bartosz": print("Hi, Bartosz")
The condition looks precisely like its Scala equivalent, so let’s move on ;)
Get Weekly AI Implementation Insights
Join engineering leaders who receive my analysis of common AI production failures and how to prevent them. No fluff, just actionable techniques.
Mapping patterns
Now, let’s take a look at something that doesn’t exist in Scala (or at least it didn’t exist in the last version I was using). We can use pattern matching in Python to check whether a dictionary contains a specified key and value.
dict_matching = {
"first_name": "Bartosz",
"last_name": "Mikulski"
}
match dict_matching:
case {"first_name": "Bartosz"}: print("Hi, Bartosz")
We can also bind the values to a variable, but it isn’t allowed to bind the keys. The key must always be literal.
match dict_matching:
case {"first_name": "Bartosz", "last_name": last_name}: print(f"Hi, Bartosz {last_name}")
Matching alternatives
In Scala, we can use pattern alternatives too. The most common usecase is to match multiple exception classes:
case _: RuntimeException | _: IOException => ""
Python gives us a similar pattern alternatives notation:
match dict_matching:
case {"first_name": ""} | {"first_name": "Bartosz"}: print("HI!")
No matching found
The most shocking difference between Python and Scala is how the pattern matching handles match statements that didn’t match anything.
In Scala, the following match expression throws a scala.MatchError
error:
val x = 0
x match {
case 1 => "one"
case 2 => "two"
}
The equivalent Python expression will… do nothing. In Python, pattern matching silently ignores situations when it fails to match anything.
The hardest thing to remember
As we see, the Python notation is almost identical to Scala pattern matching. For me, the biggest problem is remembering to write match
BEFORE the variable name. After all, in Scala, we do it the other way around.