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.

Answer from Amadan on Stack Overflow
🌐
Python
peps.python.org › pep-0636
PEP 636 – Structural Pattern Matching: Tutorial | peps.python.org
Given that messages came from an external source, the types of the field could be wrong, leading to bugs or security issues. Any class is a valid match target, and that includes built-in classes like bool str or int.
🌐
Python
peps.python.org › pep-0622
PEP 622 – Structural Pattern Matching | peps.python.org
Each item in a class pattern can be an arbitrary pattern. A simple example: match shape: case Point(x, y): ... case Rectangle(x0, y0, x1, y1, painted=True): ... Whether a match succeeds or not is determined by the equivalent of an isinstance call. If the subject (shape, in the example) is not an instance of the named class (Point or Rectangle), the match fails. Otherwise, it continues (see details in the runtime section). The named class must inherit from type.
Top answer
1 of 1
7

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.

🌐
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.org
discuss.python.org › python help
Class type pattern matching - Python Help - Discussions on Python.org
1 week ago - I am comfused by this code and the brainstorming to figure out how to make it works: from dataclasses import dataclass import collections import dataclasses class A: class AA: pass class B(A): pass @dataclass class MyClass: foo: int bar: str def match_class_type(cls): match cls: case __builtins__.int: print(" is int") case __builtins__.str: print(" is str") # Why does it work and not for A or int ??? case ...
Top answer
1 of 4
15

You want to match against a constant value. That's a case for constant value patterns:

match typ:
    case somemodule.ClassOne:
        ...
    case anothermodule.ClassTwo:
        ...

Constant value patterns must be of the form NAME ('.' NAME)+ - that is, a name followed by at least one attribute lookup. A bare name will be considered a capture pattern. That's fine for classes defined in other modules, but if you want to match against classes in the same module, you can import the current module:

# in somemodule.py
import somemodule

class ClassOne:
    ...
class ClassTwo:
    ...

match typ:
    case somemodule.ClassOne:
        ...
    case somemodule.ClassTwo:
...

or if you want to avoid the circular import, you can create a namespace:

import types

options = types.SimpleNamespace()
options.ClassOne = ClassOne
options.ClassTwo = ClassTwo

match typ:
    case options.ClassOne:
        ...
    case options.ClassTwo:
        ...

Note that if you go the "import the current module" route, you need to be aware of Python's weird thing where the entry point script is considered the __main__ module, regardless of its file name. If you do python somefile.py and try to import somefile inside that, it will perform a second run of somefile.py as the somefile module and create a second copy of all your classes, and your match statements won't work.

2 of 4
6

Try using typ() instead of typ in the match line:

        class aaa():
            pass

        class bbb():
            pass

        def f1(typ):
            if typ is aaa:
                print("aaa")
            elif typ is bbb:
                print("bbb")
            else:
                print("???")

        def f2(typ):
            match typ():
                case aaa():
                    print("aaa")
                case bbb():
                    print("bbb")
                case _:
                    print("???")

        f1(aaa)
        f1(bbb)
        f2(aaa)
        f2(bbb)        

Output:

aaa
bbb
aaa
bbb

UPDATE:

Based on OP's comment asking for solution that works for classes more generally than the example classes in the question, here is an answer addressing this:

class aaa():
    pass

class bbb():
    pass

def f1(typ):
    if typ is aaa:
        print("aaa")
    elif typ is bbb:
        print("bbb")
    else:
        print("???")

def f2(typ):
    match typ.__qualname__:
        case aaa.__qualname__:
            print("aaa")
        case bbb.__qualname__:
            print("bbb")
        case _:
            print("???")

f1(aaa)
f1(bbb)
f2(aaa)
f2(bbb)

Output:

aaa
bbb
aaa
bbb

UPDATE #2: Based on this post and some perusal of PEP 634 here, I have created an example showing how a few data types (a Python builtin, a class from the collections module, and a user defined class) can be used by match to determine an action to perform based on a class type (or more generally, a data type):

class bbb:
    pass

class namespacing_class:
    class aaa:
        pass


def f1(typ):
    if typ is aaa:
        print("aaa")
    elif typ is bbb:
        print("bbb")
    else:
        print("???")

