Skip to content

Comments

[ty] Proper assignability/subtyping checks for protocols with method members#20165

Merged
AlexWaygood merged 9 commits intomainfrom
alex/method-members
Sep 12, 2025
Merged

[ty] Proper assignability/subtyping checks for protocols with method members#20165
AlexWaygood merged 9 commits intomainfrom
alex/method-members

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Aug 30, 2025

Summary

When determining whether a nominal type N satisfies a protocol type P with a method member P.x, check that the signature of the method N.x is a subtype of the signature of P.x before deciding that N is a subtype of P.

Note that subtyping between two different types that are both protocols is a separate code path in instance.rs, and this PR doesn't touch that code path (I'll do that separately). So this doesn't yet provide a fix for e.g. astral-sh/ty#1089 or astral-sh/ty#733.

Fixes astral-sh/ty#637.
Fixes astral-sh/ty#889.

Test Plan

  • Mdtests updated
  • Ran QUICKCHECK_TESTS=1000000 cargo test --release -p ty_python_semantic -- --ignored types::property_tests::stable to check that the all_type_assignable_to_iterable_are_iterable test can be safely marked as stable.

Typing conformance suite impact

These all look good! All the lines where we're emitting new diagnostics have # E comments next to them.

Ecosystem impact

I analysed nearly all the mypy_primer diff in #20165 (comment). I don't think there's anything there that indicates a bug in protocol assignability/subtyping logic. Most new hits are due to one of:

  1. Us not understanding typeshed's overloads for builtins.open() (because that function uses PEP-613 type aliases). This is such a common source of new false positives that it might be worth us adding some temporary special-casing for open() where we infer it as returning Todo?
  2. Users using positional-or-keyword parameters in their protocol definitions, when they should probably be using positional-only parameters instead. I'm surprised this is showing up so much in the ecosystem because I would expect other type checkers to complain about this too. We should probably reimplement flake8-pyi's Y091 rule in Ruff and encourage users to enable it if they're type-checking their code with ty.

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Aug 30, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Aug 30, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-09-12 10:07:15.533009560 +0000
+++ new-output.txt	2025-09-12 10:07:18.541030789 +0000
@@ -720,6 +720,12 @@
 protocols_definition.py:159:1: error[invalid-assignment] Object of type `Concrete3_Bad4` is not assignable to `Template3`
 protocols_definition.py:160:1: error[invalid-assignment] Object of type `Concrete3_Bad5` is not assignable to `Template3`
 protocols_definition.py:219:1: error[invalid-assignment] Object of type `Concrete4_Bad2` is not assignable to `Template4`
+protocols_definition.py:285:1: error[invalid-assignment] Object of type `Concrete5_Bad1` is not assignable to `Template5`
+protocols_definition.py:286:1: error[invalid-assignment] Object of type `Concrete5_Bad2` is not assignable to `Template5`
+protocols_definition.py:287:1: error[invalid-assignment] Object of type `Concrete5_Bad3` is not assignable to `Template5`
+protocols_definition.py:288:1: error[invalid-assignment] Object of type `Concrete5_Bad4` is not assignable to `Template5`
+protocols_definition.py:289:1: error[invalid-assignment] Object of type `Concrete5_Bad5` is not assignable to `Template5`
+protocols_generic.py:40:1: error[invalid-assignment] Object of type `Concrete1` is not assignable to `Proto1[int, str]`
 protocols_merging.py:52:1: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable1`
 protocols_merging.py:53:1: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable2`
 protocols_merging.py:54:1: error[invalid-assignment] Object of type `SCConcrete2` is not assignable to `SizedAndClosable3`
@@ -848,5 +854,5 @@
 typeddicts_usage.py:28:1: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
 typeddicts_usage.py:28:18: error[invalid-key] Invalid key access on TypedDict `Movie`: Unknown key "title"
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 849 diagnostics
+Found 855 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 30, 2025

