Skip to content

Comments

[ty] Implement typing.override#21627

Merged
carljm merged 4 commits intomainfrom
alex/explicit-override
Nov 25, 2025
Merged

[ty] Implement typing.override#21627
carljm merged 4 commits intomainfrom
alex/explicit-override

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Nov 25, 2025

Summary

Part of astral-sh/ty#155. This implements the basic check (@override-decorated methods should override things!), but not the strict check specified in https://typing.python.org/en/latest/spec/class-compat.html#strict-enforcement-per-project, which should be a separate error code.

Test Plan

mdtests and snapshots

@AlexWaygood AlexWaygood added ty Multi-file analysis & type inference ecosystem-analyzer labels Nov 25, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 25, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-11-25 18:04:28.872627110 +0000
+++ new-output.txt	2025-11-25 18:04:32.604642480 +0000
@@ -194,6 +194,10 @@
 classes_classvar.py:77:8: error[invalid-type-form] `ClassVar` annotations are only allowed in class-body scopes
 classes_classvar.py:111:1: error[invalid-attribute-access] Cannot assign to ClassVar `stats` from an instance of type `Starship`
 classes_classvar.py:140:13: error[invalid-assignment] Object of type `ProtoAImpl` is not assignable to `ProtoA`
+classes_override.py:53:9: error[invalid-explicit-override] Method `method3` is decorated with `@override` but does not override anything
+classes_override.py:65:9: error[invalid-explicit-override] Method `method4` is decorated with `@override` but does not override anything
+classes_override.py:79:9: error[invalid-explicit-override] Method `static_method1` is decorated with `@override` but does not override anything
+classes_override.py:84:9: error[invalid-explicit-override] Method `class_method1` is decorated with `@override` but does not override anything
 constructors_call_init.py:21:13: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `int`, found `float`
 constructors_call_init.py:25:1: error[type-assertion-failure] Type `Class1[int | float]` does not match asserted type `Class1[float]`
 constructors_call_init.py:56:1: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `Class4[int]`, found `Class4[str]`
