crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is.md
is conditionalsis Nonefrom typing import Literal
def _(x: None | Literal[1]):
if x is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
reveal_type(x) # revealed: None | Literal[1]
is for other typesclass A: ...
def _(x: A, y: A | None):
if y is x:
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: A | None
reveal_type(y) # revealed: A | None
from dataclasses import dataclass
from enum import Enum
from typing import Literal, NewType
@dataclass
class Foo:
tag: Literal[False]
@dataclass
class Bar:
tag: Literal[True]
@dataclass
class UnknownTag:
tag: bool
def boolean_tags(value: Foo | Bar):
if value.tag is True:
reveal_type(value) # revealed: Bar
else:
reveal_type(value) # revealed: Foo
if value.tag is not True:
reveal_type(value) # revealed: Foo
else:
reveal_type(value) # revealed: Bar
if True is value.tag:
reveal_type(value) # revealed: Bar
else:
reveal_type(value) # revealed: Foo
if True is not value.tag:
reveal_type(value) # revealed: Foo
else:
reveal_type(value) # revealed: Bar
def ambiguous_tag(value: Foo | Bar | UnknownTag):
if value.tag is True:
reveal_type(value) # revealed: Bar | UnknownTag
else:
reveal_type(value) # revealed: Foo | UnknownTag
def nonsingleton_tag(value: Foo | Bar, tag: bool):
if value.tag is tag:
reveal_type(value) # revealed: Foo | Bar
else:
reveal_type(value) # revealed: Foo | Bar
def overwritten_tagged_union(value: Foo | Bar | bool):
if isinstance(value, (Foo, Bar)):
if (value := value.tag) is True:
reveal_type(value) # revealed: Literal[True]
else:
reveal_type(value) # revealed: Literal[False]
def tagged_union_rebound_by_comparator(value: Foo | Bar | bool):
if isinstance(value, (Foo, Bar)):
if value.tag is (value := True):
reveal_type(value) # revealed: Literal[True]
else:
reveal_type(value) # revealed: Literal[True]
def tagged_union_with_unrelated_assignment(value: Foo | Bar):
if value.tag is (tag := True):
reveal_type(value) # revealed: Bar
reveal_type(tag) # revealed: Literal[True]
else:
reveal_type(value) # revealed: Foo
reveal_type(tag) # revealed: Literal[True]
class MissingTag:
tag: None
class PresentTag:
tag: str
def optional_tags(value: MissingTag | PresentTag):
if value.tag is None:
reveal_type(value) # revealed: MissingTag
else:
reveal_type(value) # revealed: PresentTag
class Tag(Enum):
FOO = 1
BAR = 2
class EnumFoo:
tag: Literal[Tag.FOO]
class EnumBar:
tag: Literal[Tag.BAR]
def enum_tags(value: EnumFoo | EnumBar):
if value.tag is Tag.FOO:
reveal_type(value) # revealed: EnumFoo
else:
reveal_type(value) # revealed: EnumBar
BoolTag = NewType("BoolTag", bool)
class NewTypeTag:
tag: BoolTag
def newtype_tags(value: Foo | Bar | NewTypeTag):
if value.tag is True:
reveal_type(value) # revealed: Bar | NewTypeTag
else:
reveal_type(value) # revealed: Foo | NewTypeTag
def nonsingleton_newtype_tag(value: Foo | Bar, tag: BoolTag):
if value.tag is tag:
reveal_type(value) # revealed: Foo | Bar
else:
reveal_type(value) # revealed: Foo | Bar
def boolean_tags_after_truthiness(value: Foo | Bar | None):
if not value:
return
if value.tag is True:
reveal_type(value) # revealed: Bar & ~AlwaysFalsy
else:
reveal_type(value) # revealed: Foo & ~AlwaysFalsy
is in chained comparisonsdef _(x: bool, y: bool):
if y is x is False: # Interpreted as `(y is x) and (x is False)`
reveal_type(x) # revealed: Literal[False]
reveal_type(y) # revealed: bool
else:
# The negation of the clause above is (y is not x) or (x is not False)
# So we can't narrow the type of x or y here, because each arm of the `or` could be true
reveal_type(x) # revealed: bool
reveal_type(y) # revealed: bool
is in elif clausefrom typing import Literal
def _(x: None | Literal[1, True]):
if x is None:
reveal_type(x) # revealed: None
elif x is True:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[1]
is for enumsfrom enum import Enum
from typing import Literal
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
if answer is Answer.NO:
reveal_type(answer) # revealed: Literal[Answer.NO]
else:
reveal_type(answer) # revealed: Literal[Answer.YES]
class Single(Enum):
VALUE = 1
def _(x: Single | int):
if x is Single.VALUE:
reveal_type(x) # revealed: Single
else:
reveal_type(x) # revealed: int
def _(x: list[int] | Literal[Answer.NO]):
if x is Answer.NO:
return
reveal_type(x) # revealed: list[int]
is for EllipsisTypefrom types import EllipsisType
def _(x: int | EllipsisType):
if x is ...:
reveal_type(x) # revealed: EllipsisType
else:
reveal_type(x) # revealed: int
from typing import Literal
def f() -> Literal[1, 2] | None: ...
if (x := f()) is None:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1, 2]
value = f()
if result := (value is None):
reveal_type(value) # revealed: None
reveal_type(result) # revealed: Literal[True]
else:
reveal_type(value) # revealed: Literal[1, 2]
reveal_type(result) # revealed: Literal[False]
value = f()
if value := (value is None):
reveal_type(value) # revealed: Literal[True]
else:
reveal_type(value) # revealed: Literal[False]
is with two narrowable operandsBoth operands should be narrowed when both are narrowable expressions.
from typing import Literal
def _(t: Literal[True], tn: Literal[True] | None):
if tn is t:
reveal_type(tn) # revealed: Literal[True]
if t is tn:
reveal_type(tn) # revealed: Literal[True]
Both operands should also be narrowed in chained comparisons:
from typing import Literal
def _(a: Literal[1], b: Literal[1, 2], c: Literal[1, 2, 3]):
if a is b is c:
reveal_type(b) # revealed: Literal[1]
reveal_type(c) # revealed: Literal[1]
When a generic class object is compared with an exact class object, the exact class object is not
widened to the generic type. The intersection is retained because it preserves the relationship
between the class object and T:
[environment]
python-version = "3.12"
class Y:
def __init__(self) -> None: ...
class Z(Y):
def __init__(self, x: int) -> None: ...
def narrow[T: (Y, Z)](klass: type[T]) -> None:
if klass is Y:
reveal_type(klass) # revealed: type[T@narrow] & <class 'Y'>
reveal_type(Y) # revealed: <class 'Y'> & type[T@narrow]
if klass is Z:
reveal_type(klass) # revealed: <class 'Z'>
reveal_type(Z) # revealed: <class 'Z'>
def construct[T: (Y, Z)](klass: type[T]) -> T:
if klass is Y:
return Y()
raise AssertionError
class Generic[T]: ...
class Specialized(Generic[int]): ...
def narrow_generic_alias[T: (Generic[int], Specialized)](klass: type[T]) -> None:
if klass is Generic[int]:
reveal_type(klass) # revealed: type[T@narrow_generic_alias] & <class 'Generic[int]'>
reveal_type(Generic[int]) # revealed: <class 'Generic[int]'>
TypeVarThe is check below can discard int because it cannot be None or .... The is not check
cannot discard either remaining type: depending on the current constraint, either value could differ
from other.
from types import EllipsisType
from typing import TypeVar
T = TypeVar("T", None, EllipsisType)
def takes_singleton(value: None | EllipsisType) -> None: ...
def f(value: int | None | EllipsisType, other: T) -> None:
if value is other:
takes_singleton(value)
if value is not other:
reveal_type(value) # revealed: int | (None & ~T@f) | (EllipsisType & ~T@f)
is with NewTypesNewTypes with the same baseCalling a NewType returns its argument unchanged. Values with distinct NewTypes over Foo can
therefore be the same object even though their types are disjoint. The examples below cover direct
comparisons and narrowing through unions and intersections.
from typing import NewType
from ty_extensions import Intersection
class Foo: ...
class FooSub(Foo): ...
FooNewType1 = NewType("FooNewType1", Foo)
FooNewType2 = NewType("FooNewType2", Foo)
def same_base(foo1: FooNewType1, foo2: FooNewType2) -> None:
reveal_type(foo1 is foo2) # revealed: bool
if foo1 is foo2:
reveal_type(foo1) # revealed: FooNewType1
reveal_type(foo2) # revealed: FooNewType2
def union(value: FooNewType1 | None, other: FooNewType2) -> None:
if value is other:
reveal_type(value) # revealed: FooNewType1
def intersection(left: Intersection[FooNewType1, FooSub], right: FooNewType2) -> None:
if left is right:
reveal_type(right) # revealed: FooNewType2 & FooSub
NewTypes in TypeVar bounds and constraintsNewTypes inside TypeVar bounds and constraints can likewise refer to the same runtime object.
Comparing the distinct TypeVars below is not always false, and a true branch keeps the original
TypeVar.
from typing import NewType, TypeVar
class Foo: ...
FooNewType1 = NewType("FooNewType1", Foo)
FooNewType2 = NewType("FooNewType2", Foo)
FooNewType3 = NewType("FooNewType3", Foo)
FooNewType4 = NewType("FooNewType4", Foo)
BoundedT = TypeVar("BoundedT", bound=FooNewType1)
BoundedU = TypeVar("BoundedU", bound=FooNewType2)
def bounded_typevars(left: BoundedT, right: BoundedU) -> tuple[BoundedU, BoundedU]:
reveal_type(left is right) # revealed: bool
if left is right:
# TODO: This should narrow to `BoundedT & BoundedU` and avoid the false positive below.
reveal_type(left) # revealed: BoundedT@bounded_typevars
return (left, left) # error: [invalid-return-type]
return (right, right)
ConstrainedT = TypeVar("ConstrainedT", FooNewType1, FooNewType2)
ConstrainedU = TypeVar("ConstrainedU", FooNewType3, FooNewType4)
def constrained_typevars(left: ConstrainedT, right: ConstrainedU) -> tuple[ConstrainedU, ConstrainedU]:
reveal_type(left is right) # revealed: bool
if left is right:
# TODO: This should narrow to `ConstrainedT & ConstrainedU` and avoid the false positive.
reveal_type(left) # revealed: ConstrainedT@constrained_typevars
return (left, left) # error: [invalid-return-type]
return (right, right)
Every constraint below is a NewType based on EllipsisType, so other always refers to the same
... object as a SingletonC value. After an is not check, repeating the opposite check must be
unreachable.
from types import EllipsisType
from typing import NewType, TypeVar
from typing_extensions import assert_never
SingletonA = NewType("SingletonA", EllipsisType)
SingletonB = NewType("SingletonB", EllipsisType)
SingletonC = NewType("SingletonC", EllipsisType)
SingletonT = TypeVar("SingletonT", SingletonA, SingletonB)
def direct(value: SingletonC | int, other: SingletonT) -> None:
if value is not other:
if value is other:
assert_never(value)
NewType in the true branchIf an object is identical to a value with a NewType, the true branch narrows the object to that
NewType rather than its underlying type.
from typing import NewType
UserId = NewType("UserId", int)
def preserve_newtype(x: object, user_id: UserId) -> None:
if x is user_id:
reveal_type(x) # revealed: UserId
NewTypes with literalsCalls to NewType return their arguments unchanged. Comparisons with bool and int literals can
therefore succeed, so the true branches below remain reachable.
from typing import Literal, NewType
BoolNewType = NewType("BoolNewType", bool)
IntNewType = NewType("IntNewType", int)
def literals(true: Literal[True], b: BoolNewType, forty_two: Literal[42], i: IntNewType) -> None:
if b is true:
reveal_type(true) # revealed: Literal[True]
reveal_type(b) # revealed: BoolNewType
if i is forty_two:
reveal_type(forty_two) # revealed: Literal[42]
reveal_type(i) # revealed: IntNewType
is not with singleton NewTypesBoth NewTypes below are based on EllipsisType, which contains only the ... object. The
is not branch therefore removes the NewType alternative.
from types import EllipsisType
from typing import NewType
SingletonA = NewType("SingletonA", EllipsisType)
SingletonB = NewType("SingletonB", EllipsisType)
def singleton_is_not(value: SingletonA | int, other: SingletonB) -> None:
if value is not other:
reveal_type(value) # revealed: int
The type ~Literal[True] excludes the literal type but accepts the distinct BoolNewType. However,
BoolNewType(True) returns True unchanged, so value is True can be either true or false.
from __future__ import annotations
from typing import Literal, NewType
BoolNewType = NewType("BoolNewType", bool)
def excludes_true(value: ~Literal[True]) -> None:
reveal_type(value is True) # revealed: bool
excludes_true(BoolNewType(True))
Similarly, int & ~Literal[1] accepts IntNewType(1), which returns the 1 object unchanged, so
the comparison remains possible.
from typing import Literal, NewType
IntNewType = NewType("IntNewType", int)
def excludes_one(value: int & ~Literal[1]) -> None:
reveal_type(value is 1) # revealed: bool
excludes_one(IntNewType(1))
An identity comparison is still always false when the two runtime types are distinct final classes.
from typing import NewType, final
@final
class A: ...
@final
class B: ...
ANewType = NewType("ANewType", A)
BNewType = NewType("BNewType", B)
def disjoint_bases(a: ANewType, b: BNewType) -> None:
reveal_type(a is b) # revealed: Literal[False]
is where the other operand is a call expressionfrom typing import Literal, final
def foo() -> Literal[42]:
return 42
def f(x: object):
if x is foo():
reveal_type(x) # revealed: Literal[42]
else:
reveal_type(x) # revealed: object
if x is not foo():
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: Literal[42]
if foo() is x:
reveal_type(x) # revealed: Literal[42]
else:
reveal_type(x) # revealed: object
if foo() is not x:
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: Literal[42]
def bar() -> int:
return 42
def g(x: object):
if x is bar():
reveal_type(x) # revealed: int
else:
reveal_type(x) # revealed: object
if x is not bar():
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: int
@final
class FinalClass: ...
def baz() -> FinalClass:
return FinalClass()
def h(x: object):
if x is baz():
reveal_type(x) # revealed: FinalClass
else:
reveal_type(x) # revealed: object
if x is not baz():
reveal_type(x) # revealed: object
else:
reveal_type(x) # revealed: FinalClass
def spam() -> None:
return None
def h(x: object):
if x is spam():
reveal_type(x) # revealed: None
else:
# `else` narrowing can occur because `spam()` returns a singleton type
reveal_type(x) # revealed: ~None
if x is not spam():
reveal_type(x) # revealed: ~None
else:
reveal_type(x) # revealed: None