Skip to content

Commit 92cda7b

Browse files
committed
Check element type comparability for tuple ordering comparisons
When comparing tuples with ordering operators (<, >, <=, >=), mypy now verifies that the element types actually support the comparison operator. Previously, due to tuple's covariant TypeVar, the reverse operator could pass at the type level even when element types don't support the ordering comparison at runtime. For example, tuple[object, ...].__gt__ would accept any tuple argument via covariance, but object doesn't define __gt__. The fix adds element-level validation after the tuple-level check succeeds: if the element types can't be compared with the same operator, an 'Unsupported operand types' error is reported. Fixes #21042
1 parent 9790459 commit 92cda7b

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

mypy/checkexpr.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3706,6 +3706,17 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
37063706
right_type = try_getting_literal(right_type)
37073707
self.msg.dangerous_comparison(left_type, right_type, "equality", e)
37083708

3709+
# For ordering comparisons on tuples, verify that element types
3710+
# actually support the comparison. The tuple stubs use a covariant
3711+
# TypeVar which can allow the reverse operator to pass even when
3712+
# element types don't support the comparison at runtime.
3713+
if not w.has_new_errors() and operator in ("<", ">", "<=", ">="):
3714+
right_type = self.accept(right)
3715+
if not self.chk.can_skip_diagnostics:
3716+
self._check_tuple_element_comparison(
3717+
operator, left_type, right_type, e
3718+
)
3719+
37093720
elif operator == "is" or operator == "is not":
37103721
right_type = self.accept(right) # validate the right operand
37113722
sub_result = self.bool_type()
@@ -3876,6 +3887,52 @@ def dangerous_comparison(
38763887
return False
38773888
return not is_overlapping_types(left, right, ignore_promotions=False)
38783889

3890+
def _check_tuple_element_comparison(
3891+
self,
3892+
operator: str,
3893+
left_type: Type,
3894+
right_type: Type,
3895+
context: Context,
3896+
) -> None:
3897+
"""Check that tuple element types support an ordering comparison.
3898+
3899+
Tuple comparisons are element-wise at runtime, but the typeshed stubs
3900+
use a covariant TypeVar which can allow comparisons to pass at the type
3901+
level even when element types don't support the operator.
3902+
"""
3903+
left_proper = get_proper_type(left_type)
3904+
right_proper = get_proper_type(right_type)
3905+
3906+
left_elem = self._get_tuple_item_type(left_proper)
3907+
right_elem = self._get_tuple_item_type(right_proper)
3908+
3909+
if left_elem is None or right_elem is None:
3910+
return
3911+
3912+
# Skip check if either element type is Any
3913+
left_elem_proper = get_proper_type(left_elem)
3914+
right_elem_proper = get_proper_type(right_elem)
3915+
if isinstance(left_elem_proper, AnyType) or isinstance(right_elem_proper, AnyType):
3916+
return
3917+
3918+
method = operators.op_methods[operator]
3919+
with self.msg.filter_errors() as w:
3920+
self.check_op(
3921+
method,
3922+
left_elem,
3923+
TempNode(right_elem, context=context),
3924+
context,
3925+
allow_reverse=True,
3926+
)
3927+
if w.has_new_errors():
3928+
self.msg.unsupported_operand_types(operator, left_type, right_type, context)
3929+
3930+
def _get_tuple_item_type(self, typ: ProperType) -> Type | None:
3931+
"""Get the element type of a homogeneous tuple type, or None if not applicable."""
3932+
if isinstance(typ, Instance) and typ.type.fullname == "builtins.tuple":
3933+
return typ.args[0] if typ.args else None
3934+
return None
3935+
38793936
def check_method_call_by_name(
38803937
self,
38813938
method: str,

test-data/unit/check-tuples.test

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,3 +1838,59 @@ from typing_extensions import Concatenate
18381838
def c(t: tuple[Concatenate[int, ...]]) -> None: # E: Cannot use "[int, VarArg(Any), KwArg(Any)]" for tuple, only for ParamSpec
18391839
reveal_type(t) # N: Revealed type is "tuple[Any]"
18401840
[builtins fixtures/tuple.pyi]
1841+
1842+
[case testTupleComparisonNonComparableElements]
1843+
from typing import Tuple
1844+
a: Tuple[object, ...]
1845+
b: Tuple[object, ...]
1846+
c = a < b # E: Unsupported operand types for < ("tuple[object, ...]" and "tuple[object, ...]")
1847+
d = a > b # E: Unsupported operand types for > ("tuple[object, ...]" and "tuple[object, ...]")
1848+
e = a <= b # E: Unsupported operand types for <= ("tuple[object, ...]" and "tuple[object, ...]")
1849+
f = a >= b # E: Unsupported operand types for >= ("tuple[object, ...]" and "tuple[object, ...]")
1850+
[builtins fixtures/ops.pyi]
1851+
1852+
[case testTupleComparisonComparableElements]
1853+
from typing import Tuple
1854+
a: Tuple[int, ...]
1855+
b: Tuple[int, ...]
1856+
c = a < b
1857+
d = a > b
1858+
e = a <= b
1859+
f = a >= b
1860+
[builtins fixtures/ops.pyi]
1861+
1862+
[case testTupleComparisonOnlyGT]
1863+
from typing import Tuple, Any
1864+
1865+
class OnlyGT:
1866+
def __gt__(self, other: 'OnlyGT') -> bool: ...
1867+
1868+
a: Tuple[OnlyGT, ...]
1869+
b: Tuple[object, ...]
1870+
c = a < b # E: Unsupported operand types for < ("tuple[OnlyGT, ...]" and "tuple[object, ...]")
1871+
[builtins fixtures/ops.pyi]
1872+
1873+
[case testTupleComparisonOnlyGTValid]
1874+
from typing import Tuple, Any
1875+
1876+
class OnlyGT:
1877+
def __gt__(self, other: 'OnlyGT') -> bool: ...
1878+
1879+
a: Tuple[OnlyGT, ...]
1880+
b: Tuple[OnlyGT, ...]
1881+
c = a > b
1882+
[builtins fixtures/ops.pyi]
1883+
1884+
[case testTupleComparisonCovariantReverse]
1885+
# Regression test for https://github.com/python/mypy/issues/21042
1886+
from typing import Tuple, Any
1887+
1888+
class OnlyGT:
1889+
def __gt__(self, other: Any) -> bool: ...
1890+
1891+
a: Tuple[OnlyGT, ...]
1892+
b: Tuple[object, ...]
1893+
# Tuple-level reverse op (tuple[object, ...].__gt__) passes due to covariance,
1894+
# but element types don't support <
1895+
c = a < b # E: Unsupported operand types for < ("tuple[OnlyGT, ...]" and "tuple[object, ...]")
1896+
[builtins fixtures/ops.pyi]

0 commit comments

Comments
 (0)