I was quite surprised to see that this feature is actually possible!! (at least using python 3.11, not tried before)

This is enabled thanks to https://peps.python.org/pep-0622/#exhaustiveness-checks that provides a match construction that must pass type checking.

Execute the following code with $ python foo.py and run the type checking with $ mypy --strict foo.py.

Demo:

### Define our types

# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved

class Mult:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Add:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Const:
    val: int
    def __init__(self, val:int):
        self.val = val

Expr = Const | Add | Mult

### Define our functions

def my_eval(e : Expr) -> int:
    match e:
        case Const():
            return e.val
        case Add():
            return my_eval(e.left) * my_eval(e.right)
        case Mult():
            return my_eval(e.left) + my_eval(e.right)

### Use them
        
print(my_eval(Const(42)))
print(my_eval(Add(Const(42),Const(45))))
$ python foo.py
42
1890
$ mypy --strict foo.py
Success: no issues found in 1 source file

If you decide now to remove one item, for instance by commenting the Add() case, you get an error (I have to admit, not extremely clear):

$ mypy --strict foo2.py
foo2.py:37: error: Missing return statement  [return]
Found 1 error in 1 file (checked 1 source file)

TODO It would be great to find a simpler interface to define types, notably I found

class Mult(tuple[int, int]):
    pass

but it does not seem to be working so far inside mypy https://github.com/python/mypy/issues/17139

Caveats

Note that this method has a few drawbacks. Notably, as pointed in comments (I filled a bug report here), if the return type is None (say you want to return nothing) like def my_eval(e : Expr) -> None, then the test will pass even if you remove a case:

# Execute with "python foo.py" and
# run the type checking with "mypy --strict foo.py"


### Define our types

# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved

class Mult:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Add:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Const:
    val: int
    def __init__(self, val:int):
        self.val = val

Expr = Const | Add | Mult

### Define our functions
x = 0

def my_eval(e : Expr) -> None:
    match e:
# This should fail: still it works
#        case Const():
#            print(e.val)
        case Add():
            print("(")
            my_eval(e.left)
            print(") + (")
            my_eval(e.right)
            print(")")
        case Mult():
            print("(")
            my_eval(e.left)
            print(") + (")
            my_eval(e.right)
            print(")")

### Use them

my_eval(Add(Const(42),Const(45)))

Note also that this does not prevent the function from raising an Exception, or never ending https://github.com/python/mypy/issues/17140

Answer from tobiasBora on Stack Overflow
🌐
Python.org
discuss.python.org › python help
How can you match on a union of types? - Python Help - Discussions on Python.org
May 15, 2023 - I would like to exhaustively match against a union of type[T] | type[V]. For example: from typing_extensions import assert_never def f(t: type[int] | type[float]) -> None: match t: case int: print("it's a int") case float: print("it's a float") ...
🌐
Pydantic
docs.pydantic.dev › latest › concepts › unions
Unions - Pydantic Validation
If you want to validate data against a union, and solely a union, you can use pydantic's TypeAdapter construct instead of inheriting from the standard BaseModel. In the context of the previous example, we have the following: type_adapter = TypeAdapter(Pet) pet = type_adapter.validate_python( {'pet_type': 'cat', 'color': 'black', 'black_name': 'felix'} ) print(repr(pet)) #> BlackCat(pet_type='cat', color='black', black_name='felix')
🌐
Preferred
tech.preferred.jp › ホーム › blog › exhaustive union matching in python
Exhaustive Union Matching in Python - Preferred Networks Tech Blog
January 12, 2022 - One way to resolve this problem was brought up by GitHub user @bluetech in https://github.com/python/mypy/issues/5818 inspired by the TypeScript approach of exhaustive checking, has since been described in the mypy Literal documentation as well, and is based on the following approach: For a value x, handle each possible variant of the corresponding sum type in an elif block. Add an else block. If indeed all variants were covered before, the only remaining possible type for x is the empty Union.
🌐
Python.org
discuss.python.org › ideas
Syntactic sugar for union cases in match statements - Ideas - Discussions on Python.org
March 23, 2024 - Given these definitions: from typing import NamedTuple, TypeAlias class A(NamedTuple): x: int y: str class B(NamedTuple): x: int y: str it would be nice if I could write the following: AB: TypeAlias = A | B match ab: case AB(x, y): print(f"{x=}, {y=}") instead of having to write: match ab: case A(x, y) | B(x, y): print(f"{x=}, {y=}") That is, it would be great if the union type was expanded into an or-pattern in match-case.
🌐
GitHub
github.com › python › cpython › issues › 106246
[match-case] Allow matching Union types · Issue #106246 · python/cpython
May 21, 2023 - from typing import TypeAlias Numeric: TypeAlias = int | float assert isinstance(1.2, Numeric) # ✔ match 1.2: case Numeric(): # TypeError: called match pattern must be a type pass · This carries extra benefits, as TypeAliases are kind of necessary to write legible code when working with large unions, such as e.g. PythonScalar: TypeAlias = None | bool | int | float | complex | bool | str | datetime | timedelta.
Published   Jun 29, 2023
Top answer
1 of 1
2

