27

I want to convert this existing code to use pattern matching:

if isinstance(x, int):
    pass
elif isinstance(x, str):
    x = int(x)
elif isinstance(x, (float, Decimal)):
    x = round(x)
else:
    raise TypeError('Unsupported type')

How do you write isinstance checks with pattern matching, and how do you test against multiple possible types like (float, Decimal) at the same time?

Nathan
  • 9,651
  • 4
  • 45
  • 65
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485

2 Answers2

43

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

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • 1
    Also see the [pattern matching tutorial](https://peps.python.org/pep-0636/#adding-a-ui-matching-objects) for more advanced `isinstance` checks which include constraints on properties of the class. – Nathan Jun 17 '22 at 13:46
  • Unfortunately this pattern is bit prone to [breaking exhaustiveness checking](https://mypy-play.net/?mypy=latest&python=3.11&gist=cc8fa2867a9f2990a768ffd708782a99), i.e., when accidentally forgetting a `()` the `assert_never` suddenly loses its functionality! In this regard a traditional chain of `if instance(): ... elif isinstance(): ... else: assert_never()` is less error prone. – bluenote10 Aug 04 '23 at 07:11
  • @bluenote10 That makes no sense. In `case _: raise TypeError`, it is trivial to substitute `case _: assert_never()`, error logging, or anything else that you would normally use in an else-clause. – Raymond Hettinger Aug 04 '23 at 13:41
  • @RaymondHettinger I know. I was just trying to say that forgetting the parenthesis is dangerous, because it will create an irrefutable pattern, i.e., it will accidentally match everything, and therefore `assert_never` loses its capability of performing exhaustiveness checks. Clearly, my latter example has a bug, but there is no (obvious?) way for the type checker to identify a bug here, because it cannot guess if the broad capture was an accident. Perhaps a linter can avoid such issues, if it forbids irrefutable patterns. – bluenote10 Aug 04 '23 at 14:30
2

Using python match case

Without Exception handling

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

Some extras

There are still some flows in this code.

With exception handling

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

As a function

def func(x):
    match x:
        case int():
            pass
        case str():
            try:
                x = int(x)
            except ValueError:
                raise TypeError('Unsupported type')
        case float() | Decimal():
            x = round(x)
        case _:
            raise TypeError('Unsupported type')
    return x
Kavindu Ravishka
  • 711
  • 4
  • 11