crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md
in conditionalsin for tuplesdef _(x: int):
if x in (1, 2, 3):
reveal_type(x) # revealed: int
else:
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[True] & ~Literal[2] & ~Literal[3]
def _(x: str):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]
from typing import Literal
def _(x: Literal[1, 2, "a", "b", False, b"abc"]):
if x in (1,):
reveal_type(x) # revealed: Literal[1]
elif x in (2, "a"):
reveal_type(x) # revealed: Literal[2, "a"]
elif x in (b"abc",):
reveal_type(x) # revealed: Literal[b"abc"]
elif x not in (3,):
reveal_type(x) # revealed: Literal["b", False]
else:
reveal_type(x) # revealed: Never
def _(x: Literal["a", "b", "c", 1]):
if x in ("a", "b", "c", 2):
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal[1]
in for PEP 695 aliases[environment]
python-version = "3.12"
from typing import Literal, assert_never
type Foo = Literal["a", "b", "c", "d"]
def _(x: Foo):
if x in ("a", "b"):
reveal_type(x) # revealed: Literal["a", "b"]
else:
reveal_type(x) # revealed: Literal["c", "d"]
def _(x: Foo) -> str:
if x in ("a", "b"):
return "AB"
match x:
case "c":
return "C"
case "d":
return "D"
case _ as never:
assert_never(never)
in for mixed PEP 695 aliases[environment]
python-version = "3.12"
from typing import Literal
type Foo = Literal["a", "b", "c"] | int
def _(x: Foo):
if x in ("a", "b"):
reveal_type(x) # revealed: Literal["a", "b"] | int
else:
reveal_type(x) # revealed: Literal["c"] | int
def _(x: Foo):
if x not in ("a", "c"):
reveal_type(x) # revealed: Literal["b"] | int
else:
reveal_type(x) # revealed: Literal["a", "c"] | int
in for str and literal stringsdef _(x: str):
if x in "abc":
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]
Per-character exclusions are limited to haystacks of at most 128 characters. Longer haystacks do not
synthesize a large intersection for a broad str, but they still narrow subjects that are already
unions of string literals.
from typing import Literal
def at_exclusion_limit(x: str):
if x in (
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str & ~Literal["a"]
def above_exclusion_limit(x: str):
if x in (
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"a"
):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str
def literal_union_above_exclusion_limit(x: Literal["a", "z"]):
if x in (
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaa"
"a"
):
reveal_type(x) # revealed: Literal["a"]
else:
reveal_type(x) # revealed: Literal["z"]
from typing import Literal, TypeVar
T = TypeVar("T", Literal["a"], Literal["d"])
def _(x: Literal["a", "b", "c", "d"]):
if x in "abc":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal["d"]
def substring(x: Literal["", "ab", "z"]):
if x in "abc":
reveal_type(x) # revealed: Literal["", "ab"]
else:
reveal_type(x) # revealed: Literal["z"]
def constrained_substring(x: T):
if x in "abc":
reveal_type(x) # revealed: T@constrained_substring & Literal["a"]
else:
reveal_type(x) # revealed: T@constrained_substring & Literal["d"]
def union_literal_haystack(x: Literal["a", "ab", "z"], flag: bool):
values = "abc" if flag else "def"
if x in values:
reveal_type(x) # revealed: Literal["a", "ab"]
else:
reveal_type(x) # revealed: Literal["a", "ab", "z"]
def mixed_literal_union_haystack(
x: Literal["a", "z", "missing"],
values: Literal["abc"] | tuple[Literal["z"]],
):
if x in values:
reveal_type(x) # revealed: Literal["a", "z"]
def _(x: Literal["a", "b", "c", "e"]):
if x in "abcd":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal["e"]
def _(x: Literal[1, "a", "b", "c", "d"]):
# error: [unsupported-operator]
if x in "abc":
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: Literal[1, "d"]
def empty_string(x: str):
if x in "":
reveal_type(x) # revealed: str
def empty_bytes(x: bytes):
if x in b"":
reveal_type(x) # revealed: bytes
bytes and bytearray accept byte subsequences and objects implementing __index__, not only the
integers described by their iteration type. We therefore leave the subject unchanged:
from typing import Literal, final
@final
class ByteSubstring(bytes): ...
@final
class ByteIndex:
def __index__(self) -> int:
return 97
def bytes_subsequence(value: ByteSubstring | Literal[97]) -> None:
if value in b"abc":
reveal_type(value) # revealed: ByteSubstring | Literal[97]
else:
reveal_type(value) # revealed: ByteSubstring | Literal[97]
def bytes_index(value: ByteIndex | Literal[97], values: bytes) -> None:
if value in values:
reveal_type(value) # revealed: ByteIndex | Literal[97]
else:
reveal_type(value) # revealed: ByteIndex | Literal[97]
def bytes_union_container(
value: Literal[b"a", 97],
values: bytes | tuple[int, ...],
) -> None:
if value in values:
reveal_type(value) # revealed: Literal[b"a", 97]
else:
reveal_type(value) # revealed: Literal[b"a", 97]
def bytearray_index(value: ByteIndex | Literal[97], values: bytearray) -> None:
if value in values:
reveal_type(value) # revealed: ByteIndex | Literal[97]
else:
reveal_type(value) # revealed: ByteIndex | Literal[97]
def bytearray_subsequence(value: Literal[b"a", 97], values: bytearray) -> None:
if value in values:
reveal_type(value) # revealed: Literal[b"a", 97]
else:
reveal_type(value) # revealed: Literal[b"a", 97]
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) in (1,):
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal[2, 3]
Literal, None and intfrom typing import Literal
def test(x: Literal["a", "b", "c"] | None | int = None):
if x in ("a", "b"):
# int is included because custom __eq__ methods could make
# an int equal to "a" or "b", so we can't eliminate it
reveal_type(x) # revealed: Literal["a", "b"] | int
else:
reveal_type(x) # revealed: Literal["c"] | None | int
def broad_element_type(x: str | None, values: dict[str, int]):
if x in values:
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str | None
def broad_element_type_with_unknown(values: dict[str, int]):
x = [None][0]
if x in values:
reveal_type(x) # revealed: Unknown
else:
reveal_type(x) # revealed: None | Unknown
Membership in a tuple containing a constrained type variable can preserve the relationship between
that type variable and a broader subject type. In the first example, a successful membership test
proves that x has the same enum-literal constraint as y, so returning x as T is valid.
Equality compatibility alone is not enough to establish that relationship: AlwaysEqual can compare
equal to every U, but it does not become a U, so the return in the second example remains
invalid.
from enum import Enum
from typing import Literal, TypeVar
class E(Enum):
A = 1
B = 2
T = TypeVar("T", Literal[E.A], Literal[E.B])
def correlated_typevar(x: E, y: T) -> T:
if x in (y,):
reveal_type(x) # revealed: T@correlated_typevar
return x
return y
U = TypeVar("U", int, str)
class AlwaysEqual:
def __eq__(self, other: object) -> Literal[True]:
return True
def unrelated_typevar(x: AlwaysEqual, y: U) -> U:
if x in (y,):
reveal_type(x) # revealed: AlwaysEqual
# error: [invalid-return-type] "Return type does not match returned value: expected `U@unrelated_typevar`, found `AlwaysEqual`"
return x
return y
not in conditionalfrom typing import Any, Literal, TypeVar
T = TypeVar("T", Literal[1], Literal[2])
def test(x: Literal["a", "b", "c"] | None | int = None):
if x not in ("a", "c"):
# int is included because custom __eq__ methods could make
# an int equal to "a" or "c", so we can't eliminate it
reveal_type(x) # revealed: Literal["b"] | None | int
else:
reveal_type(x) # revealed: Literal["a", "c"] | int
def broad_set_element(x: Literal[1, 2], values: set[int]) -> None:
if x not in values:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
def broad_dict_element(x: str | None, values: dict[str, int]) -> None:
if x not in values:
reveal_type(x) # revealed: str | None
else:
reveal_type(x) # revealed: str
def union_tuple_slot(x: Literal[1, 2], values: tuple[Literal[1, 2]]) -> None:
if x not in values:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
def union_tuple_slot_with_exact_value(
x: Literal[1, 2, 3],
values: tuple[Literal[1, 2], Literal[3]],
) -> None:
if x not in values:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2, 3]
def equality_equivalent_union_slot(
x: Literal[0, False, 2],
values: tuple[Literal[0, False]],
) -> None:
if x not in values:
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: Literal[0, False]
def correlated_typevar(x: T | None, y: T) -> None:
if x not in (y,):
reveal_type(x) # revealed: None
def tuple_with_any_slot(x: str | None, missing: Any) -> None:
if x not in (missing, None):
reveal_type(x) # revealed: str
else:
reveal_type(x) # revealed: str | None
def local_literal_rhs(x: str | None) -> None:
unavailable = [None, ""]
if x not in unavailable:
# TODO: This should narrow to `str` if we can prove that the local
# literal collection has not been mutated or aliased before the test.
reveal_type(x) # revealed: str | None
else:
reveal_type(x) # revealed: str | None
def mutable_global_rhs(x: str | None, unavailable: set[str | None]) -> None:
if x not in unavailable:
reveal_type(x) # revealed: str | None
else:
reveal_type(x) # revealed: str | None
When containment is known to compare items using equality, we can remove a union member that cannot
compare equal to any item in the container. A TypedDict cannot compare equal to a string, and a
final class with the default identity-based equality cannot compare equal to an integer. We retain
types such as int and classes with custom equality when they might still match an item.
from typing import Literal, TypedDict, final
class Payload(TypedDict):
value: int
@final
class Token: ...
@final
class AlwaysEqual:
def __eq__(self, other: object) -> bool:
return True
def typed_dict(x: Payload | Literal["missing"]):
if x in ("missing",):
reveal_type(x) # revealed: Literal["missing"]
def default_equality(x: Token | Literal[1]):
if x in (1,):
reveal_type(x) # revealed: Literal[1]
def overlapping_union_member(x: int | Literal["missing"]):
if x in ("missing", 1):
reveal_type(x) # revealed: int | Literal["missing"]
def custom_equality(x: AlwaysEqual | Literal[1]):
if x in (1,):
reveal_type(x) # revealed: AlwaysEqual | Literal[1]
def empty_tuple(x: Payload | Literal["missing"], values: tuple[()]):
if x in values:
reveal_type(x) # revealed: Never
__contains__When a class defines __contains__, membership need not check the values produced by iteration. The
iterator's element type therefore cannot narrow the value being tested:
from collections.abc import Iterator
from typing import Literal
class ContainsEverything:
def __iter__(self) -> Iterator[Literal["missing"]]:
yield "missing"
def __contains__(self, value: object) -> bool:
return True
def custom_contains(
x: Literal["present", "missing"],
values: ContainsEverything,
):
if x in values:
reveal_type(x) # revealed: Literal["present", "missing"]
__iter__A subclass can add __contains__, so an __iter__ annotation on a non-final class is not enough to
narrow a membership test. A final class cannot gain a new __contains__ method through subclassing;
if it only defines __iter__, membership is known to check the iterated values:
from collections.abc import Iterator
from typing import Literal, TypedDict, final
class Payload(TypedDict):
value: int
class IteratesMissing:
def __iter__(self) -> Iterator[Literal["missing"]]:
yield "missing"
def non_final_iterable(x: Payload | Literal["missing"], values: IteratesMissing):
if x in values:
reveal_type(x) # revealed: Payload | Literal["missing"]
@final
class FinalIterable:
def __iter__(self) -> Iterator[Literal["missing"]]:
yield "missing"
def final_iterable(x: Payload | Literal["missing"], values: FinalIterable):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
For list, set, frozenset, and tuple, ty uses the container's element type to describe the
values compared by membership. For dict, it uses the key type.
from typing import Literal, TypedDict
class Payload(TypedDict):
value: int
def builtin_list(x: Payload | Literal["missing"], values: list[Literal["missing"]]):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def builtin_set(x: Payload | Literal["missing"], values: set[Literal["missing"]]):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def builtin_frozenset(
x: Payload | Literal["missing"],
values: frozenset[Literal["missing"]],
):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def builtin_dict(
x: Payload | Literal["missing"],
values: dict[Literal["missing"], object],
):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def builtin_tuple(x: Payload | Literal["missing"], values: tuple[Literal["missing"], ...]):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
For subclasses of these built-ins, ty intentionally assumes that an unseen runtime subclass will not
add an incompatible __contains__. This is unsound: only a statically visible custom implementation
disables narrowing today. A future diagnostic should reject incompatible overrides at their
definition.
When no custom __contains__ is visible in the MRO, a subclass uses the element or key type of its
specialized built-in base.
from typing import Literal, TypedDict
class Payload(TypedDict):
value: int
class MissingList(list[Literal["missing"]]): ...
class MissingSet(set[Literal["missing"]]): ...
class MissingFrozenSet(frozenset[Literal["missing"]]): ...
class MissingDict(dict[Literal["missing"], object]): ...
class MissingTuple(tuple[Literal["missing"], ...]): ...
def inherited_list_contains(x: Payload | Literal["missing"], values: MissingList):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def inherited_set_contains(x: Payload | Literal["missing"], values: MissingSet):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def inherited_frozenset_contains(
x: Payload | Literal["missing"],
values: MissingFrozenSet,
):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def inherited_dict_contains(x: Payload | Literal["missing"], values: MissingDict):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
def inherited_tuple_contains(x: Payload | Literal["missing"], values: MissingTuple):
if x in values:
reveal_type(x) # revealed: Literal["missing"]
An inherited built-in __contains__ still searches the values stored in the container if the
subclass overrides __iter__. Here, the list stores arbitrary objects even though iteration is
annotated to yield only Literal["missing"].
from collections.abc import Iterator
from typing import Literal, TypedDict
class Payload(TypedDict):
value: int
class OverridesBuiltinIteration(list[object]):
def __iter__(self) -> Iterator[Literal["missing"]]:
yield "missing"
def overridden_iteration(
x: Payload | Literal["missing"],
values: OverridesBuiltinIteration,
):
if x in values:
reveal_type(x) # revealed: Payload | Literal["missing"]
A custom __contains__ on the class or an intermediate base disables both positive and negative
membership narrowing.
from typing import Literal, TypedDict
class Payload(TypedDict):
value: int
class ContainsEverythingList(list[Literal["missing"]]):
def __contains__(self, value: object) -> bool:
return True
class InheritsCustomContains(ContainsEverythingList): ...
def inherited_custom_contains(
x: Payload | Literal["missing"],
values: InheritsCustomContains,
):
if x in values:
reveal_type(x) # revealed: Payload | Literal["missing"]
class ContainsNothingTuple(tuple[Literal[1]]):
def __contains__(self, value: object) -> bool:
return False
def custom_tuple_not_in(x: Literal[1] | None, values: ContainsNothingTuple):
if x not in values:
reveal_type(x) # revealed: Literal[1] | None
A named expression around a tuple literal is still known to use tuple membership:
from typing import Literal, final
@final
class Token: ...
def assignment_expression(value: Token | Literal[1]) -> None:
if value in (values := (1,)):
reveal_type(value) # revealed: Literal[1]
NewType containersA NewType uses the containment behavior of its underlying type, so membership in a wrapped tuple
can narrow the tested value to the tuple's item type.
from typing import Literal, NewType, final
@final
class Token: ...
WrappedTuple = NewType("WrappedTuple", tuple[Literal[1], ...])
def wrapped_tuple(
value: Token | Literal[1],
values: WrappedTuple,
) -> None:
if value in values:
reveal_type(value) # revealed: Literal[1]
A bound or constrained type variable can use its item type only if membership checks those items for
every possible container. BoundTuple and both possible values of ConstrainedTuple use tuple
containment.
from collections.abc import Iterator
from typing import Literal, TypeVar, final
@final
class Token: ...
BoundTuple = TypeVar("BoundTuple", bound=tuple[Literal[1], ...])
def bounded_tuple(
value: Token | Literal[1],
values: BoundTuple,
) -> None:
if value in values:
reveal_type(value) # revealed: Literal[1]
ConstrainedTuple = TypeVar(
"ConstrainedTuple",
tuple[Literal[1], ...],
tuple[Literal[1]],
)
def constrained_tuple(
value: Token | Literal[1],
values: ConstrainedTuple,
) -> None:
if value in values:
reveal_type(value) # revealed: Literal[1]
A constrained type variable remains conservative if any constraint has unknown containment behavior.
OpenIterable is not final, so a subclass could define __contains__ that accepts a Token even
though iteration only yields Literal[1]. Because MixedContainers permits such an instance,
membership does not narrow the tested value.
from collections.abc import Iterator
from typing import Literal, TypeVar, final
@final
class Token: ...
class OpenIterable:
def __iter__(self) -> Iterator[Literal[1]]:
yield 1
MixedContainers = TypeVar(
"MixedContainers",
tuple[Literal[1], ...],
OpenIterable,
)
def mixed_constraints(
value: Token | Literal[1],
values: MixedContainers,
) -> None:
if value in values:
reveal_type(value) # revealed: Token | Literal[1]
Membership narrowing requires every possible container in a union to have known element-based containment. If they do, ty combines their element types. If any alternative has unknown or custom containment behavior, the tested value remains unchanged.
Both alternatives use known built-in containment, so the positive branch is limited to the union of their element types.
from typing import Literal, final
@final
class Token: ...
def known_container_union(
value: Token | Literal[1, 2],
values: list[Literal[1]] | set[Literal[2]],
) -> None:
if value in values:
reveal_type(value) # revealed: Literal[1, 2]
A non-final iterable can have a runtime subclass with a custom __contains__, so its iterator type
does not establish which values membership accepts.
from collections.abc import Iterator
from typing import Literal, final
@final
class Token: ...
class OpenIterable:
def __iter__(self) -> Iterator[Literal[1]]:
yield 1
def partially_known_container_union(
value: Token | Literal[1],
values: tuple[Literal[1], ...] | OpenIterable,
) -> None:
if value in values:
reveal_type(value) # revealed: Token | Literal[1]
A visible custom __contains__ can accept values outside the other alternative's element type, so
the union does not narrow the tested value.
from typing import Literal, final
@final
class Token: ...
class ContainsEverything:
def __contains__(self, value: object) -> bool:
return True
def custom_container_union(
value: Token | Literal[1],
values: tuple[Literal[1], ...] | ContainsEverything,
) -> None:
if value in values:
reveal_type(value) # revealed: Token | Literal[1]
After the isinstance check, values has type Iterable[Literal[1]] & tuple[object, ...]. Until
this intersection can be simplified to
tuple[Literal[1], ...], membership narrowing preserves the behavior from before containment
semantics were checked: the tuple component establishes that membership compares against its
elements, while the Iterable component constrains those elements to Literal[1].
from collections.abc import Iterable
from typing import Literal, final
@final
class Token: ...
def tuple_component_establishes_containment(
value: Token | Literal[1],
values: Iterable[Literal[1]],
) -> None:
if isinstance(values, tuple) and value in values:
reveal_type(value) # revealed: Literal[1]
Narrowing an iterable to Sized says nothing about how it implements membership. It could still be
an instance of a class with a custom __contains__ that accepts Token, so the positive branch
must preserve both union members.
from collections.abc import Iterable, Sized
from typing import Literal, final
@final
class Token: ...
def unrelated_protocol_does_not_establish_containment(
value: Token | Literal[1],
values: Iterable[Literal[1]],
) -> None:
if isinstance(values, Sized) and value in values:
reveal_type(value) # revealed: Token | Literal[1]
CustomContainsItems shows that this intersection can be inhabited by a class whose MRO selects the
custom __contains__ before list.__contains__. The order of intersection components does not
describe the runtime MRO, so the visible custom implementation prevents narrowing.
from typing import Literal, final
@final
class Token: ...
class ContainsEverything:
def __contains__(self, value: object) -> bool:
return True
class LiteralItems(list[Literal[1]]): ...
class CustomContainsItems(ContainsEverything, LiteralItems): ...
def custom_containment_component_prevents_narrowing(
value: Token | Literal[1],
values: LiteralItems,
) -> None:
if isinstance(values, ContainsEverything) and value in values:
reveal_type(value) # revealed: Token | Literal[1]
A range contains integers, so a string literal can be removed from the type of the tested value:
from typing import Literal
def range_membership(value: Literal["x", 1], values: range) -> None:
if value in values:
reveal_type(value) # revealed: Literal[1]
TypedDict key membershipMembership in a TypedDict checks its string keys, so the tested value can be narrowed to a
possible key. We do not apply key-based narrowing to arbitrary values, because in may test
substrings or elements instead:
from typing import Literal, TypedDict
class Values(TypedDict):
present: int
def typed_dict_container(value: Literal["present", 1], values: Values) -> None:
if value in values:
reveal_type(value) # revealed: Literal["present"]
def f(x: Literal["abc", "def"]):
if "a" in x:
# `x` could also be validly narrowed to `Literal["abc"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
else:
# `x` could also be validly narrowed to `Literal["def"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
if "a" not in x:
# `x` could also be validly narrowed to `Literal["def"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
else:
# `x` could also be validly narrowed to `Literal["abc"]` here:
reveal_type(x) # revealed: Literal["abc", "def"]
def _(x: bool):
if x in (True,):
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
def _(x: bool | str):
if x in (False,):
# `str` remains due to possible custom __eq__ methods on a subclass
reveal_type(x) # revealed: Literal[False] | str
else:
reveal_type(x) # revealed: Literal[True] | str
from typing_extensions import LiteralString
def _(x: LiteralString):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: Literal["a", "b", "c"]
else:
reveal_type(x) # revealed: LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]
def _(x: LiteralString | int):
if x in ("a", "b", "c"):
reveal_type(x) # revealed: Literal["a", "b", "c"] | int
else:
reveal_type(x) # revealed: (LiteralString & ~Literal["a"] & ~Literal["b"] & ~Literal["c"]) | int
from enum import Enum
from typing import Literal
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
def _(x: Color):
if x in (Color.RED, Color.GREEN):
reveal_type(x) # revealed: Literal[Color.RED, Color.GREEN]
else:
reveal_type(x) # revealed: Literal[Color.BLUE]
def after_excluding_red(x: Color):
if x is Color.RED:
return
if x in (Color.GREEN,):
reveal_type(x) # revealed: Literal[Color.GREEN]
else:
reveal_type(x) # revealed: Literal[Color.BLUE]
def after_excluding_red_mixed(x: Color | int):
if x is Color.RED:
return
if x in (Color.GREEN,):
reveal_type(x) # revealed: Literal[Color.GREEN] | int
else:
reveal_type(x) # revealed: Literal[Color.BLUE] | int
When the container's element type is a union of enum literals, membership narrows to that union.
Without the annotation, the tuple's elements are widened to Color, so the comprehension remains
list[Color]:
SelectedColor = Literal[Color.RED, Color.GREEN]
SELECTED_COLORS: tuple[SelectedColor, ...] = (Color.RED, Color.GREEN)
def selected_colors(colors: list[Color]) -> list[SelectedColor]:
result: list[SelectedColor] = []
result.extend([color for color in colors if color in SELECTED_COLORS])
return result
def _(colors: list[Color]):
inline = [color for color in colors if color in (Color.RED, Color.GREEN)]
reveal_type(inline) # revealed: list[Color]
An enum that can have additional runtime members can still be narrowed by a membership test against an explicit member. The other branch excludes that member without assuming that the declared members are exhaustive.
from enum import Enum, EnumMeta
class InjectingEnumMeta(EnumMeta):
def __new__(metacls, name, bases, namespace, **kwargs):
namespace["INJECTED"] = 2
return super().__new__(metacls, name, bases, namespace, **kwargs)
class InjectedEnum(Enum, metaclass=InjectingEnumMeta):
ONLY = 1
def _(value: InjectedEnum):
if value in (InjectedEnum.ONLY,):
reveal_type(value) # revealed: Literal[InjectedEnum.ONLY]
else:
reveal_type(value) # revealed: InjectedEnum & ~Literal[InjectedEnum.ONLY]
intfrom enum import Enum
class Status(Enum):
PENDING = 1
APPROVED = 2
REJECTED = 3
def test(x: Status | int):
if x in (Status.PENDING, Status.APPROVED):
# int is included because custom __eq__ methods could make
# an int equal to Status.PENDING or Status.APPROVED, so we can't eliminate it
reveal_type(x) # revealed: Literal[Status.PENDING, Status.APPROVED] | int
else:
reveal_type(x) # revealed: Literal[Status.REJECTED] | int
LiteralA built-in tuple cannot compare equal to a string literal, so the tuple alternative is excluded from the narrowed type.
from typing import Literal
def test(x: Literal["none", "auto", "required"] | tuple[list[str], Literal["auto", "required"]]):
if x in ("auto", "required"):
reveal_type(x) # revealed: Literal["auto", "required"]
else:
reveal_type(x) # revealed: Literal["none"] | tuple[list[str], Literal["auto", "required"]]