Example converted to pattern matching

Here's the equivalent code using match and case:

match x:
    case int():
        pass
    case str():
        x = int(x)
    case float() | Decimal():
        x = round(x)
    case _:
        raise TypeError('Unsupported type')

Explanation

PEP 634, available since Python 3.10, specifies that isinstance() checks are performed with class patterns. To check for an instance of str, write case str(): .... Note that the parentheses are essential. That is how the grammar determines that this is a class pattern.

To check multiple classes at a time, PEP 634 provides an or-pattern using the | operator. For example, to check whether an object is an instance of float or Decimal, write case float() | Decimal(): .... As before, the parentheses are essential.

Answer from Raymond Hettinger on Stack Overflow
🌐
Python
peps.python.org › pep-0622
PEP 622 – Structural Pattern Matching | peps.python.org
The pattern operator | separates alternative patterns (not unlike regular expressions or EBNF grammars), and _ is a wildcard. (Note that the match syntax used here will accept user-defined sequences, as well as lists and tuples.) In some occasions, extraction of information is not as relevant as identifying structure. Take the following example from the Python standard library: def is_tuple(node): if isinstance(node, Node) and node.children == [LParen(), RParen()]: return True return (isinstance(node, Node) and len(node.children) == 3 and isinstance(node.children[0], Leaf) and isinstance(node.children[1], Node) and isinstance(node.children[2], Leaf) and node.children[0].value == "(" and node.children[2].value == ")")
🌐
Plain English Westminster
benhoyt.com › writings › python-pattern-matching
Structural pattern matching in Python 3.10
Originally I was also concerned that match’s class patterns don’t play well with Python’s use of duck typing, where you just access attributes and call methods on an object, without checking its type first (for example, when using file-like objects). With class patterns, however, you specify the type, and it performs an isinstance check.
🌐
Python
peps.python.org › pep-0636
PEP 636 – Structural Pattern Matching: Tutorial | peps.python.org
Rather than writing multiple isinstance() checks, you can use patterns to recognize different kinds of objects, and also apply patterns to its attributes: match event.get(): case Click(position=(x, y)): handle_click_at(x, y) case KeyPress(k...
🌐
Hillel Wayne
hillelwayne.com › post › python-abc
Crimes with Python's Pattern Matching
July 31, 2022 - class PalindromicName(ABC): @classmethod def __subclasshook__(cls, C): name = C.__name__.lower() return name[::-1] == name class Abba: ... class Baba: ... >>> isinstance(Abba(), PalindromicName) True >>> isinstance(Baba(), PalindromicName) False · You can do some weird stuff with this. Back in 2019 I used it to create non-monotonic types, where something counts as a NotIterable if it doesn’t have the __iter__ method. There wasn’t anything too diabolical you could do with this: nothing in Python really interacted with ABCs, limiting the damage you could do with production code. Then Python 3.10 added pattern matching.
🌐
Python
peps.python.org › pep-0635
PEP 635 – Structural Pattern Matching: Motivation and Rationale | peps.python.org
In Python, we simply use isinstance() together with the __match_args__ field of a class to check whether an object has the correct structure and then transform some of its attributes into a tuple.
🌐
Better Stack
betterstack.com › community › guides › scaling-python › python-pattern-matching
Structural Pattern Matching in Python: A Comprehensive Guide | Better Stack Community
python3 app.py · You'll see: Output · === Type Matching === An integer: 42 A string of length 5 A list with 3 items A dictionary with 2 keys Nothing to describe Something else: float · Without pattern matching, you'd need to write a series of isinstance() checks to achieve the same result: Copied!
Find elsewhere
🌐
Inspired Python
inspiredpython.com › course › pattern-matching › mastering-structural-pattern-matching
Mastering Structural Pattern Matching • Inspired Python
>>> assert True == 1 >>> assert isinstance(True, bool) >>> assert isinstance(True, int) True and False are both bool and int, thus True == 1 and, in the literal pattern example above, the case True clause would never run as case 1 matches it first!
🌐
Grant Jenks
grantjenks.com › docs › patternmatching › reference.html
Python Pattern Matching Reference — Pattern Matching 3.0.1 documentation
def set_predicate(matcher, value, pattern): return isinstance(pattern, Set) def set_action(matcher, value, pattern): value_sequence = tuple(value) for permutation in itertools.permutations(pattern): try: matcher.names.push() matcher.visit(value_sequence, permutation) matcher.names.pull() return except Mismatch: matcher.names.undo() else: raise Mismatch
🌐
GitHub
github.com › microsoft › pylance-release › issues › 1221
Type narrowing with ``isinstance`` and ``issubclass`` in match statement · Issue #1221 · microsoft/pylance-release
May 2, 2021 - Python version (and distribution if applicable, e.g. Anaconda): 3.10.0a7 ... An isinstance or issubclass call in a match statement should narrow the type just like the same call in an if statement.
Published   May 02, 2021
🌐
Buddy
buddy.works › home › tutorials › languages › structural pattern matching in python
Structural Pattern Matching In Python | Tutorials
April 28, 2021 - Any class can be matched, even the built-in classes. Class patterns fulfill two purposes: checking whether a given subject is indeed an instance of a specific class, and extracting data from specific attributes of the subject. Consider the function below that sums two integers. We only want to perform the addition if both numbers are of type int. pythondef sum_two_integers(num1, num2): if isinstance(num1, int) and isinstance(num2, int): return num1 + num2
🌐
ArjanCodes
arjancodes.com › blog › how-to-use-structural-pattern-matching-in-python
Introduction to Structural Pattern Matching in Python | ArjanCodes
June 20, 2024 - If you’ve spent any time with isinstance and hasattr, you know the pain. These methods work, but they’re often clunky and cumbersome. What if there was a more elegant solution that not only simplifies your code but also makes it more readable? The answer is Structural Pattern Matching (SPM).
🌐
Reddit
reddit.com › r/python › pep 622 -- structural pattern matching
r/Python on Reddit: PEP 622 -- Structural Pattern Matching
June 23, 2020 - Yea, it will call a __match__ method on the given type (which by default is if isinstance(instance, cls): return instance So assuming, you wrote your implementation in a duck-typish way, you could have it match whatever you wanted. ... You might be able to use a Protocol to achieve this, but why not just use inheritance to declare your is-a relationship? ... The PEP mentions isinstance() usage as one of the motivations. To me isinstance => duck typing isn't good enough in that situation. ... Given that Python is well known for duck typing is good cause to keep it in mind.
🌐
Python Morsels
pythonmorsels.com › match-case-parsing-python
Appreciating Python's match-case by parsing Python code - Python Morsels
June 22, 2022 - You end up in a scary land full of isinstance checking · You're matching lists/tuples by their size and contents · You're pattern matching against dictionary keys and values · Though in all likelihood, you don't need match-case and your code would likely be simpler without it. Python's structural pattern matching definitely makes parsing Python code much easier and I'm grateful I thought to use it when creating my undataclass tool.
Top answer
1 of 4
22

This is a common "gotcha" of the new syntax: case clauses are not expressions. That is, if you put a variable name in a case clause, the syntax assigns to that name rather than reading that name.

It's a common misconception to think of match as like switch in other languages: it is not, not even really close. switch cases are expressions which test for equality against the switch expression; conversely, match cases are structured patterns which unpack the match expression. It's really much more akin to generalized iterable unpacking. It asks the question: "does the structure of the match expression look like the structure of the case clause?", a very different question from what a switch statement asks.

For example:

t = 12.0
match t:
    case newvar: # This is equal to `newvar = t`
        print(f"bound a new variable called newvar: {newvar}")
        # prints "bound a new variable called newvar: 12.00000000"
        # this pattern matches anything at all, so all following cases never run

    case 13.0:
        print("found 13.0")

    case [a, b, c]: # matches an iterable with exactly 3 elements,
        # and *assigns* those elements to the variables `a`, `b` and `c`
        print(f"found an iterable of length exactly 3.")
        print(f"these are the values in the iterable: {a} {b} {c}")

    case [*_]:
        print("found some sort of iterable, but it's definitely")
        print("not of length 3, because that already matched earlier")

    case my_fancy_type(): # match statement magic: this is how to type check!
        print(f"variable t = {t} is of type {my_fancy_type}")

    case _:
        print("no match")

So what your OP actually does is kinda like this:

t = 12.0
tt = type(t) # float obviously
match tt:

    case int: # assigns to int! `int = tt`, overwriting the builtin
       print(f"the value of int: {int}")
       # output: "the value of int: <class 'float'>"
       print(int == float) # output: True (!!!!!!!!)
       # In order to get the original builtin type, you'd have to do
       # something like `from builtins import int as int2`

    case float: # assigns to float, in this case the no-op `float = float`
        # in fact this clause is identical to the previous clause:
        # match anything and bind the match to its new name
        print(f"match anything and bind it to name 'float': {float}")
        # never prints, because we already matched the first case

    case float(): # since this isn't a variable name, no assignment happens.
        # under the hood, this equates to an `isinstance` check. 
        # `float` is not an instance of itself, so this wouldn't match.
        print(f"tt: {tt} is an instance of float") # never prints
        # of course, this case never executes anyways because the
        # first case matches anything, skipping all following cases

Frankly, I'm not entirely sure how the under-the-hood instance check works, but it definitely works like the other answer says: by defintion of the match syntax, type checks are done like this:

match instance:
    case type():
        print(f"object {instance} is of type {type}!")

So we come back to where we started: case clauses are not expressions. As the PEP says, it's better to think of case clauses as kind of like function declarations, where we name the arguments to the function and possibly bind some default values to those newly-named arguments. But we never, ever read existing variables in case clauses, only make new variables. (There's some other subtleties involved as well, for instance a dotted access doesn't count as a "variable" for this purpose, but this is complicated already, best to end this answer here.)

2 of 4
17

Lose the type() and also add parentheses to your types:

t = 12.0
match t:
  case int():
    print("int")
  case float():
    print("float")

I'm not sure why what you've wrote is not working, but this one works.

🌐
Real Python
realpython.com › structural-pattern-matching
Structural Pattern Matching in Python – Real Python
August 6, 2024 - Note: Because pattern matching does two things at once, the Python interpreter can take advantage of this to optimize the underlying bytecode with specialized opcodes, making the code run slightly faster. The brief code snippet above merely scratches the surface of what you can achieve with pattern matching, but it already shows you its expressiveness, especially when you compare it with the traditional if...elif... statements and isinstance() checks.
🌐
Tobiaskohn
tobiaskohn.ch › index.php › 2018 › 09 › 18 › pattern-matching-syntax-in-python
On the Syntax of Pattern Matching in Python – Tobias Kohn
However, after having written enough isinstance, hasattr, and getattr calls, along with deeply nested conditions, I decided to rewrite the same code using pattern matching, which ended up looking like this:
🌐
GitHub
github.com › python › cpython › issues › 106246
[match-case] Allow matching Union types · Issue #106246 · python/cpython
May 21, 2023 - Since python 3.10, isinstance and issubclass allow testing against Union-types (PEP604). However, the same is not true for match-case. case klass(): should probably be 1:1 equivalent with isinstance(x, klass):. from typing import TypeAlias Numeric: TypeAlias = int | float assert isinstance(1.2, Numeric) # ✔ match 1.2: case Numeric(): # TypeError: called match pattern must be a type pass ·
Published   Jun 29, 2023
Top answer
1 of 7
29

Update

I condensed this answer into a python package to make matching as easy as pip install regex-spm,

import regex_spm

match regex_spm.fullmatch_in("abracadabra"):
  case r"\d+": print("It's all digits")
  case r"\D+": print("There are no digits in the search string")
  case _: print("It's something else")

Original answer

As Patrick Artner correctly points out in the other answer, there is currently no official way to do this. Hopefully the feature will be introduced in a future Python version and this question can be retired. Until then:

PEP 634 specifies that Structural Pattern Matching uses the == operator for evaluating a match. We can override that.

import re
from dataclasses import dataclass

# noinspection PyPep8Naming
@dataclass
class regex_in:
    string: str

    def __eq__(self, other: str | re.Pattern):
        if isinstance(other, str):
            other = re.compile(other)
        assert isinstance(other, re.Pattern)
        # TODO extend for search and match variants
        return other.fullmatch(self.string) is not None

Now you can do something like:

match regex_in(validated_string):
    case r'\d+':
        print('Digits')
    case r'\s+':
        print('Whitespaces')
    case _:
        print('Something else')

Caveat #1 is that you can't pass re.compile'd patterns to the case directly, because then Python wants to match based on class. You have to save the pattern somewhere first.

Caveat #2 is that you can't actually use local variables either, because Python then interprets it as a name for capturing the match subject. You need to use a dotted name, e.g. putting the pattern into a class or enum:

class MyPatterns:
    DIGITS = re.compile('\d+')

match regex_in(validated_string):
    case MyPatterns.DIGITS:
        print('This works, it\'s all digits')

Groups

This could be extended even further to provide an easy way to access the re.Match object and the groups.

# noinspection PyPep8Naming
@dataclass
class regex_in:
    string: str
    match: re.Match = None

    def __eq__(self, other: str | re.Pattern):
        if isinstance(other, str):
            other = re.compile(other)
        assert isinstance(other, re.Pattern)
        # TODO extend for search and match variants
        self.match = other.fullmatch(self.string)
        return self.match is not None

    def __getitem__(self, group):
        return self.match[group]

# Note the `as m` in in the case specification
match regex_in(validated_string):
    case r'\d(\d)' as m:
        print(f'The second digit is {m[1]}')
        print(f'The whole match is {m.match}')
2 of 7
23

Clean solution

There is a clean solution to this problem. Just hoist the regexes out of the case-clauses where they aren't supported and into the match-clause which supports any Python object.

The combined regex will also give you better efficiency than could be had by having a series of separate regex tests. Also, the regex can be precompiled for maximum efficiency during the match process.

Example

Here's a worked out example for a simple tokenizer:

pattern = re.compile(r'(\d+\.\d+)|(\d+)|(\w+)|(".*)"')
Token = namedtuple('Token', ('kind', 'value', 'position'))
env = {'x': 'hello', 'y': 10}

for s in ['123', '123.45', 'x', 'y', '"goodbye"']:
    mo = pattern.fullmatch(s)
    match mo.lastindex:
        case 1:
            tok = Token('NUM', float(s), mo.span())
        case 2:
            tok = Token('NUM', int(s), mo.span())
        case 3:
            tok = Token('VAR', env[s], mo.span())
        case 4:
            tok = Token('TEXT', s[1:-1], mo.span())
        case _:
            raise ValueError(f'Unknown pattern for {s!r}')
    print(tok) 

This outputs:

Token(kind='NUM', value=123, position=(0, 3))
Token(kind='NUM', value=123.45, position=(0, 6))
Token(kind='VAR', value='hello', position=(0, 1))
Token(kind='VAR', value=10, position=(0, 1))
Token(kind='TEXT', value='goodbye', position=(0, 9))

Better Example

The code can be improved by writing the combined regex in verbose format for intelligibility and ease of adding more cases. It can be further improved by naming the regex sub patterns:

pattern = re.compile(r"""(?x)
    (?P<float>\d+\.\d+) |
    (?P<int>\d+) |
    (?P<variable>\w+) |
    (?P<string>".*")
""")

That can be used in a match/case statement like this:

for s in ['123', '123.45', 'x', 'y', '"goodbye"']:
    mo = pattern.fullmatch(s)
    match mo.lastgroup:
        case 'float':
            tok = Token('NUM', float(s), mo.span())
        case 'int':
            tok = Token('NUM', int(s), mo.span())
        case 'variable':
            tok = Token('VAR', env[s], mo.span())
        case 'string':
            tok = Token('TEXT', s[1:-1], mo.span())
        case _:
            raise ValueError(f'Unknown pattern for {s!r}')
    print(tok)

Comparison to if/elif/else

Here is the equivalent code written using an if-elif-else chain:

for s in ['123', '123.45', 'x', 'y', '"goodbye"']:
    if (mo := re.fullmatch('\d+\.\d+', s)):
        tok = Token('NUM', float(s), mo.span())
    elif (mo := re.fullmatch('\d+', s)):
        tok = Token('NUM', int(s), mo.span())
    elif (mo := re.fullmatch('\w+', s)):
        tok = Token('VAR', env[s], mo.span())
    elif (mo := re.fullmatch('".*"', s)):
        tok = Token('TEXT', s[1:-1], mo.span())
    else:
        raise ValueError(f'Unknown pattern for {s!r}')
    print(tok)

Compared to the match/case, the if-elif-else chain is slower because it runs multiple regex matches and because there is no precompilation. Also, it is less maintainable without the case names.

Because all the regexes are separate we have to capture all the match objects separately with repeated use of assignment expressions with the walrus operator. This is awkward compared to the match/case example where we only make a single assignment.