ruff/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
cake-monotone b6ffa51c16
[red-knot] Type inference for comparisons between arbitrary instances (#13903)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@oddbird.net>
2024-10-26 18:19:56 +00:00

5.5 KiB

Comparison: Tuples

Heterogeneous

For tuples like tuple[int, str, Literal[1]]

Value Comparisons

"Value Comparisons" refers to the operators: ==, !=, <, <=, >, >=

Results without Ambiguity

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]

Results with Ambiguity

def bool_instance() -> bool: ...
def int_instance() -> int:
    return 42

a = (bool_instance(),)
b = (int_instance(),)

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

Comparison Unsupported

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")

# TODO: should be Literal[False], once we implement (in)equality for mismatched literals
reveal_type(a == b)  # revealed: bool

# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
reveal_type(a != b)  # revealed: bool

# TODO: should be Unknown and add more informative diagnostics
reveal_type(a < b)  # revealed: bool
reveal_type(a <= b)  # revealed: bool
reveal_type(a > b)  # revealed: bool
reveal_type(a >= b)  # revealed: bool

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]

Matryoshka Tuples

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]

Non Boolean Rich Comparisons

class A:
    def __eq__(self, o) -> str: ...
    def __ne__(self, o) -> int: ...
    def __lt__(self, o) -> float: ...
    def __le__(self, o) -> object: ...
    def __gt__(self, o) -> tuple: ...
    def __ge__(self, o) -> list: ...

a = (A(), A())

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

Membership Test Comparisons

"Membership Test Comparisons" refers to the operators in and not in.

def int_instance() -> int:
    return 42

a = (1, 2)
b = ((3, 4), (1, 2))
c = ((1, 2, 3), (4, 5, 6))
d = ((int_instance(), int_instance()), (int_instance(), int_instance()))

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

Identity Comparisons

"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

# TODO should be Literal[False] once we implement comparison of mismatched literal types
reveal_type(a is b)  # revealed: bool
# TODO should be Literal[True] once we implement comparison of mismatched literal types
reveal_type(a is not b)  # revealed: bool

reveal_type(a is c)  # revealed: Literal[False]
reveal_type(a is not c)  # revealed: Literal[True]

Homogeneous

For tuples like tuple[int, ...], tuple[Any, ...]

// TODO