Skip to content

Conversation

@MatthewMckee4
Copy link
Contributor

@MatthewMckee4 MatthewMckee4 commented Dec 22, 2025

Summary

Resolves astral-sh/ty#2171

Test Plan

Add unsafe_tuple_subclass.md mdtest file.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 22, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 22, 2025

mypy_primer results

Changes were detected when running on open source projects
parso (https://github.com/davidhalter/parso)
+ parso/utils.py:151:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ parso/utils.py:158:9: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
- Found 199 diagnostics
+ Found 201 diagnostics

pip (https://github.com/pypa/pip)
+ src/pip/_vendor/rich/segment.py:100:9: warning[unsafe-tuple-subclass] Unsafe override of method `__bool__` in a subclass of `tuple`
+ src/pip/_vendor/rich/text.py:60:9: warning[unsafe-tuple-subclass] Unsafe override of method `__bool__` in a subclass of `tuple`
- Found 612 diagnostics
+ Found 614 diagnostics

kopf (https://github.com/nolar/kopf)
+ kopf/_cogs/structs/diffs.py:53:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ kopf/_cogs/structs/diffs.py:59:9: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
- Found 263 diagnostics
+ Found 265 diagnostics

graphql-core (https://github.com/graphql-python/graphql-core)
+ src/graphql/language/location.py:36:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ src/graphql/language/location.py:41:9: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
- Found 430 diagnostics
+ Found 432 diagnostics

pylint (https://github.com/pycqa/pylint)
+ pylint/checkers/symilar.py:186:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 216 diagnostics
+ Found 217 diagnostics

rich (https://github.com/Textualize/rich)
+ rich/segment.py:100:9: warning[unsafe-tuple-subclass] Unsafe override of method `__bool__` in a subclass of `tuple`
+ rich/text.py:60:9: warning[unsafe-tuple-subclass] Unsafe override of method `__bool__` in a subclass of `tuple`
- Found 348 diagnostics
+ Found 350 diagnostics

dedupe (https://github.com/dedupeio/dedupe)
+ dedupe/predicates.py:356:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 56 diagnostics
+ Found 57 diagnostics

mypy (https://github.com/python/mypy)
+ mypy/typeshed/stdlib/importlib/metadata/__init__.pyi:89:13: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ mypy/typeshed/stdlib/unittest/mock.pyi:90:13: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ mypy/typeshed/stdlib/unittest/mock.pyi:91:13: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
+ mypy/typeshed/stdlib/unittest/mock.pyi:121:13: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ mypy/typeshed/stdlib/unittest/mock.pyi:122:13: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
- Found 1780 diagnostics
+ Found 1785 diagnostics

Tanjun (https://github.com/FasterSpeeding/Tanjun)
- tanjun/dependencies/data.py:347:12: error[invalid-return-type] Return type does not match returned value: expected `_T@cached_inject`, found `_T@cached_inject | Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]]`
+ tanjun/dependencies/data.py:347:12: error[invalid-return-type] Return type does not match returned value: expected `_T@cached_inject`, found `Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]] | _T@cached_inject`

discord.py (https://github.com/Rapptz/discord.py)
+ discord/app_commands/namespace.py:53:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 562 diagnostics
+ Found 563 diagnostics

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
+ pymongo/message.py:1894:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ pymongo/message.py:1899:9: warning[unsafe-tuple-subclass] Unsafe override of method `__ne__` in a subclass of `tuple`
- Found 448 diagnostics
+ Found 450 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 42 diagnostics
+ Found 43 diagnostics

prefect (https://github.com/PrefectHQ/prefect)
+ src/prefect/utilities/annotations.py:33:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 5537 diagnostics
+ Found 5538 diagnostics

jax (https://github.com/google/jax)
+ jax/_src/pjit.py:129:7: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 2803 diagnostics
+ Found 2804 diagnostics

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Top[Index[Any]] | Top[Series[Any, Any]] | TypeBlocks | ... omitted 6 union elements, object_]`
- static_frame/core/bus.py:675:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Top[Index[Any]] | TypeBlocks | Top[Bus[Any]] | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/bus.py:675:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Self@iloc | Bus[Any], object_ | Self@iloc]`
- static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Top[Index[Any]] | TypeBlocks | Top[Bus[Any]] | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/series.py:772:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Series[Any, Any], TVDtype@Series]`, found `InterGetItemILocReduces[Top[Index[Any]] | Top[Series[Any, Any]] | TypeBlocks | ... omitted 6 union elements, generic[object]]`
- static_frame/core/series.py:4072:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[SeriesHE[Any, Any], TVDtype@SeriesHE]`, found `InterGetItemILocReduces[Top[Index[Any]] | TypeBlocks | Top[Bus[Any]] | ... omitted 7 union elements, generic[object]]`
+ static_frame/core/series.py:4072:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[SeriesHE[Any, Any], TVDtype@SeriesHE]`, found `InterGetItemILocReduces[Top[Index[Any]] | Top[Series[Any, Any]] | TypeBlocks | ... omitted 6 union elements, generic[object]]`
- static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Top[Yarn[Any]] | TypeBlocks | Batch | ... omitted 6 union elements, generic[object]]`
+ static_frame/core/yarn.py:418:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Yarn[Any], object_]`, found `InterGetItemILocReduces[Top[Index[Any]] | Top[Series[Any, Any]] | Top[Yarn[Any]] | ... omitted 6 union elements, generic[object]]`
- Found 1849 diagnostics
+ Found 1848 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ tests/frame/test_groupby.py:228:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
+ tests/frame/test_groupby.py:624:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 5104 diagnostics
+ Found 5106 diagnostics

sympy (https://github.com/sympy/sympy)
+ sympy/combinatorics/free_groups.py:706:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 15345 diagnostics
+ Found 15346 diagnostics

rotki (https://github.com/rotki/rotki)
+ rotkehlchen/chain/bitcoin/xpub.py:53:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
+ rotkehlchen/types.py:399:9: warning[unsafe-tuple-subclass] Unsafe override of method `__eq__` in a subclass of `tuple`
- Found 2095 diagnostics
+ Found 2097 diagnostics

No memory usage changes detected ✅

Comment on lines 4065 to 4074
let mut diagnostic =
builder.into_diagnostic(format_args!("Unsafe override of method `{member}`"));

diagnostic.set_primary_message(format_args!(
"Unsafely overriding method `{member}` in a subclass of `tuple`"
));

diagnostic.help(format!(
"Overriding `{member}` can cause unexpected behavior"
));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how much we should explain why it is unsafe to override certain methods.

@ntBre ntBre added the ty Multi-file analysis & type inference label Dec 23, 2025
@MatthewMckee4 MatthewMckee4 changed the title Emit diagnostic on tuple subclasses that override certain dunder methods [ty] Warn on tuple subclasses that override certain dunder methods Dec 23, 2025
@MatthewMckee4 MatthewMckee4 marked this pull request as ready for review December 23, 2025 09:45
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

This looks like it's a bit too trigger-happy right now:

  • I don't think we ever need to issue unsafe-tuple-subclass for a __len__ override. Our existing Liskov Substitution Principle check takes care of that for us
  • For other methods such as __bool__, __eq__, etc., I think we should avoid emitting this lint if the override would anyway cause us to emit a Liskov diagnostic. For something like this, we don't yet emit a Liskov diagnostic, but we should, so I don't think it should be covered by this check:
    class Foo(tuple[int]):
        __len__ = None
  • I don't think we need to issue unsafe-tuple-subclass for a __bool__ override unless we would infer the tuple superclass as either always-truthy or always-falsy. It's fine to override __bool__ on a tuple subclass if the superclass has ambiguous truthiness; that won't violate any of our type-inference or narrowing assumptions elsewhere

}
}

const PROHIBITED_TUPLE_SUBCLASS_METHODS: &[&str] = &["__eq__", "__len__", "__bool__"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other methods where we have special-cased behaviour for tuples are __lt__, __le__, __gt__, __ge__, __ne__, and __contains__. We probably need to ban overriding all of those too, unfortunately.

But as I said elsewhere, I think we can get rid of __len__ from this list (it's already covered by our Liskov checks). And there are many situations where overriding __bool__ on a tuple subclass should be fine.

We have very special-cased behaviour for __getitem__, but that should also be taken care of by our Liskov checks once we've finished implementing them, so I don't think you need to worry about __getitem__.

Copy link
Member

@AlexWaygood AlexWaygood Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have very special-cased behaviour for __getitem__, but that should also be taken care of by our Liskov checks once we've finished implementing them, so I don't think you need to worry about __getitem__.

Ah, no, we do need to ban __getitem__ overrides too, I think, because our special casing for slices of tuples is not reflected in our synthesized __getitem__ overloads for specialized tuples.

And there is maybe one specific kind of __len__ override that we need to ban, which is __len__ being overridden on subclasses of "mixed" tuples. tuple[int, *tuple[int, ...]] means "a tuple of length at least one, but of potentially arbitrary length". Either of these overrides would not be caught by our Liskov checks, but both are clearly inconsistent with the tuple spec of the superclass:

from typing import Literal

class Foo(tuple[int, *tuple[int, ...]]):
    def __len__(self) -> int:
        return 2

class Bar(tuple[int, *tuple[int, ...]):
    def __len__(self) -> Literal[2]:
        return 2

@MatthewMckee4
Copy link
Contributor Author

Perhaps this can be repurposed to just cover __eq__, __ne__ and __bool__

@AlexWaygood
Copy link
Member

Perhaps this can be repurposed to just cover __eq__, __ne__ and __bool__

Sure!

@MatthewMckee4 MatthewMckee4 marked this pull request as draft December 23, 2025 16:21
@MatthewMckee4 MatthewMckee4 marked this pull request as ready for review December 23, 2025 16:22
@MatthewMckee4
Copy link
Contributor Author

I maybe need some advice on diagnostic messages

@MatthewMckee4 MatthewMckee4 changed the title [ty] Warn on tuple subclasses that override certain dunder methods [ty] Emit diagnostic on tuple subclasses that override __eq__, __ne__ or __bool__ methods Dec 23, 2025
@carljm carljm removed their request for review December 24, 2025 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

emit a diagnostic on unsafe dunder method overrides on tuple subclasses

3 participants