Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Change Log

New error codes:
* Introduce Y068: Don't use `@override` in stub files.

## 5.5.0

New error codes:
Expand Down
1 change: 1 addition & 0 deletions ERRORCODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The following warnings are currently emitted by default:
| <a id="Y066" href="#Y066">Y066</a> | When using if/else with `sys.version_info`, put the code for new Python versions first. | Style
| <a id="Y067" href="#Y067">Y067</a> | Don't use `Incomplete | None = None` in
argument annotations. Instead, just use `=None`. | Style
| <a id="Y068" href="#Y068">Y068</a> | Don't use `@override` in stub files. Problems with a function signature deviating from its superclass are inherited from the implementation, and other tools such as stubtest are better placed to recognize deviations between stubs and the implementation. | Understanding stubs

## Warnings disabled by default

Expand Down
2 changes: 2 additions & 0 deletions flake8_pyi/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ class Error(NamedTuple):
'put the code for new Python versions first, e.g. "{new_syntax}"'
)
Y067 = 'Y067 Use "=None" instead of "Incomplete | None = None"'
Y068 = 'Y068 Do not use "@override" in stub files.'

Y090 = (
'Y090 "{original}" means '
'"a tuple of length 1, in which the sole element is of type {typ!r}". '
Expand Down
8 changes: 8 additions & 0 deletions flake8_pyi/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ def _is_object(node: ast.AST | None, name: str, *, from_: Container[str]) -> boo
)
_is_Generic = partial(_is_object, name="Generic", from_=_TYPING_MODULES)
_is_Unpack = partial(_is_object, name="Unpack", from_=_TYPING_MODULES)
_is_override = partial(_is_object, name="override", from_=_TYPING_MODULES)


def _is_union(node: ast.expr | None) -> TypeIs[ast.BinOp]:
Expand Down Expand Up @@ -2032,6 +2033,12 @@ def check_protocol_param_kinds(
pos_or_kw, errors.Y091.format(arg=pos_or_kw.arg, method=node.name)
)

def check_for_override(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
for deco in node.decorator_list:
if _is_override(deco):
self.error(deco, errors.Y068)
return
Comment on lines +2036 to +2040
Copy link
Collaborator

@AlexWaygood AlexWaygood Sep 30, 2025

Choose a reason for hiding this comment

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

or we could just ban it from being imported by adding a branch to _check_import_or_attribute, rather than emitting a separate diagnostic on each usage in every stub file?

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 thought about it, but I like this direct approach a bit more: While it may spam a bit in case someone uses it, it's more "direct". "Here's your error", instead of "You're importing something here that's going to be a problem later." It still allows overriding the warning on a case-by-cases basis (although I'm not sure why you would need that). No strong opinion, though.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel like emitting the diagnostic at the location of the import (or attribute access) is more in keeping with our other rules such as Y024 and Y057... but I also don't have a strong opinion :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, then let's laziness win and not change it?


@staticmethod
def _is_positional_pre_570_argname(name: str) -> bool:
# https://peps.python.org/pep-0484/#positional-only-arguments
Expand Down Expand Up @@ -2094,6 +2101,7 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
self.check_self_typevars(node, decorator_names)
if self.enclosing_class_ctx.is_protocol_class:
self.check_protocol_param_kinds(node, decorator_names)
self.check_for_override(node)

def visit_arg(self, node: ast.arg) -> None:
if _is_NoReturn(node.annotation):
Expand Down
24 changes: 24 additions & 0 deletions tests/override.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import typing
import typing as t
from typing import override, override as over

import typing_extensions

class Foo:
def f(self) -> None: ...
def g(self) -> None: ...
def h(self) -> None: ...
def j(self) -> None: ...
def k(self) -> None: ...

class Bar(Foo):
@override # Y068 Do not use "@override" in stub files.
def f(self) -> None: ...
@typing.override # Y068 Do not use "@override" in stub files.
def g(self) -> None: ...
@t.override # Ideally we'd catch this too, but the infrastructure is not in place.
def h(self) -> None: ...
@over # Ideally we'd catch this too, but the infrastructure is not in place.
def j(self) -> None: ...
@typing_extensions.override # Y068 Do not use "@override" in stub files.
def k(self) -> None: ...