How to do structural pattern matching in Python 3.10 with a type to match?
Asked Answered
S

4

14

I am trying to match a type in Python 3.10 using the console:

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

And I get this error:

  File "<stdin>", line 2
SyntaxError: name capture 'int' makes remaining patterns unreachable

How can I fix this issue?

Swear answered 22/1, 2022 at 17:9 Comment(0)
A
7

First, let's explain the code in the question:

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

In Python, the match statement operates by trying to fit the subject (the value of type(t), i.e. float) into one of the patterns.

The above code has two patterns (case int: and case float:). These patterns, by nature of their syntax, are capture patterns, meaning that they attempt to capture the subject (the value of type(t), i.e. float) into the variable name specified in each pattern (the names int and float, respectively).

The patterns case int: and case float: are identical to each other. Moreover, they will both match any subject. Hence, the error: SyntaxError: name capture 'int' makes remaining patterns unreachable.

Additionally, note that these two patterns are functionally identical to the conventional default pattern of case _:. case _: is also simply a capture pattern, nothing more. (The only difference between case int:, case float: and case _: is the name of the variable that will capture the subject.)

Now let's explain the code in @mmohaveri's answer.

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

The above match statement will attempt to fit the value 12.0 first into the pattern case int():, and then into the pattern case float():.

These patterns, by nature of their syntax, are class patterns. The parentheses can be used to specify positional arguments and keyword arguments for the pattern. (The parentheses are not function calls.)

For the first pattern (case int():), 12.0 is not an instance of int, so the pattern match fails.

For the second pattern, (case float():), 12.0 is an instance of float, and no positional or keyword arguments are specified inside the parentheses, so the pattern match succeeds.

Here is more information about the matching process.

Anathema answered 22/12, 2023 at 7:14 Comment(1)
This is a great explanation. I thought pattern matching was about execution switching. But it is about destructuring.Swear
K
15

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

Karinkarina answered 15/4, 2023 at 19:49 Comment(2)
In the comment for your case int, you wrote tt = int. But wouldn't it be int = tt?Anathema
You're correct, it assigns to int, my comment was backwards. I've edited the post to fix the error, thanks.Karinkarina
T
14

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.

Trieste answered 26/4, 2022 at 16:0 Comment(3)
It's odd that calling the type works instead of asking for the type in the match clause and caseing against the possibilities. It would be interesting to have some further information about why this works instead of the approach of most languages, as in the question. Nevertheless, it works. Thanks!Ellingston
@Ellingston My understanding is that other languages mainly have switch-case structure, which is a fancy if-else. But what python's structural pattern matching is offering is far more powerful, it allows you to select a branch of code based on the structure of the object you're comparing. So here, you're not "saying if type of t is int", you're saying "if you can put t in an int container". But take my word with a grain of salt as that's just my understanding from different things that I've read regarding this new feature and I'm in no means an expert in this area.Trieste
The PEP for Structural Pattern Matching is a great resource for seeing why this is the case. The type isn't "called" (though the syntax would make you think so), but rather it's set up for destructuring the object. See the PEP peps.python.org/pep-0622/#matching-process, where case (x, y) is used for tuple unpacking/destructuring, and you can imagine the same can be done with any type of data class.Messily
A
7

First, let's explain the code in the question:

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

In Python, the match statement operates by trying to fit the subject (the value of type(t), i.e. float) into one of the patterns.

The above code has two patterns (case int: and case float:). These patterns, by nature of their syntax, are capture patterns, meaning that they attempt to capture the subject (the value of type(t), i.e. float) into the variable name specified in each pattern (the names int and float, respectively).

The patterns case int: and case float: are identical to each other. Moreover, they will both match any subject. Hence, the error: SyntaxError: name capture 'int' makes remaining patterns unreachable.

Additionally, note that these two patterns are functionally identical to the conventional default pattern of case _:. case _: is also simply a capture pattern, nothing more. (The only difference between case int:, case float: and case _: is the name of the variable that will capture the subject.)

Now let's explain the code in @mmohaveri's answer.

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

The above match statement will attempt to fit the value 12.0 first into the pattern case int():, and then into the pattern case float():.

These patterns, by nature of their syntax, are class patterns. The parentheses can be used to specify positional arguments and keyword arguments for the pattern. (The parentheses are not function calls.)

For the first pattern (case int():), 12.0 is not an instance of int, so the pattern match fails.

For the second pattern, (case float():), 12.0 is an instance of float, and no positional or keyword arguments are specified inside the parentheses, so the pattern match succeeds.

Here is more information about the matching process.

Anathema answered 22/12, 2023 at 7:14 Comment(1)
This is a great explanation. I thought pattern matching was about execution switching. But it is about destructuring.Swear
S
2

My fix was:

match type(t):
  case v if v is int:
    print("int")
  case v if v is float:
    print("float")

It is not very elegant but it is the best I can do with pattern matching.

Swear answered 22/1, 2022 at 22:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.