crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md
!= and == conditionalsx != Nonedef _(flag: bool):
x = None if flag else 1
if x != None:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
None != x (reversed operands)def _(flag: bool):
x = None if flag else 1
if None != x:
reveal_type(x) # revealed: Literal[1]
else:
reveal_type(x) # revealed: None
This also works for == with reversed operands:
def _(flag: bool):
x = None if flag else 1
if None == x:
reveal_type(x) # revealed: None
else:
reveal_type(x) # revealed: Literal[1]
!= for other singleton typesdef _(x: bool):
if x != False:
reveal_type(x) # revealed: Literal[True]
else:
reveal_type(x) # revealed: Literal[False]
def _(x: bool):
if x == False:
reveal_type(x) # revealed: Literal[False]
else:
reveal_type(x) # revealed: Literal[True]
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
def _(answer: Answer):
if answer != Answer.NO:
reveal_type(answer) # revealed: Literal[Answer.YES]
else:
reveal_type(answer) # revealed: Literal[Answer.NO]
def _(answer: Answer):
if answer == 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 != Single.VALUE:
reveal_type(x) # revealed: int
else:
# `int` is not eliminated here because there could be subclasses of `int` with custom `__eq__`/`__ne__` methods
reveal_type(x) # revealed: Single | int
def _(x: Single | int):
if x == Single.VALUE:
reveal_type(x) # revealed: Single | int
else:
reveal_type(x) # revealed: int
This narrowing behavior is only safe if the enum has no custom __eq__/__ne__ method:
from enum import Enum
class AmbiguousEnum(Enum):
NO = 0
YES = 1
def __ne__(self, other) -> bool:
return True
def _(answer: AmbiguousEnum):
if answer != AmbiguousEnum.NO:
reveal_type(answer) # revealed: AmbiguousEnum
else:
reveal_type(answer) # revealed: AmbiguousEnum
Similar if that method is inherited from a base class:
from enum import Enum
class Mixin:
def __eq__(self, other) -> bool:
return True
class AmbiguousEnum(Mixin, Enum):
NO = 0
YES = 1
def _(answer: AmbiguousEnum):
if answer == AmbiguousEnum.NO:
reveal_type(answer) # revealed: AmbiguousEnum
else:
reveal_type(answer) # revealed: AmbiguousEnum
x != y where y is of literal typedef _(flag: bool):
x = 1 if flag else 2
if x != 1:
reveal_type(x) # revealed: Literal[2]
x != y where y is a single-valued typedef _(flag: bool):
class A: ...
class B: ...
C = A if flag else B
if C != A:
reveal_type(C) # revealed: <class 'B'>
else:
reveal_type(C) # revealed: <class 'A'>
x != y where y has multiple single-valued optionsdef _(flag1: bool, flag2: bool):
x = 1 if flag1 else 2
y = 2 if flag2 else 3
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[2]
== with PEP 695 alias to a union of literals[environment]
python-version = "3.12"
from typing import Literal
type Y = Literal[2, 3]
def _(x: Literal[1, 2], y: Y):
if x == y:
reveal_type(x) # revealed: Literal[2]
else:
reveal_type(x) # revealed: Literal[1, 2]
!= for non-single-valued typesOnly single-valued types should narrow the type:
def _(flag: bool, a: int, y: int):
x = a if flag else None
if x != y:
reveal_type(x) # revealed: int | None
def _(flag1: bool, flag2: bool, a: int):
x = 1 if flag1 else 2
y = 2 if flag2 else a
if x != y:
reveal_type(x) # revealed: Literal[1, 2]
else:
reveal_type(x) # revealed: Literal[1, 2]
== / != with two narrowable operandsBoth operands should be narrowed when both are narrowable expressions.
from typing import Literal
def _(x: Literal[1], y: Literal[1, 2]):
if x == y:
reveal_type(y) # revealed: Literal[1]
if y == x:
reveal_type(y) # revealed: Literal[1]
if x != y:
reveal_type(y) # revealed: Literal[2]
if y != x:
reveal_type(y) # revealed: Literal[2]
from typing import Literal
def f() -> Literal[1, 2, 3]:
return 1
if (x := f()) != 1:
reveal_type(x) # revealed: Literal[2, 3]
else:
reveal_type(x) # revealed: Literal[1]
Anyfrom typing import Any
def _(x: Any | None, y: Any | None):
if x != 1:
reveal_type(x) # revealed: (Any & ~Literal[1]) | None
if y == 1:
reveal_type(y) # revealed: Any & ~None
from typing import Literal
def _(b: bool, i: Literal[1, 2]):
if b == 1:
reveal_type(b) # revealed: Literal[True]
else:
reveal_type(b) # revealed: Literal[False]
if b == 6:
reveal_type(b) # revealed: Never
else:
reveal_type(b) # revealed: bool
if b == 0:
reveal_type(b) # revealed: Literal[False]
else:
reveal_type(b) # revealed: Literal[True]
if i == True:
reveal_type(i) # revealed: Literal[1]
else:
reveal_type(i) # revealed: Literal[2]
LiteralString in unionfrom typing_extensions import Literal, LiteralString, Any
def _(s: LiteralString | None, t: LiteralString | Any):
if s == "foo":
reveal_type(s) # revealed: Literal["foo"]
elif s == "bar":
reveal_type(s) # revealed: Literal["bar"]
else:
reveal_type(s) # revealed: (LiteralString & ~Literal["foo"] & ~Literal["bar"]) | None
if s == 1:
reveal_type(s) # revealed: Never
if t == "foo":
# TODO could be `Literal["foo"] | Any`
reveal_type(t) # revealed: LiteralString | Any
We assume that tuple subclasses don't override tuple.__eq__, which only returns True for other
tuples. So they are excluded from the narrowed type when comparing to non-tuple values.
from typing import Literal
def _(x: Literal["a", "b"] | tuple[int, int]):
if x == "a":
# tuple type is excluded because it's disjoint from the string literal
reveal_type(x) # revealed: Literal["a"]
else:
# tuple type remains in the else branch
reveal_type(x) # revealed: Literal["b"] | tuple[int, int]