@@ -776,6 +780,7 @@
 overloads_definitions.py:159:46: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int | str`
 overloads_definitions.py:167:44: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int | str`
 overloads_definitions.py:183:10: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int | str`
+overloads_definitions.py:198:9: error[invalid-explicit-override] Method `bad_override` is decorated with `@override` but does not override anything
 overloads_definitions.py:198:45: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int | str`
 overloads_definitions.py:215:46: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int | str`
 overloads_definitions.py:229:9: error[invalid-overload] `@override` decorator should be applied only to the overload implementation
@@ -786,6 +791,7 @@
 overloads_definitions_stub.pyi:44:9: error[invalid-overload] Overloaded function `func6` does not use the `@classmethod` decorator consistently
 overloads_definitions_stub.pyi:76:9: error[invalid-overload] `@final` decorator should be applied only to the first overload
 overloads_definitions_stub.pyi:86:9: error[invalid-overload] `@final` decorator should be applied only to the first overload
+overloads_definitions_stub.pyi:122:9: error[invalid-explicit-override] Method `bad_override` is decorated with `@override` but does not override anything
 overloads_definitions_stub.pyi:147:9: error[invalid-overload] `@override` decorator should be applied only to the first overload
 overloads_evaluation.py:38:1: error[no-matching-overload] No overload of function `example1_1` matches arguments
 overloads_evaluation.py:46:15: error[invalid-argument-type] Argument to function `example1_1` is incorrect: Expected `str`, found `Literal[1]`
@@ -1005,5 +1011,5 @@
 typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
 typeddicts_usage.py:28:18: error[invalid-key] Unknown key "title" for 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
-Found 1007 diagnostics
+Found 1013 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 25, 2025

mypy_primer results

Changes were detected when running on open source projects
graphql-core (https://github.com/graphql-python/graphql-core)
- tests/type/test_definition.py:163:72: error[invalid-argument-type] Argument to function `parse_literal` is incorrect: Expected `ValueNode`, found `dict[Unknown | str, Unknown | str] & dict[str, Any]`
+ tests/type/test_definition.py:163:72: error[invalid-argument-type] Argument to function `parse_literal` is incorrect: Expected `ValueNode`, found `dict[str, Any] & dict[Unknown | str, Unknown | str]`

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 45 diagnostics
+ Found 44 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ddtrace/appsec/_handlers.py:404:13: error[invalid-argument-type] Argument to bound method `add_configurations` is incorrect: Expected `list[tuple[str, str, str]]`, found `list[Unknown | tuple[str, int, Unknown]] & list[tuple[str, str, str] | tuple[str, int, Unknown]]`
+ ddtrace/appsec/_handlers.py:404:13: error[invalid-argument-type] Argument to bound method `add_configurations` is incorrect: Expected `list[tuple[str, str, str]]`, found `list[tuple[str, str, str] | tuple[str, int, Unknown]] & list[Unknown | tuple[str, int, Unknown]]`

No memory usage changes detected ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Nov 25, 2025

ecosystem-analyzer results

No diagnostic changes detected ✅
Full report with detailed diff (timing results)

@AlexWaygood AlexWaygood force-pushed the alex/explicit-override branch from 5442f81 to ca53612 Compare November 25, 2025 13:23
@AlexWaygood AlexWaygood force-pushed the alex/explicit-override branch 2 times, most recently from 0a1b839 to 23246c9 Compare November 25, 2025 14:52
@AlexWaygood AlexWaygood force-pushed the alex/explicit-override branch from 23246c9 to 375709b Compare November 25, 2025 14:53
@AlexWaygood AlexWaygood marked this pull request as ready for review November 25, 2025 14:54
@AlexWaygood
Copy link
Member Author

The typing conformance suite results are all good -- all the places where we're newly emitting diagnostics are marked with # E.

The ecosystem results are also excellent -- this is one of the few new diagnostics where you'd really expect no new ecosystem hits, and indeed that's what we have!

/// @override
/// def foo(self): ... # fine: overrides `A.foo`
/// ```
pub(crate) static EXPLICIT_OVERRIDE = {
Copy link
Member

@MichaReiser MichaReiser Nov 25, 2025

Choose a reason for hiding this comment

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

I think I'd prefer invalid-override or invalid-explicit-override. explicit-override sounds like explicit overrides are a bad thing.

And incorrect-override for an override that doesn't match the signature of the parent class

Copy link
Member Author

@AlexWaygood AlexWaygood Nov 25, 2025

Choose a reason for hiding this comment

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

yeah, I wasn't sure what to call this. I don't want to call it invalid-override because that implies a Liskov violation to me (e.g. the name for the "bad method override" diagnostic is invalid-method-override).

invalid-explicit-override is okay, I can switch to that. (Edit: I pushed this rename.)

Copy link
Member

Choose a reason for hiding this comment

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

Hmm yeah, this is tricky. invalid-explicit-override is a bit long.

Should invalid-method-override be incorrect-method-override to make the distinction clearer? What name do you have in mind for the strict enforcement?

  • Pyrefly: bad-override

Copy link
Member Author

Choose a reason for hiding this comment

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

invalid-explicit-override is a bit long.

not as long as some of the ones we already have, such as subclass-of-final-class 😄

Should invalid-method-override be incorrect-method-override to make the distinction clearer?

I'm not sure that would make the distinction much clearer. Applying @override to a method that doesn't override anything is just as incorrect as overriding a method in a way that violates the Liskov Substitution Principle.

What name do you have in mind for the strict enforcement?

The one I'm using in my head right now is invalid-explicit-override-strict or similar. But it's not great.

Another option here would be for the strict enforcement would actually just be the same error code, but we'd have a tool.ty.explicit-override-enforcement = true configuration setting (etc.) that allows you to opt into the stricter checks. That would obviate the need for a separate error code, which is nice. But the strict enforcement is really detecting a conceptually pretty different (and far more pedantic) problem -- so a separate error code does feel appropriate in some ways.

Do you have any suggestions?

Copy link
Member Author

@AlexWaygood AlexWaygood Nov 25, 2025

Choose a reason for hiding this comment

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

Other possible names for the Liskov violation code might be:

  • unsound-method-override
  • liskov-violating-override
  • incompatible-method-override

But I think even if we went with any of those for the Liskov violation error, I would still want the term "explicit" somewhere in the error code for invalid uses of typing.override, so that we're clear that this error code isn't about overrides in general, it's about overrides that are demarcated with typing.override.

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 do also think that the most important thing is to get this check in soon. It's very easy to change the name of an error code later, especially while we still declare ty to be alpha or beta software

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO invalid-explicit-override is fine for this code. I think for the strict check the right name would be invalid-implicit-override, since the nature of the strict check is "you failed to use @override here, but you are required to" -- that is, you are making an implicit override, but overrides are required to be explicit.

Comment on lines +161 to +169
if has_typeddict_in_mro {
if !KnownClass::TypedDictFallback
.to_instance(db)
.member(db, &member.name)
.place
.is_undefined()
{
subclass_overrides_superclass_declaration = true;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

methods on TypedDict subclasses are invalid (as noted in comments in liskov.md)... but they exist in the wild anyway, and some of them are decorated with @override: https://github.com/Toufool/AutoSplit/blob/d9c5e3b48eeda52a13c08e75b5ffd61873ce5b8f/src/user_profile.py#L50-L53

Comment on lines +219 to +220
In a stub file, `@override` should always be applied to the first overload. Even if it isn't, we
always emit `invalid-explicit-override` diagnostics on the first overload.
Copy link
Member Author

Choose a reason for hiding this comment

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

It's a bit annoying to implement this but it means that we pass several typing conformance-suite tests that we'd otherwise fail, so I think it's worth it

@AlexWaygood AlexWaygood force-pushed the alex/explicit-override branch from f2ff136 to a25e9b1 Compare November 25, 2025 15:16
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 good to me!

Comment on lines +286 to +287
# TODO: this raises an exception at runtime (which we should emit a diagnostic for).
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think an invalid-explicit-override would be wrong here, exactly -- NamedTuple isn't really a base class, so you aren't overriding anything here. But I think it's fine if we don't emit it.

/// @override
/// def foo(self): ... # fine: overrides `A.foo`
/// ```
pub(crate) static EXPLICIT_OVERRIDE = {
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO invalid-explicit-override is fine for this code. I think for the strict check the right name would be invalid-implicit-override, since the nature of the strict check is "you failed to use @override here, but you are required to" -- that is, you are making an implicit override, but overrides are required to be explicit.

@carljm carljm merged commit 81c97e9 into main Nov 25, 2025
41 checks passed
@carljm carljm deleted the alex/explicit-override branch November 25, 2025 18:42
carljm added a commit to mtshiba/ruff that referenced this pull request Nov 25, 2025
* main:
  [ty] Implement `typing.override` (astral-sh#21627)
  [ty] Avoid expression reinference for diagnostics (astral-sh#21267)
  [ty] Improve autocomplete suppressions of keywords in variable bindings
  [ty] Only suggest completions based on text before the cursor
  Implement goto-definition and find-references for global/nonlocal statements (astral-sh#21616)
  [ty] Inlay Hint edit follow up (astral-sh#21621)
  [ty] Implement lsp support for string annotations (astral-sh#21577)
  [ty] Add 'remove unused ignore comment' code action (astral-sh#21582)
  [ty] Refactor `CheckSuppressionContext` to use `DiagnosticGuard` (astral-sh#21587)
  [ty] Improve several "Did you mean?" suggestions (astral-sh#21597)
  [ty] Add more and update existing projects in `ty_benchmark` (astral-sh#21536)
  [ty] fix ty playground initialization and vite optimization issues (astral-sh#21471)
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.

3 participants