Skip to content

Comments

[ty] Emit diagnostic for unimplemented abstract method on @final class#22753

Merged
charliermarsh merged 12 commits intomainfrom
charlie/final
Jan 21, 2026
Merged

[ty] Emit diagnostic for unimplemented abstract method on @final class#22753
charliermarsh merged 12 commits intomainfrom
charlie/final

Conversation

@charliermarsh
Copy link
Member

Summary

Closes astral-sh/ty#2525.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 20, 2026

Typing conformance results

No changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 20, 2026

mypy_primer results

Changes were detected when running on open source projects
tornado (https://github.com/tornadoweb/tornado)
- tornado/gen.py:255:62: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `None | Awaitable[Unknown] | list[Awaitable[Unknown]] | dict[Any, Awaitable[Unknown]] | Future[Unknown]`, found `_T@next | _T@next | _VT@next`
+ tornado/gen.py:255:62: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `None | Awaitable[Unknown] | list[Awaitable[Unknown]] | dict[Any, Awaitable[Unknown]] | Future[Unknown]`, found `_T@next | _VT@next | _T@next`

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 `Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]] | _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 `_T@cached_inject | Coroutine[Any, Any, _T@cached_inject | Coroutine[Any, Any, _T@cached_inject]]`

prefect (https://github.com/PrefectHQ/prefect)
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:94:28: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/integrations/prefect-dbt/prefect_dbt/core/settings.py:99:28: error[invalid-assignment] Object of type `T@resolve_variables | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:86:21: error[invalid-assignment] Object of type `T@resolve_block_document_references | dict[str, Any]` is not assignable to `dict[str, Any]`
- src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables | str | int | ... omitted 4 union elements` is not assignable to `dict[str, Any]`
+ src/prefect/cli/deploy/_core.py:87:21: error[invalid-assignment] Object of type `T@resolve_variables` is not assignable to `dict[str, Any]`
- src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/deployments/steps/core.py:137:38: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
- src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements` on object of type `dict[str, Any]`
+ src/prefect/utilities/templating.py:320:13: error[invalid-assignment] Invalid subscript assignment with key of type `object` and value of type `T@resolve_block_document_references | dict[str, Any]` on object of type `dict[str, Any]`
- src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | T@resolve_block_document_references | dict[str, Any] | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:323:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_block_document_references | dict[str, Any]`, found `list[Unknown | T@resolve_block_document_references | dict[str, Any]]`
- src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | T@resolve_variables | str | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:437:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `dict[object, Unknown | T@resolve_variables]`
- src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | T@resolve_variables | str | ... omitted 5 union elements]`
+ src/prefect/utilities/templating.py:442:16: error[invalid-return-type] Return type does not match returned value: expected `T@resolve_variables`, found `list[Unknown | T@resolve_variables]`
- src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any] | str | ... omitted 4 union elements`
+ src/prefect/workers/base.py:232:13: error[invalid-argument-type] Argument is incorrect: Expected `T@resolve_variables`, found `T@resolve_block_document_references | dict[str, Any]`
- src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables | str | int | ... omitted 4 union elements`
+ src/prefect/workers/base.py:234:20: error[invalid-argument-type] Argument expression after ** must be a mapping type: Found `T@resolve_variables`

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