def f2(typ):
    match typ.__qualname__:
        case aaa.__qualname__:
            print("aaa")
        case bbb.__qualname__:
            print("bbb")
        case _:
            print("???")

def f3(typ):
    import collections
    match typ:
        case namespacing_class.aaa:
            print("aaa")
        case __builtins__.str:
            print("str")
        case collections.Counter:
            print("Counter")
        case _:
            print("???")

'''
f1(aaa)
f1(bbb)
f2(aaa)
f2(bbb)
'''
f3(namespacing_class.aaa)
f3(str)
import collections
f3(collections.Counter)

Outputs:

aaa
str
Counter

As stated in this answer in another post:

A variable name in a case clause is treated as a name capture pattern. It always matches and tries to make an assignment to the variable name. ... We need to replace the name capture pattern with a non-capturing pattern such as a value pattern that uses the . operator for attribute lookup. The dot is the key to matching this a non-capturing pattern.

In other words, if we try to say case aaa: for example, aaa will be interpreted as a name to which we assign the subject (typ in your code) and will always match and block any attempts to match subsequent case lines.

To get around this, for class type names (or names generally) that can be specified using a dot (perhaps because they belong to a namespace or another class), we can use the dotted name as a pattern that will not be interpreted as a name capture.

For built-in type str, we can use case __builtins__.str:. For the Counter class in Python's collections module, we can use case collections.Counter:. If we define class aaa within another class named namespacing_class, we can use case namespacing_class.aaa:.

However, if we define class bbb at the top level within our Python code, it's not clear to me that there is any way to use a dotted name to refer to it and thereby avoid name capture.

It's possible there's a way to specify a user defined class type in a case line and I simply haven't figured it out yet. Otherwise, it seems rather arbitrary (and unfortunate) to be able to do this for dottable types and not for non-dottable ones.

Top answer
1 of 4
22

If you just pass a type directly, it will consider it to be a "name capture" rather than a "value capture." You can coerce it to use a value capture by importing the builtins module, and using a dotted notation to check for the type.

import builtins
from typing import Type


def main(type_: Type):
    match (type_):
        case builtins.str:  # it works with the dotted notation
            print(f"{type_} is a String")
        case builtins.int:
            print(f"{type_} is an Int")
        case _:
            print("\nhttps://en.meming.world/images/en/0/03/I%27ve_Never_Met_This_Man_In_My_Life.jpg")

if __name__ == "__main__":
    main(type("hello"))  # <class 'str'> is a String
    main(str)  # <class 'str'> is a String
    main(type(42))  # <class 'int'> is an Int
    main(int)  # <class 'int'> is an Int
2 of 4
7

As its name suggests, structural pattern matching is more suited for matching patterns, not values (like a classic switch/case in other languages). For example, it makes it very easy to check different possible structures of a list or a dict, but for values there is not much advantage over a simple if/else structure:

if type_to_match == str:
    print("This is a String")
elif type_to_match == int:
    print("This is an Int")
else:
    print("\nhttps://en.meming.world/images/en/0/03/I%27ve_Never_Met_This_Man_In_My_Life.jpg")

But if you really want to use SPM, you could use the guard feature along with issublcass to check if a type is or is a child of any other:

match type_to_match:
    case s if issubclass(type_to_match, str):
        print(f"{s} - This is a String")
    case n if issubclass(type_to_match, int):
        print(f"{n} - This is an Int")
    case _:
        print("\nhttps://en.meming.world/images/en/0/03/I%27ve_Never_Met_This_Man_In_My_Life.jpg")
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.

