Pattern matching in Python vs Scala

When I switched from Scala to Python, the most annoying thing was the lack of pattern matching.

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:

1
2
3
4
5
6
x match {
    case 0 => "zero"
    case true => "true"
    case "hello" => "'hello'"
    case _ => "whatever"
}

The equivalent Python code looks like this:

1
2
3
4
5
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
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:

1
2
3
seq match {
    case Seq(first, rest@_*) => s"First: $first, rest: $rest"
}

and here is the Python code:

1
2
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.

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str

Now, we can instantiate it and try matching the content:

1
2
3
4
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?

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
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:

1
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:

1
2
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 ;)


Subscribe to the newsletter and join the free email course.

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.

1
2
3
4
5
6
7
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.

1
2
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:

1
case _: RuntimeException | _: IOException => ""

Python gives us a similar pattern alternatives notation:

1
2
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:

1
2
3
4
5
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.


Remember to share on social media!
If you like this text, please share it on Facebook/Twitter/LinkedIn/Reddit or other social media.

If you want to contact me, send me a message on LinkedIn or Twitter.


Bartosz Mikulski
Bartosz Mikulski * MLOps Engineer / data engineer * conference speaker * co-founder of Software Craft Poznan & Poznan Scala User Group

Subscribe to the newsletter and get access to my free email course on building trustworthy data pipelines.