Search code examples
pythonstructural-pattern-matching

How to use Python pattern matching to match class types?


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.


Solution

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