I was quite surprised to see that this feature is actually possible!! (at least using python 3.11, not tried before)

This is enabled thanks to https://peps.python.org/pep-0622/#exhaustiveness-checks that provides a match construction that must pass type checking.

Execute the following code with $ python foo.py and run the type checking with $ mypy --strict foo.py.

Demo:

### Define our types

# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved

class Mult:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Add:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Const:
    val: int
    def __init__(self, val:int):
        self.val = val

Expr = Const | Add | Mult

### Define our functions

def my_eval(e : Expr) -> int:
    match e:
        case Const():
            return e.val
        case Add():
            return my_eval(e.left) * my_eval(e.right)
        case Mult():
            return my_eval(e.left) + my_eval(e.right)

### Use them
        
print(my_eval(Const(42)))
print(my_eval(Add(Const(42),Const(45))))
$ python foo.py
42
1890
$ mypy --strict foo.py
Success: no issues found in 1 source file

If you decide now to remove one item, for instance by commenting the Add() case, you get an error (I have to admit, not extremely clear):

$ mypy --strict foo2.py
foo2.py:37: error: Missing return statement  [return]
Found 1 error in 1 file (checked 1 source file)

TODO It would be great to find a simpler interface to define types, notably I found

class Mult(tuple[int, int]):
    pass

but it does not seem to be working so far inside mypy https://github.com/python/mypy/issues/17139

Caveats

Note that this method has a few drawbacks. Notably, as pointed in comments (I filled a bug report here), if the return type is None (say you want to return nothing) like def my_eval(e : Expr) -> None, then the test will pass even if you remove a case:

# Execute with "python foo.py" and
# run the type checking with "mypy --strict foo.py"


### Define our types

# See https://github.com/python/mypy/issues/17139 to get a nicer
# syntax once the bug is solved

class Mult:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Add:
    # Expr is not yet defined, so we use forward references
    # https://peps.python.org/pep-0484/#forward-references
    left: 'Expr'
    right: 'Expr'
    def __init__(self, left:'Expr', right:'Expr'):
        self.left = left
        self.right = right

class Const:
    val: int
    def __init__(self, val:int):
        self.val = val

Expr = Const | Add | Mult

### Define our functions
x = 0

def my_eval(e : Expr) -> None:
    match e:
# This should fail: still it works
#        case Const():
#            print(e.val)
        case Add():
            print("(")
            my_eval(e.left)
            print(") + (")
            my_eval(e.right)
            print(")")
        case Mult():
            print("(")
            my_eval(e.left)
            print(") + (")
            my_eval(e.right)
            print(")")

### Use them

my_eval(Add(Const(42),Const(45)))

