crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
For tuples like tuple[int, str, Literal[1]]
"Value Comparisons" refers to the operators: ==, !=, <, <=, >, >=
Cases where the result can be definitively inferred as a BooleanLiteral.
a = (1, "test", (3, 13), True)
b = (1, "test", (3, 14), False)
reveal_type(a == a) # revealed: Literal[True]
reveal_type(a != a) # revealed: Literal[False]
reveal_type(a < a) # revealed: Literal[False]
reveal_type(a <= a) # revealed: Literal[True]
reveal_type(a > a) # revealed: Literal[False]
reveal_type(a >= a) # revealed: Literal[True]
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
Even when tuples have different lengths, comparisons should be handled appropriately.
a = (1, 2, 3)
b = (1, 2, 3, 4)
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
c = ("a", "b", "c", "d")
d = ("a", "b", "c")
reveal_type(c == d) # revealed: Literal[False]
reveal_type(c != d) # revealed: Literal[True]
reveal_type(c < d) # revealed: Literal[False]
reveal_type(c <= d) # revealed: Literal[False]
reveal_type(c > d) # revealed: Literal[True]
reveal_type(c >= d) # revealed: Literal[True]
def _(x: bool, y: int):
a = (x,)
b = (y,)
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
If two tuples contain types that do not support comparison, the result may be Unknown. However,
== and != are exceptions and can still provide definite results.
a = (1, 2)
b = (1, "hello")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[Literal[1], Literal[2]]` and `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a < b) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported between objects of type `tuple[Literal[1], Literal[2]]` and `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a <= b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported between objects of type `tuple[Literal[1], Literal[2]]` and `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a > b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported between objects of type `tuple[Literal[1], Literal[2]]` and `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a >= b) # revealed: Unknown
# error: [unsupported-operator]
# error: [unsupported-operator]
reveal_type((object(),) < (object(),) < (object(),)) # revealed: Unknown
However, if the lexicographic comparison completes without reaching a point where str and int are compared, Python will still produce a result based on the prior elements.
a = (1, 2)
b = (999999, "hello")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
a = (1, True, "Hello")
b = (a, a, a)
c = (b, b, b)
reveal_type(c == c) # revealed: Literal[True]
reveal_type(c != c) # revealed: Literal[False]
reveal_type(c < c) # revealed: Literal[False]
reveal_type(c <= c) # revealed: Literal[True]
reveal_type(c > c) # revealed: Literal[False]
reveal_type(c >= c) # revealed: Literal[True]
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference should be possible even in cases where these methods return non-boolean types.
Note: Tuples use lexicographic comparisons. If the == result for all paired elements in the tuple
is True, the comparison then considers the tuple’s length. Regardless of the return type of the
dunder methods, the final result can still be a boolean value.
(+cpython: For tuples, == and != always produce boolean results, regardless of the return type
of the dunder methods.)
from __future__ import annotations
from typing import Literal
class EqReturnType: ...
class NeReturnType: ...
class LtReturnType: ...
class LeReturnType: ...
class GtReturnType: ...
class GeReturnType: ...
class A:
def __eq__(self, o: object) -> EqReturnType: # error: [invalid-method-override]
return EqReturnType()
def __ne__(self, o: object) -> NeReturnType: # error: [invalid-method-override]
return NeReturnType()
def __lt__(self, o: A) -> LtReturnType:
return LtReturnType()
def __le__(self, o: A) -> LeReturnType:
return LeReturnType()
def __gt__(self, o: A) -> GtReturnType:
return GtReturnType()
def __ge__(self, o: A) -> GeReturnType:
return GeReturnType()
a = (A(), A())
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: LtReturnType | Literal[False]
reveal_type(a <= a) # revealed: LeReturnType | Literal[True]
reveal_type(a > a) # revealed: GtReturnType | Literal[False]
reveal_type(a >= a) # revealed: GeReturnType | Literal[True]
# If lexicographic comparison is finished before comparing A()
b = ("1_foo", A())
c = ("2_bar", A())
reveal_type(b == c) # revealed: Literal[False]
reveal_type(b != c) # revealed: Literal[True]
reveal_type(b < c) # revealed: Literal[True]
reveal_type(b <= c) # revealed: Literal[True]
reveal_type(b > c) # revealed: Literal[False]
reveal_type(b >= c) # revealed: Literal[False]
class LtReturnTypeOnB: ...
class B:
def __lt__(self, o: B) -> LtReturnTypeOnB:
return LtReturnTypeOnB()
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
class LaterLtReturnType: ...
class CustomEq:
def __eq__(self, other: object) -> Literal[True]:
return True
class Later:
def __lt__(self, other: Later) -> LaterLtReturnType:
return LaterLtReturnType()
reveal_type((CustomEq(), Later()) < (CustomEq(), Later())) # revealed: LaterLtReturnType | Literal[False]
Example:
(<int instance>, "foo") == (<int instance>, "bar")
Eq and NotEq have unique behavior compared to other operators in lexicographic comparisons.
Specifically, for Eq, if any non-equal pair exists within the tuples being compared, we can
immediately conclude that the tuples are not equal. Conversely, for NotEq, if any non-equal pair
exists, we can determine that the tuples are unequal.
In contrast, with operators like < and >, the comparison must consider each pair of elements
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
def _(x: str, y: int):
reveal_type("foo" == "bar") # revealed: Literal[False]
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
a = (x, y, "foo")
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bool
reveal_type(a <= a) # revealed: bool
reveal_type(a > a) # revealed: bool
reveal_type(a >= a) # revealed: bool
b = (x, y, "bar")
reveal_type(a == b) # revealed: Literal[False]
reveal_type(a != b) # revealed: Literal[True]
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
c = (x, y, "foo", "different_length")
reveal_type(a == c) # revealed: Literal[False]
reveal_type(a != c) # revealed: Literal[True]
reveal_type(a < c) # revealed: bool
reveal_type(a <= c) # revealed: bool
reveal_type(a > c) # revealed: bool
reveal_type(a >= c) # revealed: bool
Errors occurring within a tuple comparison should propagate outward. However, if the tuple comparison can clearly conclude before encountering an error, the error should not be raised.
def _(n: int, s: str):
class A: ...
# error: [unsupported-operator] "Operator `<` is not supported between two objects of type `A`"
A() < A()
# error: [unsupported-operator] "Operator `<=` is not supported between two objects of type `A`"
A() <= A()
# error: [unsupported-operator] "Operator `>` is not supported between two objects of type `A`"
A() > A()
# error: [unsupported-operator] "Operator `>=` is not supported between two objects of type `A`"
A() >= A()
a = (0, n, A())
# error: [unsupported-operator] "Operator `<` is not supported between two objects of type `tuple[Literal[0], int, A]`"
reveal_type(a < a) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported between two objects of type `tuple[Literal[0], int, A]`"
reveal_type(a <= a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported between two objects of type `tuple[Literal[0], int, A]`"
reveal_type(a > a) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported between two objects of type `tuple[Literal[0], int, A]`"
reveal_type(a >= a) # revealed: Unknown
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
# and should terminate immediately.
b = (99999, n, A())
reveal_type(a < b) # revealed: Literal[True]
reveal_type(a <= b) # revealed: Literal[True]
reveal_type(a > b) # revealed: Literal[False]
reveal_type(a >= b) # revealed: Literal[False]
"Membership Test Comparisons" refers to the operators in and not in.
def _(n: int):
a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((n, n), (n, n))
reveal_type(a in b) # revealed: Literal[True]
reveal_type(a not in b) # revealed: Literal[False]
reveal_type(a in c) # revealed: Literal[False]
reveal_type(a not in c) # revealed: Literal[True]
reveal_type(a in d) # revealed: bool
reveal_type(a not in d) # revealed: bool
Membership in a fixed-length tuple compares each element with the needle, regardless of whether the needle is itself a tuple:
from typing import Literal
def scalar_membership(value: int, values: tuple[Literal[1], Literal[2]]):
reveal_type(1 in values) # revealed: Literal[True]
reveal_type(3 in values) # revealed: Literal[False]
reveal_type(value in values) # revealed: bool
reveal_type(1 not in values) # revealed: Literal[False]
reveal_type(3 not in values) # revealed: Literal[True]
reveal_type(value not in values) # revealed: bool
def empty_tuple(value: object, values: tuple[()]):
reveal_type(value in values) # revealed: Literal[False]
reveal_type(value not in values) # revealed: Literal[True]
A variable-length tuple might be empty, even if all its possible elements would compare equal to the needle:
from typing import Literal
def variable_length(
nested: tuple[tuple[()], ...],
values: tuple[Literal[1], ...],
):
reveal_type(() in nested) # revealed: bool
reveal_type(() not in nested) # revealed: bool
reveal_type(1 in values) # revealed: bool
reveal_type(1 not in values) # revealed: bool
Tuple membership checks whether the needle is the same object as an element before comparing the objects for equality. A non-reflexive equality method therefore cannot establish that membership is always false:
from typing import Literal
class NeverEqual:
def __eq__(self, other: object) -> Literal[False]:
return False
class AlwaysEqual:
def __eq__(self, other: object) -> Literal[True]:
return True
def identity_before_equality(value: NeverEqual):
reveal_type(value == value) # revealed: Literal[False]
reveal_type(value in (value,)) # revealed: bool
reveal_type(value not in (value,)) # revealed: bool
def custom_equality(value: AlwaysEqual):
reveal_type(value in (1,)) # revealed: bool
reveal_type(value not in (1,)) # revealed: bool
reveal_type((value,) == (1,)) # revealed: Literal[True]
reveal_type((value,) != (1,)) # revealed: Literal[False]
def custom_equality_union(value: AlwaysEqual | None):
reveal_type(value in (1,)) # revealed: bool
reveal_type(value not in (1,)) # revealed: bool
def custom_equality_union_member(value: AlwaysEqual | None, member: AlwaysEqual):
reveal_type(value.__eq__(member)) # revealed: bool
reveal_type(member.__eq__(value)) # revealed: Literal[True]
reveal_type(value in (member,)) # revealed: Literal[True]
reveal_type(value not in (member,)) # revealed: Literal[False]
reveal_type((value,) == (member,)) # revealed: bool
reveal_type((value,) != (member,)) # revealed: bool
class Base:
def __eq__(self, other: object) -> bool:
return False
class AlwaysEqualChild(Base):
def __eq__(self, other: object) -> Literal[True]:
return True
def reflected_custom_equality(value: Base, child: AlwaysEqualChild):
reveal_type(value == child) # revealed: bool
reveal_type(child == value) # revealed: Literal[True]
reveal_type(value in (child,)) # revealed: Literal[True]
reveal_type(value not in (child,)) # revealed: Literal[False]
"Identity Comparisons" refers to is and is not.
a = (1, 2)
b = ("a", "b")
c = (1, 2, 3)
reveal_type(a is (1, 2)) # revealed: bool
reveal_type(a is not (1, 2)) # revealed: bool
reveal_type(a is b) # revealed: Literal[False]
reveal_type(a is not b) # revealed: Literal[True]
reveal_type(a is c) # revealed: Literal[False]
reveal_type(a is not c) # revealed: Literal[True]
For tuples like tuple[int, ...], tuple[Any, ...]
Comparisons between homogeneous tuples with incompatible element types should emit diagnostics for
ordering operators (<, <=, >, >=), but not for equality operators (==, !=).
def f(
a: tuple[int, ...],
b: tuple[str, ...],
c: tuple[str],
):
# Equality comparisons are always valid
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
# Ordering comparisons between incompatible types should emit errors
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
a < b
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
b < a
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
a < c
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
c < a
When comparing fixed-length tuples with variable-length tuples, all element types that could potentially be compared must be compatible.
def _(
var_int: tuple[int, ...],
var_str: tuple[str, ...],
fixed_int_str: tuple[int, str],
):
# Fixed `tuple[int, str]` vs. variable `tuple[int, ...]`:
# Position 0: `int` vs. `int` are comparable.
# Position 1 (if `var_int` has 2+ elements): `str` vs. `int` are not comparable.
# error: [unsupported-operator]
fixed_int_str < var_int
# Variable `tuple[int, ...]` vs. fixed `tuple[int, str]`:
# Position 0: `int` vs. `int` are comparable.
# Position 1 (if `var_int` has 2+ elements): `int` vs. `str` are not comparable.
# error: [unsupported-operator]
var_int < fixed_int_str
# Variable `tuple[str, ...]` vs. fixed `tuple[int, str]`:
# Position 0: `str` vs. `int` are not comparable.
# error: [unsupported-operator]
var_str < fixed_int_str
Comparisons between homogeneous tuples with compatible element types should work.
def _(a: tuple[int, ...], b: tuple[int, ...], c: tuple[bool, ...]):
# Same element types - always valid
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# int and bool are compatible for comparison
reveal_type(a < c) # revealed: bool
reveal_type(c < a) # revealed: bool
Variable-length tuples with prefixes and suffixes are also checked.
[environment]
python-version = "3.11"
def _(
prefix_int_var_str: tuple[int, *tuple[str, ...]],
prefix_str_var_int: tuple[str, *tuple[int, ...]],
):
# Prefix `int` vs. prefix `str` are not comparable.
# error: [unsupported-operator]
prefix_int_var_str < prefix_str_var_int
Tuples with compatible prefixes/suffixes are allowed.
def _(
prefix_int_var_int: tuple[int, *tuple[int, ...]],
prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
):
# Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable.
reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
__bool__For an operation A() < A() to succeed at runtime, the A.__lt__ method does not necessarily need
to return an object that is convertible to a bool. However, the return type does need to be
convertible to a bool for the operation A() < A() < A() (a chained comparison) to succeed.
This is because A() < A() < A() desugars to something like this, which involves several implicit
conversions to bool:
def compute_chained_comparison():
a1 = A()
a2 = A()
first_comparison = a1 < a2
return first_comparison and (a2 < A())
class NotBoolable:
__bool__: int = 5
class Comparable:
def __lt__(self, other) -> NotBoolable:
return NotBoolable()
def __gt__(self, other) -> NotBoolable:
return NotBoolable()
a = (1, Comparable())
b = (1, Comparable())
# error: [unsupported-bool-conversion]
a < b < b
a < b # fine
__bool__Python does not generally attempt to coerce the result of == and != operations between two
arbitrary objects to a bool, but a comparison of tuples will fail if the result of comparing any
pair of elements at equivalent positions cannot be converted to a bool:
class NotBoolable:
__bool__: None = None
class A:
# error: [invalid-method-override]
def __eq__(self, other) -> NotBoolable:
return NotBoolable()
# error: [unsupported-bool-conversion]
reveal_type((A(),) == (A(),)) # revealed: bool
# error: [unsupported-bool-conversion]
reveal_type((A(), "x") == (A(), "y")) # revealed: Literal[False]
# error: [unsupported-bool-conversion]
reveal_type((A(),) != (A(), 0)) # revealed: Literal[True]
Tuple identity comparisons do not compare elements and therefore do not coerce their equality
results to bool:
def tuple_identity(left: tuple[A], right: tuple[A]) -> None:
reveal_type(left is right) # revealed: bool
reveal_type(left is not right) # revealed: bool
from __future__ import annotations
from typing import NamedTuple
class Node(NamedTuple):
parent: Node | None
def _(n: Node):
reveal_type(n.parent is n) # revealed: bool