mypy_primer results

Changes were detected when running on open source projects
pytest-robotframework (https://github.com/detachhead/pytest-robotframework)
+ pytest_robotframework/__init__.py:264:13: error[invalid-assignment] Object of type `_FullStackStatusReporter | nullcontext[None]` is not assignable to `AbstractContextManager[object, bool]`
- Found 175 diagnostics
+ Found 176 diagnostics

nionutils (https://github.com/nion-software/nionutils)
+ nion/utils/StructuredModel.py:91:16: error[invalid-return-type] Return type does not match returned value: expected `ModelLike`, found `RecordModel`
+ nion/utils/StructuredModel.py:93:16: error[invalid-return-type] Return type does not match returned value: expected `ModelLike`, found `ArrayModel`
- Found 5 diagnostics
+ Found 7 diagnostics

yarl (https://github.com/aio-libs/yarl)
- yarl/_quoting_py.py:148:19: warning[redundant-cast] Value is already of type `BufferedIncrementalDecoder`
- Found 49 diagnostics
+ Found 48 diagnostics

antidote (https://github.com/Finistere/antidote)
+ tests/lib/interface/test_conditions.py:193:45: error[invalid-argument-type] Argument to bound method `when` is incorrect: Expected `Predicate[Weight | None] | Weight | None | NeutralWeight | bool`, found `OnlyIf`
- tests/lib/interface/test_custom.py:324:51: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:327:48: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:330:53: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:333:50: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:338:56: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:368:79: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:371:76: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:374:81: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:377:78: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:382:85: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:392:79: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:395:76: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:398:81: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:401:78: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- tests/lib/interface/test_custom.py:406:85: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 330 diagnostics
+ Found 316 diagnostics

hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- src/hydra_zen/structured_configs/_implementations.py:492:89: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 559 diagnostics
+ Found 558 diagnostics

rich (https://github.com/Textualize/rich)
+ tests/test_containers.py:17:17: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Iterable[Unknown]`, found `Renderables`
- Found 310 diagnostics
+ Found 311 diagnostics

comtypes (https://github.com/enthought/comtypes)
+ comtypes/test/test_variant.py:329:32: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
- Found 399 diagnostics
+ Found 400 diagnostics

isort (https://github.com/pycqa/isort)
+ isort/io.py:47:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `TextIOWrapper[_WrappedBuffer]` does not satisfy upper bound `_WrappedBuffer` of type variable `_BufferT_co`
+ isort/io.py:47:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `_WrappedBuffer`, found `TextIOWrapper[_WrappedBuffer]`
- Found 34 diagnostics
+ Found 36 diagnostics

schemathesis (https://github.com/schemathesis/schemathesis)
+ src/schemathesis/auths.py:323:53: error[invalid-argument-type] Argument is incorrect: Expected `AuthProvider[Unknown]`, found `RequestsAuth[Unknown]`
+ src/schemathesis/auths.py:351:17: error[invalid-assignment] Object of type `CachingAuthProvider[Unknown]` is not assignable to `AuthProvider[Unknown]`
+ src/schemathesis/auths.py:353:17: error[invalid-assignment] Object of type `KeyedCachingAuthProvider[Unknown]` is not assignable to `AuthProvider[Unknown]`
+ src/schemathesis/auths.py:360:13: error[invalid-assignment] Object of type `SelectiveAuthProvider[Unknown]` is not assignable to `AuthProvider[Unknown]`
- Found 267 diagnostics
+ Found 271 diagnostics

vision (https://github.com/pytorch/vision)
- test/datasets_utils.py:1045:9: error[invalid-assignment] Object of type `LiteralString` is not assignable to `tuple[str, ...]`
+ torchvision/datasets/lsun.py:28:37: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ torchvision/datasets/lsun.py:32:36: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
- Found 1459 diagnostics
+ Found 1460 diagnostics

pywin32 (https://github.com/mhammond/pywin32)
+ Pythonwin/pywin/scintilla/config.py:104:40: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:107:53: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:108:46: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:109:45: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:110:46: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:117:55: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:163:55: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:164:64: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:165:59: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:166:54: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:167:55: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ Pythonwin/pywin/scintilla/config.py:168:42: error[invalid-argument-type] Argument to function `dump` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ com/win32com/client/gencache.py:86:28: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `SupportsWrite[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ com/win32com/client/gencache.py:130:30: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `_ReadableFileobj`, found `BytesIO | TextIOWrapper[_WrappedBuffer]`
- Found 1986 diagnostics
+ Found 2000 diagnostics

tornado (https://github.com/tornadoweb/tornado)
+ tornado/websocket.py:773:16: error[invalid-return-type] Return type does not match returned value: expected `_Compressor`, found `_Compress`
+ tornado/websocket.py:810:16: error[invalid-return-type] Return type does not match returned value: expected `_Decompressor`, found `_Decompress`
- Found 246 diagnostics
+ Found 248 diagnostics

urllib3 (https://github.com/urllib3/urllib3)
- test/test_response.py:743:36: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- test/test_response.py:755:41: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- test/test_response.py:760:39: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- test/test_response.py:773:40: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 390 diagnostics
+ Found 386 diagnostics

werkzeug (https://github.com/pallets/werkzeug)
- tests/conftest.py:240:43: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
+ tests/test_wsgi.py:158:49: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `LimitedStream` does not satisfy upper bound `_BufferedReaderStream` of type variable `_BufferedReaderStreamT`
+ tests/test_wsgi.py:158:49: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `_BufferedReaderStream`, found `LimitedStream`
- Found 369 diagnostics
+ Found 370 diagnostics

pytest (https://github.com/pytest-dev/pytest)
+ src/_pytest/assertion/rewrite.py:402:31: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `SupportsRead[bytes]`, found `TextIOWrapper[_WrappedBuffer]`
+ src/_pytest/capture.py:143:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Argument type `TextIOWrapper[_WrappedBuffer]` does not satisfy upper bound `_WrappedBuffer` of type variable `_BufferT_co`
+ src/_pytest/capture.py:143:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `_WrappedBuffer`, found `TextIOWrapper[_WrappedBuffer]`
- Found 471 diagnostics
+ Found 474 diagnostics

pwndbg (https://github.com/pwndbg/pwndbg)
- pwndbg/lib/zig.py:130:5: error[invalid-assignment] Object of type `LiteralString` is not assignable to `list[Path] | None`
- Found 2529 diagnostics
+ Found 2528 diagnostics

xarray (https://github.com/pydata/xarray)
+ xarray/backends/api.py:1516:42: error[invalid-argument-type] Argument to function `_remove_path` is incorrect: Expected `NestedSequence[_FLike@_remove_path]`, found `(_FLike@_remove_path & Top[list[Unknown]]) | (NestedSequence[_FLike@_remove_path] & Top[list[Unknown]])`
- xarray/backends/common.py:192:56: warning[unused-ignore-comment] Unused blanket `type: ignore` directive

mitmproxy (https://github.com/mitmproxy/mitmproxy)
- examples/addons/contentview-interactive.py:20:18: error[invalid-argument-type] Argument to function `add` is incorrect: Expected `Contentview`, found `<class 'InteractiveSwapCase'>`
- examples/addons/contentview.py:15:18: error[invalid-argument-type] Argument to function `add` is incorrect: Expected `Contentview`, found `<class 'SwapCase'>`
- test/individual_coverage.py:103:46: error[invalid-argument-type] Argument to function `run_tests` is incorrect: Expected `bool`, found `Match[str] | None`
- test/mitmproxy/contentviews/test__registry.py:67:23: error[invalid-argument-type] Argument to bound method `register` is incorrect: Expected `Contentview`, found `<class 'ExampleContentview'>`
- Found 1822 diagnostics
+ Found 1818 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ tests/test_pandas.py:245:9: error[type-assertion-failure] Argument does not have asserted type `DataFrame`
- Found 4962 diagnostics
+ Found 4963 diagnostics

sphinx (https://github.com/sphinx-doc/sphinx)
+ sphinx/ext/apidoc/_generate.py:50:12: error[no-matching-overload] No overload of bound method `join` matches arguments
- sphinx/search/ja.py:143:16: error[invalid-return-type] Return type does not match returned value: expected `list[str]`, found `list[LiteralString]`

meson (https://github.com/mesonbuild/meson)
+ mesonbuild/backend/backends.py:1396:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/backend/backends.py:1405:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/backend/ninjabackend.py:2995:16: error[invalid-return-type] Return type does not match returned value: expected `tuple[ImmutableListProtocol[str], ImmutableListProtocol[str]]`, found `tuple[list[str], list[str] | list[@Todo(list literal element type)]]`
+ mesonbuild/build.py:1093:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[Unknown]`, found `list[Unknown]`
+ mesonbuild/build.py:1126:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[Unknown]`, found `list[Unknown]`
+ mesonbuild/build.py:1877:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[Unknown]`
+ mesonbuild/build.py:2792:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[Unknown]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/cmake/interpreter.py:549:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/cmake/interpreter.py:556:20: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `(Unknown & list[str] & ~AlwaysFalsy) | list[@Todo(list literal element type)]`
+ mesonbuild/cmake/interpreter.py:558:20: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/compilers/mixins/apple.py:27:5: error[invalid-assignment] Object of type `list[@Todo(list literal element type)]` is not assignable to `ImmutableListProtocol[str]`
+ mesonbuild/compilers/mixins/clike.py:233:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/compilers/mixins/gnu.py:321:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list literal element type)]`
+ mesonbuild/dependencies/pkgconfig.py:211:16: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list comprehension element type)]`
+ mesonbuild/utils/universal.py:232:9: error[invalid-assignment] Object of type `list[@Todo(list literal element type)]` is not assignable to `ImmutableListProtocol[str] | None`
+ mesonbuild/utils/universal.py:235:9: error[invalid-assignment] Object of type `Unknown | list[@Todo(list literal element type)]` is not assignable to `ImmutableListProtocol[str] | None`
+ mesonbuild/utils/universal.py:238:9: error[invalid-assignment] Object of type `Unknown | list[@Todo(list literal element type)]` is not assignable to `ImmutableListProtocol[str] | None`
+ mesonbuild/utils/universal.py:712:12: error[invalid-return-type] Return type does not match returned value: expected `ImmutableListProtocol[str]`, found `list[@Todo(list comprehension element type)]`
- Found 794 diagnostics
+ Found 812 diagnostics

static-frame (https://github.com/static-frame/static-frame)
+ static_frame/test/unit/test_frame.py:9855:34: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `Iterable[<class 'int'>]`, found `repeat[<class 'str'>]`
- static_frame/test/unit/test_quilt.py:1110:71: warning[unused-ignore-comment] Unused blanket `type: ignore` directive

dd-trace-py (https://github.com/DataDog/dd-trace-py)
+ benchmarks/bm/iast_fixtures/str_methods.py:462:25: error[invalid-argument-type] Argument to bound method `format_map` is incorrect: Expected `_FormatMapMapping`, found `str`
+ ddtrace/vendor/ply/yacc.py:2011:34: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ ddtrace/vendor/ply/yacc.py:2014:38: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ ddtrace/vendor/ply/yacc.py:2015:38: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ ddtrace/vendor/ply/yacc.py:2016:38: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ ddtrace/vendor/ply/yacc.py:2017:38: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
+ ddtrace/vendor/ply/yacc.py:2018:38: error[invalid-argument-type] Argument to function `load` is incorrect: Expected `_ReadableFileobj`, found `TextIOWrapper[_WrappedBuffer]`
- Found 6665 diagnostics
+ Found 6672 diagnostics

zulip (https://github.com/zulip/zulip)
+ zerver/views/registration.py:612:17: error[invalid-argument-type] Argument to bound method `update` is incorrect: Expected `SupportsGetItem[str, str]`, found `QueryDict`
- Found 2671 diagnostics
+ Found 2672 diagnostics

pandas (https://github.com/pandas-dev/pandas)
+ pandas/tests/interchange/test_impl.py:270:29: error[invalid-argument-type] Argument to function `from_dlpack` is incorrect: Expected `_SupportsDLPack[None]`, found `Buffer`
+ pandas/tests/scalar/timedelta/test_arithmetic.py:876:18: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:882:18: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:895:18: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:900:18: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:908:18: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:919:13: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:943:13: error[no-matching-overload] No overload of function `divmod` matches arguments
+ pandas/tests/scalar/timedelta/test_arithmetic.py:946:13: error[no-matching-overload] No overload of function `divmod` matches arguments
- Found 3002 diagnostics
+ Found 3011 diagnostics

core (https://github.com/home-assistant/core)
+ homeassistant/helpers/device_registry.py:916:50: error[invalid-argument-type] Argument to function `_normalize_connections` is incorrect: Expected `Iterable[tuple[str, str]]`, found `set[tuple[str, str]] | UndefinedType | (Unknown & ~None)`
+ homeassistant/loader.py:1484:9: error[invalid-argument-type] Argument to function `_resolve_integrations_dependencies` is incorrect: Expected `_ResolveDependenciesCacheProtocol`, found `dict[@Todo(dict literal key type), @Todo(dict literal value type)]`
- Found 13446 diagnostics
+ Found 13448 diagnostics

sympy (https://github.com/sympy/sympy)
+ sympy/core/tests/test_numbers.py:147:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:148:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:149:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:150:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:151:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:158:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:159:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:160:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:161:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:162:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:163:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:167:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:168:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:169:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:170:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:171:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:173:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:174:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:175:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:176:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:177:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:178:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:179:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:180:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:181:12: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:183:16: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:185:16: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:187:16: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:188:16: error[no-matching-overload] No overload of function `divmod` matches arguments
+ sympy/core/tests/test_numbers.py:189:16: error[no-matching-overload] No overload of function `divmod` matches arguments
- Found 13387 diagnostics
+ Found 13417 diagnostics
Memory usage changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
-     memo metadata = ~22MB
+     memo metadata = ~23MB

sphinx (https://github.com/sphinx-doc/sphinx)
-     struct fields = ~17MB
+     struct fields = ~18MB

@AlexWaygood

This comment was marked as resolved.

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 30, 2025

CodSpeed Instrumentation Performance Report

Merging #20165 will improve performances by 13.92%

Comparing alex/method-members (80fc8fe) with main (bb9be26)

Summary

⚡ 1 improvement
✅ 42 untouched

Benchmarks breakdown

Benchmark BASE HEAD Change
DateType 256.9 ms 225.5 ms +13.92%

@github-actions
Copy link
Contributor

github-actions bot commented Aug 30, 2025

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-argument-type 40 4 0
no-matching-overload 39 0 0
unused-ignore-comment 0 23 0
invalid-return-type 18 1 0
invalid-assignment 8 2 0
redundant-cast 0 1 0
type-assertion-failure 1 0 0
Total 106 31 0

Full report with detailed diff

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 30, 2025

CodSpeed WallTime Performance Report

Merging #20165 will not alter performance

Comparing alex/method-members (80fc8fe) with main (bb9be26)

Summary

✅ 8 untouched

@AlexWaygood AlexWaygood force-pushed the alex/method-members branch 3 times, most recently from 54de803 to 80db138 Compare September 5, 2025 18:00
@AlexWaygood AlexWaygood changed the base branch from main to alex/protocol-nominal September 9, 2025 13:24
@AlexWaygood AlexWaygood force-pushed the alex/method-members branch 2 times, most recently from 910b683 to 73c80bb Compare September 9, 2025 14:04
@AlexWaygood AlexWaygood changed the base branch from alex/protocol-nominal to alex/generic-method-tests September 9, 2025 14:04
@AlexWaygood AlexWaygood force-pushed the alex/generic-method-tests branch from aef602f to 7b94e44 Compare September 9, 2025 16:39
Base automatically changed from alex/generic-method-tests to main September 9, 2025 16:44
@AlexWaygood AlexWaygood force-pushed the alex/method-members branch 2 times, most recently from 7041a3a to a947ad4 Compare September 11, 2025 14:49
Comment on lines +2231 to +2232
# TODO: this should pass
static_assert(is_subtype_of(TypeOf[satisfies_foo], Foo)) # error: [static-assert-error]
Copy link
Member Author

Choose a reason for hiding this comment

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

I believe this is because we don't consider Foo a fully static type currently, because it has an unannotated self parameter

Comment on lines 557 to 564
if any_over_type(db, proto_member_as_bound_method, &|t| {
matches!(t, Type::TypeVar(_))
}) {
// TODO: proper validation for generic methods on protocols
return C::always_satisfiable(db);
}
Copy link
Member Author

@AlexWaygood AlexWaygood Sep 11, 2025

Choose a reason for hiding this comment

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

I wanted to get a MVP of this feature out before digging into why we have lots of false positives for method members with function-scoped generic contexts

@AlexWaygood AlexWaygood marked this pull request as ready for review September 11, 2025 18:38
@AlexWaygood
Copy link
Member Author

Merging #20165 will improve performances by 13.56%

Quite unexpected, but I'll take it!!

The DateType benchmark is so strange, because DateType has such huge protocols. My guess is that we're able to short-circuit subtyping checks sooner now for some of its very big protocols, because we more quickly realise that a given type cannot be a subtype of a given protocol due to a signature mismatch. The more quickly we short-circuit during a subtype check, the fewer protocol members we need to iterate through and test the existence of.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Nice job extracting so many fixes along the way and leaving this such a small diff!

Comment on lines +3266 to +3269
if name == "__call__" && matches!(self, Type::Callable(_) | Type::DataclassTransformer(_)) {
return Place::bound(self).into();
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any direct tests of this (that the __call__ of a callable type is itself), or only indirect tests via protocols? Would it be worth a direct test for it, to make it clearer what's happening if it ever regresses?

Copy link
Member Author

@AlexWaygood AlexWaygood Sep 12, 2025

Choose a reason for hiding this comment

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

What's changing here is a bit subtle. On main we are already able to infer that for an object c of a callable type T, the type of c.__call__ will also be T. But naively looking up the __call__ attribute directly on c isn't good enough for protocol assignability/subtyping -- it would mean that this assertion would not pass, for example, because Foo.__iter__ has type Callable[[Foo], str] rather than Callable[[], Foo]. (The enum class object itself is iterable, but so is each enum member. Iterating over the enum members uses str.__iter__ because each enum member is an instance of str; iterating over the enum class object itself uses EnumType.__iter__.):

from typing import Iterable
from enum import StrEnum
from ty_extensions import static_assert, is_assignable_to, TypeOf

class Foo(StrEnum):
    X = "foo"
    Y = "bar"

static_assert(is_assignable_to(TypeOf[Foo], Iterable[Foo]))
>>> from enum import StrEnum
>>> class Foo(StrEnum):
...     X = "foo"
...     Y = "bar"
...     
>>> list(Foo)
[<Foo.X: 'foo'>, <Foo.Y: 'bar'>]
>>> list(Foo.X)
['f', 'o', 'o']

Therefore to get the "instance get-type" of a method member, we can't simply lookup the type of that member on an object; instead we must lookup the type of that member on the object's meta-type and then invoke the descriptor protocol on the result of that lookup. On most types that works great, but not on Callable types, because we currently give the meta-type of a Callable type as being just type (it's a very lossy operation currently!). Ideally we would synthesize some kind of meta-protocol as the meta-type of a Callable type (I'd like to do that at some point!) so that it's not so lossy, but that feels out of scope for this PR.

TL;DR: no, I don't think it's really possible to add an isolated unit test for this that doesn't involve protocols. It's a change that's very specific to the way we need to look up the __call__ attribute on Callable types when determining whether a Callable type is a subtype of a protocol type with a __call__ method.

However, I will add a comment to this bit of code noting that while this isn't inaccurate, it's basically a workaround until we have a better answer for the meta-type of Callable types.

Copy link
Member Author

@AlexWaygood AlexWaygood Sep 12, 2025

Choose a reason for hiding this comment

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

I'm actually getting rid of this again in #20363 FWIW, and replacing it with special-casing somewhere else instead 😄

@AlexWaygood AlexWaygood enabled auto-merge (squash) September 12, 2025 10:06
@AlexWaygood AlexWaygood merged commit 33b3d44 into main Sep 12, 2025
37 checks passed
@AlexWaygood AlexWaygood deleted the alex/method-members branch September 12, 2025 10:10
AlexWaygood added a commit that referenced this pull request Sep 12, 2025
#20367)

## Summary

#20165 added a lot of false
positives around calls to `builtins.open()`, because our missing support
for PEP-613 type aliases means that we don't understand typeshed's
overloads for `builtins.open()` at all yet, and therefore always select
the first overload. This didn't use to matter very much, but now that we
have a much stricter implementation of protocol assignability/subtyping
it matters a lot, because most of the stdlib functions dealing with I/O
(`pickle`, `marshal`, `io`, `json`, etc.) are annotated in typeshed as
taking in protocols of some kind.

In lieu of full PEP-613 support, which is blocked on various things and
might not land in time for our next alpha release, this PR adds some
temporary special-casing for `builtins.open()` to avoid the false
positives. We just infer `Todo` for anything that isn't meant to match
typeshed's first `open()` overload. This should be easy to rip out again
once we have proper support for PEP-613 type aliases, which hopefully
should be pretty soon!

## Test Plan

Added an mdtest
Comment on lines +544 to +552
let Place::Type(attribute_type, Boundness::Bound) = other
.invoke_descriptor_protocol(
db,
self.name,
Place::Unbound.into(),
InstanceFallbackShadowsNonDataDescriptor::No,
MemberLookupPolicy::default(),
)
.place
Copy link
Contributor

@sharkdp sharkdp Sep 15, 2025

Choose a reason for hiding this comment

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

Why do you call invoke_descriptor_protocol directly here instead of going through Type::member? Does this work correctly if other is a union/intersection (is that already handled at a higher level)? And does it handle classmethods, method accesses on class objects etc. correctly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Why do you call invoke_descriptor_protocol directly here instead of going through Type::member?

I answered this in my reply to Carl here

Does this work correctly if other is a union/intersection (is that already handled at a higher level)?

I think so... I should probably add more tests around this. Is there a reason why you think this might not work for unions/intersections?

And does it handle classmethods, method accesses on class objects etc. correctly?

I should definitely add more tests around this too, thanks. Note that there are some more checks that I'd like to do (such as checking the type on the meta-type as well as the type on the instance-type) which we can't yet do while we consider methods with unannotated self/cls to be non-fully-static. This PR is really an MVP

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why you think this might not work for unions/intersections?

Not sure, but I think member does the "distribute this operation over unions/intersections" part of invoke_descriptor_protocol.

If you plan to add more tests for both of these cases, we should be fine. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

3 participants