Skip to content

[ty] Recognize functions with stub bodies in Protocol classes as implicitly abstract#22838

Merged
AlexWaygood merged 7 commits intomainfrom
alex/trivial-bodies
Jan 31, 2026
Merged

[ty] Recognize functions with stub bodies in Protocol classes as implicitly abstract#22838
AlexWaygood merged 7 commits intomainfrom
alex/trivial-bodies

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jan 24, 2026

Summary

Fixes astral-sh/ty#2580.

We now consider any method in a Protocol class to be implicitly abstract if the Protocol class is defined in a .py file and of the following conditions is met regarding the method:

  1. It has a "stub body" (all statements are docstrings, ... or pass), and they have a return type that is not assignable to None
  2. It has a body that only consists of raise NotImplementedError, and it has a return type that is not assignable to Never

We also now do not emit abstract-method-in-final-class for @final Protocol classes. Unlike non-Protocol classes, it is possible to subtype a Protocol class without explicitly subclassing it, so an @final Protocol class with unimplemented abstract methods is not inherently broken in the same way as an @final non-Protocol class.

Test Plan

mdtests and snapshots

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jan 24, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

mypy_primer results

Changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
- src/trio/_highlevel_ssl_helpers.py:120:6: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ src/trio/_highlevel_ssl_helpers.py:120:6: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`

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

prefect (https://github.com/PrefectHQ/prefect)
- src/prefect/infrastructure/provisioners/container_instance.py:217:45: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `str`
+ src/prefect/infrastructure/provisioners/container_instance.py:217:45: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
- src/prefect/server/events/stream.py:168:54: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ src/prefect/server/events/stream.py:168:54: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`
- src/prefect/server/services/base.py:104:36: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ src/prefect/server/services/base.py:104:36: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`
- src/prefect/tasks.py:1801:31: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ src/prefect/tasks.py:1801:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`
- src/prefect/tasks.py:1827:24: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ src/prefect/tasks.py:1827:24: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`

django-stubs (https://github.com/typeddjango/django-stubs)
- mypy_django_plugin/config.py:46:57: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
+ mypy_django_plugin/config.py:46:57: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Never`

static-frame (https://github.com/static-frame/static-frame)
- static_frame/core/bus.py:645:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bottom[Bus[Any]] | Bottom[Series[Any, Any]] | TypeBlocks | ... omitted 6 union elements, object_]`
- static_frame/core/bus.py:649:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Bottom[Bus[Any]] | IndexHierarchy | TypeBlocks | ... omitted 7 union elements, Self@iloc]`
+ static_frame/core/bus.py:649:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemILocReduces[Bus[Any], object_]`, found `InterGetItemILocReduces[Self@iloc, 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[Bottom[Series[Any, Any]] | Bottom[Index[Any]] | ndarray[Never, Never] | ... omitted 7 union elements, TVDtype@Series]`
- 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[Bottom[Series[Any, Any]] | Bottom[Index[Any]] | TypeBlocks | ... omitted 7 union elements, TVDtype@SeriesHE]`
- 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[Bottom[Yarn[Any]] | IndexHierarchy | TypeBlocks | ... omitted 7 union elements, object_]`
- Found 1830 diagnostics
+ Found 1826 diagnostics

streamlit (https://github.com/streamlit/streamlit)
- lib/streamlit/commands/execution_control.py:136:6: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
- lib/streamlit/commands/execution_control.py:192:6: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
- Found 95 diagnostics
+ Found 93 diagnostics

ibis (https://github.com/ibis-project/ibis)
- ibis/backends/bigquery/__init__.py:455:10: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Backend`
+ ibis/backends/bigquery/__init__.py:455:10: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Backend`

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
- tests/frame/test_groupby.py:229: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:625:15: error[type-assertion-failure] Type `Series[Any]` does not match asserted type `Series[str | bytes | int | ... omitted 12 union elements]`
- Found 4415 diagnostics
+ Found 4413 diagnostics

No memory usage changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 24, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
empty-body 0 10 0
invalid-return-type 8 0 1
invalid-parameter-default 0 0 7
invalid-argument-type 1 2 3
invalid-assignment 0 0 4
unused-type-ignore-comment 2 0 0
Total 11 12 15

Full report with detailed diff (timing results)

@AlexWaygood AlexWaygood force-pushed the alex/trivial-bodies branch 4 times, most recently from 728fc6f to 5a0043b Compare January 24, 2026 18:12
@AlexWaygood AlexWaygood marked this pull request as ready for review January 24, 2026 18:43
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

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

I think I personally would've expected def also_not_abstractmethod(self) -> None: ... to be considered implicitly abstract (but could understand why def also_not_abstractmethod(self) -> None: pass might not) just based on some argument around "programmer intent". Your approach is also reasonable though, that opinion is highly subjective.

@AlexWaygood
Copy link
Member Author

Alright, I've switched the behaviour to match that of other type checkers, and made the Salsa caching more fine-grained so that we don't over-invalidate the ClassType::abstract_methods query when the AST changes in another file. I'd appreciate another once-over from somebody, since it's now quite a big PR and the code's changed a fair amount since the last round of review 😄

@AlexWaygood AlexWaygood requested a review from carljm January 29, 2026 15:46
@AlexWaygood
Copy link
Member Author

streamlit (https://github.com/streamlit/streamlit)
- lib/streamlit/commands/execution_control.py:136:6: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
- lib/streamlit/commands/execution_control.py:192:6: error[empty-body] Function always implicitly returns `None`, which is not assignable to return type `Never`
- Found 95 diagnostics
+ Found 93 diagnostics

These diagnostics are going away because the error code is changing from empty-body to invalid-return-type (which seems correct; not sure why it was empty-body before), and they have # ty: ignore[invalid-return-type] on the function, which suppresses the new invalid-return-type error.

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.

Looks great!

@AlexWaygood AlexWaygood enabled auto-merge (squash) January 31, 2026 09:53
@AlexWaygood AlexWaygood merged commit 2ff4748 into main Jan 31, 2026
48 checks passed
@AlexWaygood AlexWaygood deleted the alex/trivial-bodies branch January 31, 2026 10:02
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

Development

Successfully merging this pull request may close these issues.

Methods on Protocol classes with trivial bodies should be understood as implicitly abstract

4 participants