static-frame (https://github.com/static-frame/static-frame)
- 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/bus.py:671:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[Bus[Any], object_]`, found `InterGetItemLocReduces[Bus[Any] | IndexHierarchy | Bottom[Series[Any, Any]] | ... omitted 7 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[Bus[Any] | IndexHierarchy | TypeBlocks | ... omitted 7 union elements, object_ | Self@iloc]`
- static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Bottom[Series[Any, Any]] | Unknown, Any]`
+ static_frame/core/node_selector.py:526:16: error[invalid-return-type] Return type does not match returned value: expected `InterGetItemLocReduces[TVContainer_co@InterfaceSelectQuartet, Any]`, found `InterGetItemLocReduces[Unknown | Bottom[Series[Any, Any]], Any]`
+ 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[Series[Any, Any] | Bottom[Index[Any]] | TypeBlocks | ... 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]] | IndexHierarchy | TypeBlocks | ... omitted 8 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[Yarn[Any] | IndexHierarchy | TypeBlocks | ... omitted 7 union elements, object_]`
- Found 1821 diagnostics
+ Found 1825 diagnostics

No memory usage changes detected ✅

@charliermarsh charliermarsh added ty Multi-file analysis & type inference ecosystem-analyzer labels Jan 20, 2026
@charliermarsh charliermarsh marked this pull request as ready for review January 20, 2026 02:50
@astral-sh-bot
Copy link

astral-sh-bot bot commented Jan 20, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 2 0 6
invalid-parameter-default 0 0 7
invalid-return-type 0 5 2
possibly-missing-attribute 3 0 1
invalid-argument-type 1 0 1
unresolved-attribute 0 0 2
unused-ignore-comment 0 1 0
Total 6 6 19

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as draft January 20, 2026 15:03
@charliermarsh charliermarsh marked this pull request as ready for review January 20, 2026 15:09
@AlexWaygood
Copy link
Member

now you're emitting false-positive errors on classes like this, which can be instantiated at runtime 😄

from abc import abstractmethod, ABC
from typing import final

class Base(ABC):
    @property
    @abstractmethod
    def f(self) -> int: ...

@final
class Child(Base):
    f = 42

The difference between Child and a class like this:

@final
class BadChild(Base):
    f: int

is that Child.f has a definition whereas BadChild.f only has a declaration

@charliermarsh
Copy link
Member Author

Oh gosh.

@charliermarsh charliermarsh marked this pull request as draft January 20, 2026 15:28
.with_qualifiers(type_and_qualifiers.qualifiers()),
Ok(type_and_qualifiers) => Place::Defined(
DefinedPlace::new(type_and_qualifiers.inner_type())
.with_origin(type_and_qualifiers.origin()),
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 think we were losing the origin here (unintentionally?).

Copy link
Member

Choose a reason for hiding this comment

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

yes, this looks correct! good catch

@charliermarsh charliermarsh marked this pull request as ready for review January 20, 2026 15:46
Comment on lines +752 to +753
class Derived(Base): # No error - not final, can be subclassed
pass
Copy link
Member

@AlexWaygood AlexWaygood Jan 20, 2026

Choose a reason for hiding this comment

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

(but we should emit an error when such a class is instantiated, of course -- which is astral-sh/ty#1877; it doesn't need to be implemented here)

@charliermarsh charliermarsh marked this pull request as draft January 20, 2026 18:46
@charliermarsh charliermarsh marked this pull request as ready for review January 20, 2026 19:13
/// Only returns methods that are still abstract on `self` (i.e., have not been overridden
/// with a concrete implementation anywhere in the MRO).
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
pub(crate) fn abstract_methods(self, db: &'db dyn Db) -> FxHashMap<Name, ClassType<'db>> {
Copy link
Member

Choose a reason for hiding this comment

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

I think there are probably marginally more efficient ways of doing this with a single MRO iteration (if we iterated through the MRO in reverse order), but this also seems fine for a first pass

Comment on lines 1300 to 1306
for (method_name, defining_class) in class_type.abstract_methods(db) {
let Some(builder) = self
.context
.report_lint(&UNIMPLEMENTED_ABSTRACT_METHOD, &class_node.name)
else {
continue;
};
Copy link
Member

Choose a reason for hiding this comment

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

if there are multiple unimplemented abstract methods, do we really want to emit a separate diagnostic for each one? Wouldn't it be better to collect a list of all the unimplemented abstract methods and then emit a single diagnostic saying "Methods foo, bar, baz are all abstract"? (But I think it would be fine to only add a subdiagnostic pointing to the definition of the first one; too many subdiagnostics would be noisy.)

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 personally find this more useful + consistent with our other patterns, but I don't feel strongly.

Copy link
Member

Choose a reason for hiding this comment

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

I know there's still a bunch of rules where we emit a separate diagnostic for each element in cases like this, but in general I think we aspire to group them together where possible -- e.g. here we only emit a single diagnostic, even though multiple elements of the union have the incorrect signature:

% uvx ty check foo.py
error[unknown-argument]: Argument `x` does not match any known parameter of function `f`
 --> foo.py:7:10
  |
5 | def i(coinflip: bool):
6 |     func = f if coinflip else g if coinflip else h
7 |     func(x=42)
  |          ^^^^
  |
info: Union variant `def f() -> Unknown` is incompatible with this call site
info: Attempted to call union type `(def f() -> Unknown) | (def h(x) -> Unknown)`
info: rule `unknown-argument` is enabled by default

Found 1 diagnostic
% cat foo.py
def f(): ...
def g(): ...
def h(x): ...

def i(coinflip: bool):
    func = f if coinflip else g if coinflip else h
    func(x=42)

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.

This looks great, thank you

Comment on lines 668 to 681
### Protocol with abstract methods

```py
from abc import abstractmethod
from typing import Protocol, final

class MyProtocol(Protocol):
@abstractmethod
def method(self) -> int: ...

@final
class Implementer(MyProtocol): # error: [unimplemented-abstract-method]
pass
```
Copy link
Member

Choose a reason for hiding this comment

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

(this can be a followup): we also need to implement the rule where all methods on Protocol classes that have "trivial bodies" (either ..., pass, a docstring, or a combination of those three elements) and have an explicit non-None return type are treated as implicitly abstract even if they are not decorated with @abstractmethod

@charliermarsh charliermarsh merged commit bd2348d into main Jan 21, 2026
134 of 142 checks passed
@charliermarsh charliermarsh deleted the charlie/final branch January 21, 2026 14:11
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.

Emit diagnostic for unimplemented abstract method on @final class

3 participants