Note also that this does not prevent the function from raising an Exception, or never ending https://github.com/python/mypy/issues/17140

🌐
Python
peps.python.org › pep-0622
PEP 622 – Structural Pattern Matching | peps.python.org
With such definition, a type checker can safely treat Node as Union[Name, Operation, Assignment, Print], and also safely treat e.g. Expression as Union[Name, Operation]. So this will result in a type checking error in the below snippet, because Name is not handled (and type checker can give a useful error message): def dump(node: Node) -> str: match node: case Assignment(target, value): return f"{target} = {dump(value)}" case Print(value): return f"print({dump(value)})" case Operation(left, op, right): return f"({dump(left)} {op} {dump(right)})"
🌐
Reddit
reddit.com › r/learnpython › checking python union types (pattern matching?)
r/learnpython on Reddit: checking Python Union types (pattern matching?)
January 21, 2021 -

I am playing around with the python typing stuff (https://docs.python.org/3/library/typing.html) and I am running python 3.8.3

I have code like:

from typing import Literal, TypedDict

UserPayload = dict[Literal['user'], str]
class IOUPayload(TypedDict):
    lender: str
    borrower: str
    amount: float

def post(path, payload:Union[UserPayload, IOUPayload]=None):
        if path == "/iou" and isinstance(payload, IOUPayload):
           print(f"{payload['borrower']} owes {payload['lender']} now!")
        if path == "/add" and isinstance(payload, UserPayload): # this is wrong!
           print(f"add {payload['user']} to DB")

Examples that I am finding use "primitive" types like list, int, dict in the Union. Which isinstance works for. How do I check for the type "alias" (custom type?) that I created?

EDIT:

I want this for mypy eventually, I am starting with the pyright plugin for now though.

As a workaround I converted UserPayload to a class::

class UserPayload(TypedDict):
    user: str

I also noticed that I have never updated my pyenv install so I thought they had not packaged python 3.9.1 yet. I'm busy installing it now and it is taking suspiciously long. I am hoping it might be to do with the version.

EDIT 2

It just clicked that typing has type hints which is why u/double_en10dre was suggestion pydantic. I was trying to use hints at runtime. My current solution is to just check a property of the dict so:

from typing import Literal, TypedDict

UserPayload = dict[Literal['user'], str]
class IOUPayload(TypedDict):
    lender: str
    borrower: str
    amount: float

def post(path, payload:Union[UserPayload, IOUPayload]=None):
        if path == "/iou" and 'lender' in payload:
           print(f"{payload['borrower']} owes {payload['lender']} now!")
        if path == "/add" and 'user' in payload:
           print(f"add {payload['user']} to DB")

The problem now is that I guess I have to write code to make sure payload is valid since I can't use external modules.

🌐
Stack Overflow
stackoverflow.com › questions › 65912706 › pattern-matching-over-nested-union-types-in-python
Pattern matching over nested `Union` types in Python - Stack Overflow
I want to avoid exposing the details of the sub-Union MyNumberT in this function. ... Your use-case is a great example to use a utility provided by functools called singledispatch. It allows you to define multiple functionality to a single function based on type of input. from functools import singledispatch # This class defines the function with # a base case if the input type doesn't match @singledispatch def my_data_to_string(datum) -> str: raise TypeError(f"unsupported format: {type(datum)}") # Registering for type str using type hint @my_data_to_string.register def _(datum: str): return d
Find elsewhere
🌐
Reddit
reddit.com › r/rust › python 3.10 has anonymous union types and rust-y structural pattern matching
r/rust on Reddit: Python 3.10 has Anonymous Union Types and Rust-y Structural Pattern Matching
May 15, 2021 - Unfortunately, there is no ? in Python so your code is still full of if err: return err type of checks. ... There's a library for that even: https://pypi.org/project/result/ It doesn't use a Result[T, E] abstract base class though, it just defines Ok and Err types and uses a union of them both as Result. Don't know if the current version is compatible with pattern matching.
🌐
GitHub
github.com › python › mypy › issues › 15426
Match statement with tuple of union type fails · Issue #15426 · python/mypy
Bug Report I am typechecking a match statement with a tuple of union types and it fails. To Reproduce Playground: https://mypy-play.net/?mypy=latest&python=3.10&gist=992b588ec7e4807f3105db45fa9ef0a8 My union type: TransactionType = str |...
🌐
Python documentation
docs.python.org › 3 › library › typing.html
typing — Support for type hints
Changed in version 3.14: types.UnionType is now an alias for Union, and both Union[int, str] and int | str create instances of the same class. To check whether an object is a Union at runtime, use isinstance(obj, Union).
🌐
GitHub
gist.github.com › ericgj › 8af30c2a89278a2442625aa7c6bd18dc
simple tagged union type matching in python · GitHub
simple tagged union type matching in python. GitHub Gist: instantly share code, notes, and snippets.
🌐
GitHub
github.com › microsoft › pylance-release › issues › 2767
Match doesn't narrow tagged union type when ...
Python version (& distribution if applicable, e.g. Anaconda): Python version 3.10 (condaforge) When matching on a tagged union's tag should narrow the type of the union
🌐
Medium
medium.com › @apps.merkurev › union-type-expression-in-python-50a1b7d012cd
Union Type Expression in Python. Another useful use of the | operator in… | by AM | Medium
September 7, 2023 - Union Type Expression in Python Another useful use of the | operator in Python 3.10. Starting from Python 3.10, you can use Union type expression in some scenarios. The built-in functions …
🌐
Python.org
discuss.python.org › typing
Draft PEP: Matching Union Types - Typing - Discussions on Python.org
July 21, 2025 - I finally got around doing a PEP-style write-up for my old PR https://github.com/python/cpython/pull/118644, allowing match-case against union types. Looking for feedback and a PEP sponsor. Abstract This PEP proposes allowing union types to be used in structural pattern matching[1]. Motivation PEP 604 introduced the | operator to create type unions, and allows its usage in isinstance and issubclass calls.
🌐
JetBrains
youtrack.jetbrains.com › issue › PY-53880
Incorrect exhaustive pattern matching of Enum or Union types
{{ (>_<) }} This version of your browser is not supported. Try upgrading to the latest stable version. Something went seriously wrong
🌐
Readthedocs
tagged-union.readthedocs.io › en › latest
Python Tagged Union — Tagged Union 0.0.2 documentation
The possible members of the tagged union are specified as class attributes, equal to a type or tuple of types representing how that union member should be constructed. All tagged union members inherit from the orignal tagged union class, allowing common implementations inside this class. ... It is worth noting that python imports ignores identifiers which start with _, so if you wish to use the _ identifier as a wildcard for matching, it must be imported explicitly:
🌐
Niklasrosenstein
niklasrosenstein.github.io › python-databind › examples › unions
Defining Unions - python-databind
The Literal will fail to deserialize if the value in the payload does not match with the literal value, and naive union types will try all types in the union in order and return the first successfully deserialized type. ... Arguably this is rather inefficient; a better implementation would be to prioritize checking values of literal fields first so we don't need to attempt to deserialize the rest if there's no match. # cat <<EOF | python - import dataclasses from databind.json import load from typing import Literal @dataclasses.dataclass class AwsMachine: region: str name: str instance_id: str
🌐
GitHub
github.com › python › mypy › issues › 2282
Check for Match instance in Union[Match, str] · Issue #2282 · python/mypy
October 19, 2016 - It looks like Match type alias supports __subclasscheck__, but mypy used with · from typing import Match, Union def process_consumed_issubclass(consumed: Union[Match, str]) -> str: if issubclass(type(consumed), Match): return consumed.group('res') return consumed
Published   Oct 19, 2016