crates/ty_python_semantic/resources/mdtest/conditional/match.md
[environment]
python-version = "3.10"
def _(target: int):
match target:
case 1:
y = 2
case _:
y = 3
reveal_type(y) # revealed: Literal[2, 3]
def _(target: int):
match target:
case 1:
y = 2
case 2:
y = 3
# revealed: Literal[2, 3]
# error: [possibly-unresolved-reference]
reveal_type(y)
from collections.abc import Sequence
def sequence_star_pattern_is_exhaustive(paths: list[int]) -> None:
match paths:
case [*_paths]:
raise ValueError
reveal_type(paths) # revealed: Never
def sequence_star_pattern_is_not_exhaustive_for_text(paths: Sequence[str]) -> None:
match paths:
case [*_paths]:
raise ValueError
# `str`, `bytes`, and `bytearray` are subtypes of `Sequence`, but sequence
# patterns explicitly do not match them.
# TODO: After https://github.com/astral-sh/ty/issues/3314 is fixed, the
# `Sequence[str] & bytes` and `Sequence[str] & bytearray` intersections
# should simplify to `Never`.
reveal_type(paths) # revealed: str | (Sequence[str] & bytes) | (Sequence[str] & bytearray)
def sequence_prefix_star_pattern_is_not_catch_all(paths: Sequence[str]) -> None:
match paths:
case []:
raise ValueError
case [_first]:
raise ValueError
case [_first, _second, *_paths]:
raise ValueError
reveal_type(paths) # revealed: Sequence[str]
def _(target: int):
y = 1
y = 2
match target:
case 1:
y = 3
case 2:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
A value pattern matches based on equality: the first case branch here will be taken if subject
is equal to 2, even if subject is not an instance of int. We can't know whether C here has a
custom __eq__ implementation that might cause it to compare equal to 2, so we have to consider
the possibility that the case branch might be taken even though the type C is disjoint from the
type Literal[2].
This leads us to infer Literal[1, 3] as the type of y after the match statement, rather than
Literal[1]:
from typing import final
@final
class C:
pass
def _(subject: C):
y = 1
match subject:
case 2:
y = 3
reveal_type(y) # revealed: Literal[1, 3]
A case branch with a class pattern is taken if the subject is an instance of the given class, and
all subpatterns in the class pattern match.
from typing import final
class Foo:
pass
class FooSub(Foo):
pass
class Bar:
pass
@final
class Baz:
pass
def _(target: FooSub):
y = 1
match target:
case Baz():
y = 2
case Foo():
y = 3
case Bar():
y = 4
reveal_type(y) # revealed: Literal[3]
def _(target: FooSub):
y = 1
match target:
case Baz():
y = 2
case Bar():
y = 3
case Foo():
y = 4
reveal_type(y) # revealed: Literal[3, 4]
def _(target: FooSub | str):
y = 1
match target:
case Baz():
y = 2
case Foo():
y = 3
case Bar():
y = 4
reveal_type(y) # revealed: Literal[1, 3, 4]
from typing_extensions import assert_never
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
class Other: ...
def _(target: Point):
y = 1
match target:
case Point(0, 0):
y = 2
case Point(x=0, y=1):
y = 3
case Point(x=1, y=0):
y = 4
reveal_type(y) # revealed: Literal[1, 2, 3, 4]
def _(target: Point):
match target:
case Point(x, y): # irrefutable sub-patterns
pass
case _:
assert_never(target)
def _(target: Point | Other):
match target:
case Point(0, 0):
reveal_type(target) # revealed: Point
case Point(x=0, y=1):
reveal_type(target) # revealed: Point
case Point(x=1, y=0):
reveal_type(target) # revealed: Point
case Other():
reveal_type(target) # revealed: Other
Singleton patterns are matched based on identity, not equality comparisons or isinstance() checks.
from typing import Literal
def _(target: Literal[True, False]):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[2, 3]
def _(target: bool):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[2, 3]
def _(target: None):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[4]
def _(target: None | Literal[True]):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[2, 4]
# bool is an int subclass
def _(target: int):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[1, 2, 3]
def _(target: str):
y = 1
match target:
case True:
y = 2
case False:
y = 3
case None:
y = 4
reveal_type(y) # revealed: Literal[1]
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
y = 0
match answer:
case Answer.YES:
reveal_type(answer) # revealed: Literal[Answer.YES]
y = 1
case Answer.NO:
reveal_type(answer) # revealed: Literal[Answer.NO]
y = 2
reveal_type(y) # revealed: Literal[1, 2]
A | pattern matches if any of the subpatterns match.
from typing import Literal, final
def _(target: Literal["foo", "baz"]):
y = 1
match target:
case "foo" | "bar":
y = 2
case "baz":
y = 3
reveal_type(y) # revealed: Literal[2, 3]
def _(target: None):
y = 1
match target:
case None | 3:
y = 2
case "foo" | 4 | True:
y = 3
reveal_type(y) # revealed: Literal[2]
@final
class Baz:
pass
def _(target: int | None | float):
y = 1
match target:
case None | 3:
y = 2
case Baz():
y = 3
reveal_type(y) # revealed: Literal[1, 2]
class Foo: ...
def _(target: None | Foo):
y = 1
match target:
case Baz() | True | False:
y = 2
case int():
y = 3
reveal_type(y) # revealed: Literal[1, 3]
as patternsdef _(target: int | str):
y = 1
match target:
case 1 as x:
y = 2
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
case "foo" as x:
y = 3
reveal_type(x) # revealed: @Todo(`match` pattern definition types)
case _:
y = 4
reveal_type(y) # revealed: Literal[2, 3, 4]
__bool__ incorrectlyclass NotBoolable:
__bool__: int = 3
def _(target: int, flag: NotBoolable):
y = 1
match target:
# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `NotBoolable`"
case 1 if flag:
y = 2
case 2:
y = 3
reveal_type(y) # revealed: Literal[1, 2, 3]
When matching on a union of an enum and None, code after the match should still be reachable if None is not covered by any case, even when all enum members are covered.
from enum import Enum
class Answer(Enum):
YES = 1
NO = 2
def _(answer: Answer | None):
y = 0
match answer:
case Answer.YES:
y = 1
case Answer.NO:
y = 2
# The match is not exhaustive because None is not covered,
# so y could still be 0
reveal_type(y) # revealed: Literal[0, 1, 2]
def _(answer: Answer | None):
match answer:
case Answer.YES:
return 1
case Answer.NO:
return 2
# Code here is reachable because None is not covered
reveal_type(answer) # revealed: None
return 3
class Foo: ...
def _(answer: Answer | None):
match answer:
case Answer.YES:
return
case Answer.NO:
return
# New assignments after the match should not be `Never`
x = Foo()
reveal_type(x) # revealed: Foo
For class patterns, the runtime first checks that the match pattern is an instance of type, and
then uses isinstance to check the match.
If the match pattern is not an instance of type, we raise a diagnostic:
from typing import Any
from ty_extensions import Intersection
def _(val, Valid1: type | Any, Valid2: Intersection[type, Any], Valid3: type[Any], Valid4: type[int]):
Invalid1 = "foo"
match val:
# error: [invalid-match-pattern] "`Literal["foo"]` cannot be used in a class pattern because it is not a type"
case Invalid1(): ...
Invalid2 = int | str
match val:
# error: [invalid-match-pattern] "`<types.UnionType special-form 'int | str'>` cannot be used in a class pattern because it is not a type"
case Invalid2():
pass
case Valid1(): # fine
pass
case Valid2(): # fine
pass
case Valid3(): # fine
pass
case Valid4(): # fine
pass
We also raise a diagnostic if the class cannot be used with isinstance:
from typing import Any, TypedDict
def _(val):
Invalid3 = Any
match val:
# TODO: this should be an `invalid-match-pattern` error
case Invalid3(): ...
class Invalid4(TypedDict): ...
match val:
# TODO: this could have the `invalid-match-pattern` error code instead.
# error: [isinstance-against-typed-dict] "`TypedDict` class `Invalid4` cannot be used in a class pattern"
case Invalid4(): ...
We do not raise a diagnostic for dynamic types:
def _(val, UnknownSymbol):
reveal_type(UnknownSymbol) # revealed: Unknown
match val:
case UnknownSymbol(): ...
We also do not raise a diagnostic if the match pattern is a non-statically known instance of type:
def _(val, IntOrStr: type[int | str]):
match val:
case IntOrStr():
print(f"Matched as {IntOrStr}: {val!r}")