Find elsewhere
🌐
Better Stack
betterstack.com › community › guides › scaling-python › python-pattern-matching
Structural Pattern Matching in Python: A Comprehensive Guide | Better Stack Community
Structural pattern matching is a feature added in Python 3.10 that makes it easier to work with different types of data and control the flow of your code. Instead of writing long chains of if, elif, and else statements, you can write cleaner and easier-to-read code using pattern matching. It works with many data types—like lists, dictionaries, and even custom classes...
🌐
Martin Heinz
martinheinz.dev › blog › 78
Recipes and Tricks for Effective Structural Pattern Matching in Python | Martin Heinz | Personal Website & Blog
August 2, 2022 - Hopefully, future versions of Python will include some level of code analysis that might catch at least some of these issues. Another hard to debug issue you might encounter stems from missing a valid branch/case - that is - when match doesn't cover all the possible cases. This can be mitigated by adding safety asserts like so: from enum import Enum from typing import NoReturn class Color(Enum): RED = "Red" GREEN = "Green" BLUE = "Blue" def exhaustiveness_check(value: NoReturn) -> NoReturn: assert False, 'This code should never be reached, got: {0}'.format(value) def some_func(color: Color) -> str: match color: case Color.RED: return "Color is red." case Color.GREEN: return "Color is green." exhaustiveness_check(color) some_func(Color.RED)
🌐
Coditation
coditation.com › blog › structural-pattern-matching-in-python-i
Structural Pattern Matching in Python I
Structural pattern matching can make your code more expressive and easier to read, as it allows you to directly compare the structure of objects without having to write long and complex type-checking code. It is available in languages such as Scala, Rust, Java (as of version 14), Python (as of version 3.10) In python, a pattern is a value defined after the case keyword which acts as a minimal structural constraint against which a subject is matched.
🌐
Buddy
buddy.works › home › tutorials › languages › structural pattern matching in python
Structural Pattern Matching In Python | Tutorials
April 28, 2021 - Python's pattern matching can also match classes, allowing us to check an object's type. Any class can be matched, even the built-in classes.
🌐
Hillel Wayne
hillelwayne.com › post › python-abc
Crimes with Python's Pattern Matching
July 31, 2022 - ... match command.split(): case ["quit"]: print("Goodbye!") quit_game() case ["look"]: current_room.describe() case ["get", obj]: character.get(obj, current_room) You can match on arrays, dictionaries, and custom objects. To support matching objects, Python uses isinstance(obj, class), which checks ...
🌐
ArjanCodes
arjancodes.com › blog › how-to-use-structural-pattern-matching-in-python
Introduction to Structural Pattern Matching in Python | ArjanCodes
June 20, 2024 - The __match_args__ attribute is a tuple that specifies the attribute names for pattern matching. By default, it’s an empty tuple, meaning no attributes can be extracted. class Point: __match_args__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y
🌐
Coditation
coditation.com › blog › structural-pattern-matching-in-python-iii
Structural Pattern Matching in Python III
# Matching a class pattern with argument where python class doesn't have a `__match_args__` class attribute (Will result in Error) subject = Furniture("table", 2200) # Output: TypeError: Furniture() accepts 0 positional sub-patterns (2 given) match subject: case Furniture("table", 2200): print(f"This {subject.name} will cost around {subject.price}")
🌐
adequateinfosoft
adequateinfosoft.com › guide › python › 1 › -how-to-use-python-pattern-matching-to-match-class-types
How to use Python pattern matching to match class types?
In Python, you can use pattern matching (also known as structural pattern matching or match statement) introduced in Python 3.10 to match class types.
🌐
Earthly
earthly.dev › blog › structural-pattern-matching-python
Structural Pattern Matching in Python - Earthly Blog
July 19, 2023 - If positional patterns are present in a class, they are converted to keyword patterns based on the arrangement in the __match_args__ attribute. In this tutorial, we delved into Python 3.10’s structural pattern matching feature. You learned about various patterns, like literal, capture, wildcard, ...
🌐
Inspired Python
inspiredpython.com › course › pattern-matching › mastering-structural-pattern-matching
Mastering Structural Pattern Matching • Inspired Python
Sequences are lists and tuples, or anything that inherits from the Abstract Base Class collections.abc.Sequence. Note though that the pattern matching engine will not expand iterables of any kind. And unlike other parts of Python where list("Hello") is a legitimate way of generating a list of a string’s characters, this use case does apply here.
🌐
Python.org
discuss.python.org › typing
Structural Pattern Matching for Types - Typing - Discussions on Python.org
April 10, 2025 - I’m sure that there are other use cases for this, but the main one that comes to mind is inferring the type of function parameters. This has been discussed at Extract kwargs types from a function signature (however I am proposing something more general). It is common that you want to override a superclass method, for example, which may accept many parameters that you don’t need in the override: class MyClass(BaseClass): def my_method(self, *args, **kwargs): ... return super(...