How can we use Python's structural pattern matching (introduced in 3.10) to match the type of a variable without invoking the constructor / in a case where a new instantiation of the class is not easily possible?
The following code fails:
from pydantic import BaseModel
# instantiation requires 'name' to be defined
class Tree(BaseModel):
name: str
my_plant = Tree(name='oak')
match type(my_plant):
case Tree:
print('My plant is a tree.')
case _:
print('error')
with error message SyntaxError: name capture 'Tree' makes remaining patterns unreachable
An alternative attempt was to re-create an instance during matching (dangerous because of instantiation during matching, but worth a shot...) - it also fails:
match type(my_plant):
case type(Tree()):
print('My plant is a tree.')
case _:
print('error')
TypeError: type() accepts 0 positional sub-patterns (1 given)
Checking against an instance of Tree()
resolves the SyntaxError, but does not lead to working output, because it always produces "error". I do not want to use the workaround to match against a derived bool
(e.g., type(my_plant) == Tree)
) because it would limit me to only compare 2 outcomes (True/False) not match against multiple class types.
To expand on what I said in comments: match
introduces a value, but case
introduces a pattern to match against. It is not an expression that is evaluated. In case the pattern represents a class, the stuff in the parentheses is not passed to a constructor, but is matched against attributes of the match
value. Here is an illustrative example:
class Tree:
def __init__(self, name):
self.kind = name
def what_is(t):
match t:
case Tree(kind="oak"):
return "oak"
case Tree():
return "tree"
case _:
return "shrug"
print(what_is(Tree(name="oak"))) # oak
print(what_is(Tree(name="birch"))) # tree
print(what_is(17)) # shrug
Note here that outside case
, Tree(kind="oak")
would be an error:
TypeError: Tree.__init__() got an unexpected keyword argument 'kind'
And, conversely, case Tree(name="oak")
would never match, since Tree
instances in my example would not normally have an attribute named name
.
This proves that case
does not invoke the constructor, even if it looks like an instantiation.
EDIT: About your second error: you wrote case type(Tree()):
, and got TypeError: type() accepts 0 positional sub-patterns (1 given)
. What happened here is this: case type(...)
is, again, specifying a pattern. It is not evaluated as an expression. It says the match value needs to be of type type
(i.e. be a class), and it has to have attributes in the parentheses. For example,
match Tree:
case type(__init__=initializer):
print("Tree is a type, and this is its initializer:")
print(initializer)
This would match, and print something like
# => Tree is a type, and this is its initializer:
# <function Tree.__init__ at 0x1098593a0>
The same case
would not match an object of type Tree
, only the type itself!
However, you can only use keyword arguments in this example. Positional arguments are only available if you define __match_args__
, like the documentation says. The type type
does not define __match_args__
, and since it is an immutable built-in type, case type(subpattern, ...)
(subpattern! not subexpression!), unlike case type(attribute=subpattern, ...)
, will always produce an error.