crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md
if x or if not x)from typing import Literal, TypeAlias
X: TypeAlias = Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]
def _(x: X):
if x:
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
else:
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
def _(x: X):
if not x:
reveal_type(x) # revealed: Literal[0, False, "", b""] | None | tuple[()]
else:
reveal_type(x) # revealed: Literal[-1, True, "foo", b"bar"]
def _(x: X):
if x and not x:
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
def _(x: X):
if not (x and not x):
reveal_type(x) # revealed: Literal[0, -1, "", "foo", b"", b"bar"] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never
def _(x: X):
if x or not x:
reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()]
else:
reveal_type(x) # revealed: Never
def _(x: X):
if not (x or not x):
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: Literal[-1, 0, "foo", "", b"bar", b""] | bool | None | tuple[()]
def _(x: X):
if (isinstance(x, int) or isinstance(x, str)) and x:
reveal_type(x) # revealed: Literal[-1, True, "foo"]
else:
reveal_type(x) # revealed: Literal[b"", b"bar", 0, False, ""] | None | tuple[()]
We can narrow on an attribute expression, even when its base is a named expression:
class Foo:
val: int | None
if (foo := Foo()).val:
reveal_type(foo.val) # revealed: int & ~AlwaysFalsy
But we don't pick up stale narrowings from before the assignment in the named expression:
foo1 = Foo()
foo1.val = None
if (foo1 := Foo()).val:
reveal_type(foo1.val) # revealed: int & ~AlwaysFalsy
Basically functions are always truthy.
def flag() -> bool:
return True
def foo(hello: int) -> bytes:
return b""
def bar(world: str, *args, **kwargs) -> float:
return 0.0
x = foo if flag() else bar
if x:
reveal_type(x) # revealed: (def foo(hello: int) -> bytes) | (def bar(world: str, *args, **kwargs) -> int | float)
else:
reveal_type(x) # revealed: Never
The boolean value of an instance is not always consistent. For example, __bool__ can be customized
to return random values, or in the case of a list(), the result depends on the number of elements
in the list. Therefore, these types should not be narrowed by if x or if not x.
class A: ...
class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
if x and not x:
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
else:
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
Also, types may not be Truthy. This is because __bool__ can be customized via a metaclass.
Although this is a very rare case, we may consider metaclass checks in the future to handle this
more accurately.
def flag() -> bool:
return True
x = int if flag() else str
reveal_type(x) # revealed: <class 'int'> | <class 'str'>
if x:
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysFalsy) | (<class 'str'> & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: (<class 'int'> & ~AlwaysTruthy) | (<class 'str'> & ~AlwaysTruthy)
Some custom classes can have a boolean value that is consistently determined as either True or
False, regardless of the instance's state. This is achieved by defining a __bool__ method that
always returns a fixed value.
These types can always be fully narrowed in boolean contexts, as shown below:
from typing import Literal
class T:
def __bool__(self) -> Literal[True]:
return True
class F:
def __bool__(self) -> Literal[False]:
return False
t = T()
if t:
reveal_type(t) # revealed: T
else:
reveal_type(t) # revealed: Never
f = F()
if f:
reveal_type(f) # revealed: Never
else:
reveal_type(f) # revealed: F
from typing import Literal
class A: ...
class B: ...
def flag() -> bool:
return True
def instance() -> A | B:
return A()
def literals() -> Literal[0, 42, "", "hello"]:
return 42
x = instance()
y = literals()
if isinstance(x, str) and not isinstance(x, B):
reveal_type(x) # revealed: A & str & ~B
reveal_type(y) # revealed: Literal[0, 42, "", "hello"]
z = x if flag() else y
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
else:
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
from typing import Literal
def f(x: Literal[0, 1], y: Literal["", "hello"]):
if x and y and not x and not y:
reveal_type(x) # revealed: Never
reveal_type(y) # revealed: Never
else:
# ~(x or not x) and ~(y or not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
if (x or not x) and (y and not y):
reveal_type(x) # revealed: Never
reveal_type(y) # revealed: Never
else:
# ~(x or not x) or ~(y and not y)
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal["", "hello"]
After merging control flows, when we take the union of all constraints applied in each branch, we should return to the original state.
class A: ...
x = A()
if x and not x:
y = x
reveal_type(y) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy
else:
y = x
reveal_type(y) # revealed: A
reveal_type(y) # revealed: A
from typing import Literal
class MetaAmbiguous(type):
def __bool__(self) -> bool:
return True
class MetaFalsy(type):
def __bool__(self) -> Literal[False]:
return False
class MetaTruthy(type):
def __bool__(self) -> Literal[True]:
return True
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous:
raise NotImplementedError
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...
class DeferredClass(metaclass=MetaDeferred): ...
def _(
a: type[AmbiguousClass],
t: type[TruthyClass],
f: type[FalsyClass],
d: type[DeferredClass],
ta: type[TruthyClass | AmbiguousClass],
af: type[AmbiguousClass] | type[FalsyClass],
flag: bool,
):
reveal_type(ta) # revealed: type[TruthyClass | AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(af) # revealed: type[AmbiguousClass | FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# error: [unsupported-bool-conversion] "Boolean conversion is not supported for type `MetaDeferred`"
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
tf = TruthyClass if flag else FalsyClass
reveal_type(tf) # revealed: <class 'TruthyClass'> | <class 'FalsyClass'>
if tf:
reveal_type(tf) # revealed: <class 'TruthyClass'>
else:
reveal_type(tf) # revealed: <class 'FalsyClass'>
from typing import Literal
class A: ...
def _(x: Literal[0, 1]):
reveal_type(x or A()) # revealed: Literal[1] | A
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
class Falsy:
def __bool__(self) -> Literal[False]:
return False
class Truthy:
def __bool__(self) -> Literal[True]:
return True
def _(x: Falsy | Truthy):
reveal_type(x or A()) # revealed: Truthy | A
reveal_type(x and A()) # revealed: Falsy | A
class MetaFalsy(type):
def __bool__(self) -> Literal[False]:
return False
class MetaTruthy(type):
def __bool__(self) -> Literal[True]:
return True
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...
def _(x: type[FalsyClass] | type[TruthyClass]):
reveal_type(x or A()) # revealed: type[TruthyClass] | A
reveal_type(x and A()) # revealed: type[FalsyClass] | A
def _(coinflip_1: bool, coinflip_2: bool):
if coinflip_1 if coinflip_2 else coinflip_1:
reveal_type(coinflip_1) # revealed: Literal[True]
else:
reveal_type(coinflip_1) # revealed: Literal[False]
from typing import Literal
def _(flag: bool, x: Literal[0, 1], y: Literal[0, 1]):
if x if flag else y:
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal[0, 1]
else:
reveal_type(x) # revealed: Literal[0, 1]
reveal_type(y) # revealed: Literal[0, 1]
def _(flag: bool, x: Literal[0, 1, 2]):
if (x == 1) if flag else (x == 2):
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[0, 2, 1]
def _(flag: bool, x: Literal[0, 1], y: int):
if (x == 1) if flag else y:
reveal_type(x) # revealed: Literal[0, 1]
from typing import Literal
def _(x: Literal[0, 1], y: Literal[0, 1]):
if x if True else y:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: Literal[0]
if x if False else y:
reveal_type(y) # revealed: Literal[1]
else:
reveal_type(y) # revealed: Literal[0]
LiteralStringfrom typing_extensions import LiteralString
def _(x: LiteralString):
if x:
reveal_type(x) # revealed: LiteralString & ~Literal[""]
else:
reveal_type(x) # revealed: Literal[""]
if not x:
reveal_type(x) # revealed: Literal[""]
else:
reveal_type(x) # revealed: LiteralString & ~Literal[""]
When a truthiness check is used with a named expression, the target of the named expression should be narrowed.
def get_value() -> str | None:
return "hello"
def f():
if x := get_value():
reveal_type(x) # revealed: str & ~AlwaysFalsy
else:
reveal_type(x) # revealed: (str & ~AlwaysTruthy) | None
The value expression on the right-hand side of the walrus operator should also be narrowed:
def foo(value: int | None):
if foo := value:
reveal_type(value) # revealed: int & ~AlwaysFalsy
else:
reveal_type(value) # revealed: (int & ~AlwaysTruthy) | None
TypedDict and Nonefrom typing_extensions import TypedDict, NotRequired, Required
class Empty(TypedDict): ...
class NonEmpty(TypedDict):
x: int
class HasNotRequired1(TypedDict):
x: NotRequired[int]
class HasNotRequired2(TypedDict, total=False):
x: int
class AlsoNonEmpty(TypedDict, total=False):
x: Required[int]
def f(arg1: Empty | None, arg2: NonEmpty | None, arg3: HasNotRequired1 | None, arg4: HasNotRequired2 | None, arg5: AlsoNonEmpty):
if arg1:
# the truthiness of `Empty` is ambiguous,
# because the `Empty` type includes possible `TypedDict` subtypes
# that might have required keys
reveal_type(arg1) # revealed: Empty & ~AlwaysFalsy
if arg2:
# but `NonEmpty` is known to be a subtype of `AlwaysTruthy`
# because of the required key, so we can narrow to a simpler type here
reveal_type(arg2) # revealed: NonEmpty
if arg3:
reveal_type(arg3) # revealed: HasNotRequired1 & ~AlwaysFalsy
if arg4:
reveal_type(arg4) # revealed: HasNotRequired2 & ~AlwaysFalsy
if arg5:
reveal_type(arg5) # revealed: AlsoNonEmpty
When using a guard clause pattern (if not p: raise), the type should be narrowed in the
continuation:
from typing import TypedDict
class Person(TypedDict, total=False):
name: str
age: int
def get_person() -> Person | None:
return None
def test() -> None:
p = get_person()
if not p:
raise ValueError("No person")
reveal_type(p) # revealed: Person & ~AlwaysFalsy
# error: [invalid-key] "Unknown key "nonexistent" for TypedDict `Person`"
print(p["nonexistent"])
NewTypesfrom typing import NewType
FloatNewType = NewType("FloatNewType", float)
ComplexNewType = NewType("ComplexNewType", complex)
def expects_float(x: float): ...
def expects_complex(x: complex): ...
def f(floaty: FloatNewType, complexy: ComplexNewType):
if floaty:
reveal_type(floaty) # revealed:FloatNewType & ~AlwaysFalsy
expects_float(floaty) # fine
if complexy:
reveal_type(complexy) # revealed: ComplexNewType & ~AlwaysFalsy
expects_complex(complexy) # fine
expects_float(complexy) # error: [invalid-argument-type]