From 4290838a55e4287a8f4b0224a53b36e4d8217816 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 23 Jul 2026 12:39:05 +0200 Subject: [PATCH 1/4] Y050: Flag `NoReturn` on import Closes: #551 --- CHANGELOG.md | 7 ++++++- ERRORCODES.md | 2 +- flake8_pyi/errors.py | 4 +--- flake8_pyi/visitor.py | 8 ++++---- tests/imports.pyi | 6 ++++-- tests/never_vs_noreturn.pyi | 17 ++++++++++++----- tests/pep695_py312.pyi | 2 -- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3af7805..dfbec671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,15 @@ flake8-pyi uses Calendar Versioning (CalVer). ## Unreleased +### Breaking Changes + +* Y050: Now flags imports of `typing.NoReturn` and `typing_extensions.NoReturn` + as it is now unconditionally deprecated. + ### Other changes * Support Python 3.15. -* Recommend to use `typing.TypeAlias` instead of `typing_extensions.TypeAlias`. +* Y026: Recommend to use `typing.TypeAlias` instead of `typing_extensions.TypeAlias`. ## 26.5.0 diff --git a/ERRORCODES.md b/ERRORCODES.md index 0c8f984e..aaa13e5f 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -63,7 +63,7 @@ The following warnings are currently emitted by default: | Y047 | A private `TypeAlias` should be used at least once in the file in which it is defined. | Redundant code | Y048 | Function bodies should contain exactly one statement. This is because stub files are never executed at runtime, so any more than one statement would be redundant. (Note that if a function body includes a docstring, the docstring counts as a "statement".) | Understanding stubs | Y049 | A private `TypedDict` should be used at least once in the file in which it is defined. | Redundant code -| Y050 | Prefer `typing_extensions.Never` over `typing.NoReturn` for argument annotations. | Style +| Y050 | Prefer `typing_extensions.Never` over `typing.NoReturn`. | Style | Y051 | Y051 detects redundant unions between `Literal` types and builtin supertypes. For example, `Literal[5]` is redundant in the union `int \| Literal[5]`, and `Literal[True]` is redundant in the union `Literal[True] \| bool`. | Redundant code | Y052 | Y052 disallows assignments to constant values where the assignment does not have a type annotation. For example, `x = 0` in the global namespace is ambiguous in a stub, as there are four different types that could be inferred for the variable `x`: `int`, `Final[int]`, `Literal[0]`, or `Final[Literal[0]]`. Enum members are excluded from this check, as are various special assignments such as `__all__` and `__match_args__`. | Correctness | Y053 | Only string and bytes literals <=50 characters long are permitted. (There are some exceptions, such as `Literal` subscripts, metadata strings inside `Annotated` subscripts, and strings passed to `@deprecated`.) | Style diff --git a/flake8_pyi/errors.py b/flake8_pyi/errors.py index 23616cac..a3e857d3 100644 --- a/flake8_pyi/errors.py +++ b/flake8_pyi/errors.py @@ -90,9 +90,7 @@ class Error(NamedTuple): Y047 = 'Y047 Type alias "{alias_name}" is not used' Y048 = "Y048 Function body should contain exactly one statement" Y049 = 'Y049 TypedDict "{typeddict_name}" is not used' -Y050 = ( - 'Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations' -) +Y050 = 'Y050 Use "typing_extensions.Never" instead of "{module}.NoReturn"' Y051 = 'Y051 "{literal_subtype}" is redundant in a union with "{builtin_supertype}"' Y052 = 'Y052 Need type annotation for "{variable}"' Y053 = "Y053 String and bytes literals >50 characters long are not permitted" diff --git a/flake8_pyi/visitor.py b/flake8_pyi/visitor.py index 4bb8d538..e0333e4b 100644 --- a/flake8_pyi/visitor.py +++ b/flake8_pyi/visitor.py @@ -217,8 +217,6 @@ def _is_object(node: ast.AST | None, name: str, *, from_: Container[str]) -> boo _is_AsyncIterable = partial( _is_object, name="AsyncIterable", from_=_TYPING_OR_COLLECTIONS_ABC ) -_is_Protocol = partial(_is_object, name="Protocol", from_=_TYPING_MODULES) -_is_NoReturn = partial(_is_object, name="NoReturn", from_=_TYPING_MODULES) _is_Final = partial(_is_object, name="Final", from_=_TYPING_MODULES) _is_Generator = partial(_is_object, name="Generator", from_=_TYPING_OR_COLLECTIONS_ABC) _is_AsyncGenerator = partial( @@ -812,6 +810,10 @@ def _check_import_or_attribute( if object_name == "Text": return errors.Y039.format(module=module_name) + # Y050 errors + if object_name == "NoReturn": + return errors.Y050.format(module=module_name) + # Y023 errors if module_name == "typing_extensions": if object_name in _BAD_TYPINGEXTENSIONS_Y023_IMPORTS: @@ -2065,8 +2067,6 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: self.check_for_override(node) def visit_arg(self, node: ast.arg) -> None: - if _is_NoReturn(node.annotation): - self.error(node, errors.Y050) if _is_Incomplete(node.annotation): self.error(node, errors.Y065.format(what=f'parameter "{node.arg}"')) with self.visiting_arg.enabled(): diff --git a/tests/imports.pyi b/tests/imports.pyi index 23a2d5d9..9dbc2028 100644 --- a/tests/imports.pyi +++ b/tests/imports.pyi @@ -63,15 +63,15 @@ from typing import ( TypedDict, AnyStr, NewType, - NoReturn, final, overload, ) from typing_extensions import ( + Annotated, Concatenate, + Never, ParamSpec, TypeGuard, - Annotated, ) # BAD IMPORTS (Y022 code) @@ -153,6 +153,8 @@ from typing import AbstractSet # Y038 Use "from collections.abc import Set as A from typing_extensions import AbstractSet # Y038 Use "from collections.abc import Set as AbstractSet" instead of "from typing_extensions import AbstractSet" (PEP 585 syntax) from typing import Text # Y039 Use "str" instead of "typing.Text" from typing_extensions import Text # Y039 Use "str" instead of "typing_extensions.Text" +from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" +from typing_extensions import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" from typing import ByteString # Y057 Do not use typing.ByteString, which has unclear semantics and is deprecated from collections.abc import ByteString # Y057 Do not use collections.abc.ByteString, which has unclear semantics and is deprecated diff --git a/tests/never_vs_noreturn.pyi b/tests/never_vs_noreturn.pyi index 2ef5643f..bf99068d 100644 --- a/tests/never_vs_noreturn.pyi +++ b/tests/never_vs_noreturn.pyi @@ -1,15 +1,22 @@ import typing -from typing import NoReturn +from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" import typing_extensions from typing_extensions import Never +from typing_extensions import NoReturn as NR # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" -def badfunc0(arg: NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations -def badfunc1(*args: typing.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations -def badfunc2(**kwargs: typing_extensions.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations # Y023 Use "typing.NoReturn" instead of "typing_extensions.NoReturn" -def badfunc3(*, arg: NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations +# NoReturn is now flagged at import time, so it should never be flagged elsewhere. + +x: NR + +def badfunc0(arg: NoReturn) -> None: ... +def badfunc1(*args: typing.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" +def badfunc2(**kwargs: typing_extensions.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" +def badfunc3(*, arg: NoReturn) -> None: ... +def badfunc4() -> NoReturn: ... def goodfunc0(arg: Never) -> None: ... def goodfunc1(*args: typing.Never) -> None: ... def goodfunc2(**kwargs: typing_extensions.Never) -> None: ... def goodfunc3(*, arg: Never) -> None: ... +def goodfunc4() -> Never: ... diff --git a/tests/pep695_py312.pyi b/tests/pep695_py312.pyi index 7bdc1f4c..e155d470 100644 --- a/tests/pep695_py312.pyi +++ b/tests/pep695_py312.pyi @@ -5,7 +5,6 @@ from typing import ( Any, Literal, NamedTuple, - NoReturn, Protocol, Self, TypedDict, @@ -30,7 +29,6 @@ class GenericPEP695Class[T]: pass # Y012 Class body must not contain "pass" def __exit__(self, *args: Any) -> None: ... # Y036 Badly defined __exit__ method: Star-args in an __exit__ method should be annotated with "object", not "Any" async def __aexit__(self) -> None: ... # Y036 Badly defined __aexit__ method: If there are no star-args, there should be at least 3 non-keyword-only args in an __aexit__ method (excluding "self") - def never_call_me(self, arg: NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "NoReturn" for argument annotations class GenericPEP695InheritingFromObject[T](object): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3 x: T From d738d47be9699356e607ff82d25ef3270e3379f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:42:43 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/never_vs_noreturn.pyi | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/never_vs_noreturn.pyi b/tests/never_vs_noreturn.pyi index bf99068d..dd825cc3 100644 --- a/tests/never_vs_noreturn.pyi +++ b/tests/never_vs_noreturn.pyi @@ -1,9 +1,13 @@ import typing -from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" +from typing import ( + NoReturn, # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" +) import typing_extensions -from typing_extensions import Never -from typing_extensions import NoReturn as NR # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" +from typing_extensions import ( # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" + Never, + NoReturn as NR, +) # NoReturn is now flagged at import time, so it should never be flagged elsewhere. From 922bb70dcf9910e55ac39a0dfcac6878306d3a17 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 23 Jul 2026 12:48:46 +0200 Subject: [PATCH 3/4] Fix auto-formatting --- tests/never_vs_noreturn.pyi | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/never_vs_noreturn.pyi b/tests/never_vs_noreturn.pyi index dd825cc3..c50ea380 100644 --- a/tests/never_vs_noreturn.pyi +++ b/tests/never_vs_noreturn.pyi @@ -1,13 +1,11 @@ +# isort: skip_file import typing -from typing import ( - NoReturn, # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" -) + +from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" import typing_extensions -from typing_extensions import ( # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" - Never, - NoReturn as NR, -) +from typing_extensions import Never +from typing_extensions import NoReturn as NR # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" # NoReturn is now flagged at import time, so it should never be flagged elsewhere. From d4553ca2a47cafaf776bd03bd9e428f7c347aac0 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Fri, 24 Jul 2026 20:32:53 +0200 Subject: [PATCH 4/4] Use Y069 and return Y050 --- CHANGELOG.md | 11 ++++++++--- ERRORCODES.md | 2 +- flake8_pyi/errors.py | 2 +- flake8_pyi/visitor.py | 4 ++-- tests/imports.pyi | 4 ++-- tests/never_vs_noreturn.pyi | 8 ++++---- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfbec671..af3a3197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,15 @@ flake8-pyi uses Calendar Versioning (CalVer). ## Unreleased -### Breaking Changes +### New Error Codes + +* Y069: Flags imports of `typing.NoReturn` and `typing_extensions.NoReturn` + as they are now unconditionally deprecated. + +### Removed Error Codes -* Y050: Now flags imports of `typing.NoReturn` and `typing_extensions.NoReturn` - as it is now unconditionally deprecated. +* Y050: Old error code that flagged `typing.NoReturn` and + `typing_extensions.NoReturn` in argument positions only. ### Other changes diff --git a/ERRORCODES.md b/ERRORCODES.md index aaa13e5f..a1440cd9 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -63,7 +63,6 @@ The following warnings are currently emitted by default: | Y047 | A private `TypeAlias` should be used at least once in the file in which it is defined. | Redundant code | Y048 | Function bodies should contain exactly one statement. This is because stub files are never executed at runtime, so any more than one statement would be redundant. (Note that if a function body includes a docstring, the docstring counts as a "statement".) | Understanding stubs | Y049 | A private `TypedDict` should be used at least once in the file in which it is defined. | Redundant code -| Y050 | Prefer `typing_extensions.Never` over `typing.NoReturn`. | Style | Y051 | Y051 detects redundant unions between `Literal` types and builtin supertypes. For example, `Literal[5]` is redundant in the union `int \| Literal[5]`, and `Literal[True]` is redundant in the union `Literal[True] \| bool`. | Redundant code | Y052 | Y052 disallows assignments to constant values where the assignment does not have a type annotation. For example, `x = 0` in the global namespace is ambiguous in a stub, as there are four different types that could be inferred for the variable `x`: `int`, `Final[int]`, `Literal[0]`, or `Final[Literal[0]]`. Enum members are excluded from this check, as are various special assignments such as `__all__` and `__match_args__`. | Correctness | Y053 | Only string and bytes literals <=50 characters long are permitted. (There are some exceptions, such as `Literal` subscripts, metadata strings inside `Annotated` subscripts, and strings passed to `@deprecated`.) | Style @@ -82,6 +81,7 @@ The following warnings are currently emitted by default: | Y066 | When using if/else with `sys.version_info`, put the code for new Python versions first. | Style | Y067 | Don't use `Incomplete \| None = None` in argument annotations. Instead, just use `=None`. | Style | Y068 | 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 +| Y069 | Prefer `typing_extensions.Never` over `typing.NoReturn`. | Style ## Warnings disabled by default diff --git a/flake8_pyi/errors.py b/flake8_pyi/errors.py index a3e857d3..1492cbd8 100644 --- a/flake8_pyi/errors.py +++ b/flake8_pyi/errors.py @@ -90,7 +90,6 @@ class Error(NamedTuple): Y047 = 'Y047 Type alias "{alias_name}" is not used' Y048 = "Y048 Function body should contain exactly one statement" Y049 = 'Y049 TypedDict "{typeddict_name}" is not used' -Y050 = 'Y050 Use "typing_extensions.Never" instead of "{module}.NoReturn"' Y051 = 'Y051 "{literal_subtype}" is redundant in a union with "{builtin_supertype}"' Y052 = 'Y052 Need type annotation for "{variable}"' Y053 = "Y053 String and bytes literals >50 characters long are not permitted" @@ -126,6 +125,7 @@ class Error(NamedTuple): ) Y067 = 'Y067 Use "=None" instead of "Incomplete | None = None"' Y068 = 'Y068 Do not use "@override" in stub files.' +Y069 = 'Y069 Use "typing_extensions.Never" instead of "{module}.NoReturn"' Y090 = ( 'Y090 "{original}" means ' diff --git a/flake8_pyi/visitor.py b/flake8_pyi/visitor.py index e0333e4b..77e850a9 100644 --- a/flake8_pyi/visitor.py +++ b/flake8_pyi/visitor.py @@ -810,9 +810,9 @@ def _check_import_or_attribute( if object_name == "Text": return errors.Y039.format(module=module_name) - # Y050 errors + # Y069 errors if object_name == "NoReturn": - return errors.Y050.format(module=module_name) + return errors.Y069.format(module=module_name) # Y023 errors if module_name == "typing_extensions": diff --git a/tests/imports.pyi b/tests/imports.pyi index 9dbc2028..f6fc6187 100644 --- a/tests/imports.pyi +++ b/tests/imports.pyi @@ -153,8 +153,8 @@ from typing import AbstractSet # Y038 Use "from collections.abc import Set as A from typing_extensions import AbstractSet # Y038 Use "from collections.abc import Set as AbstractSet" instead of "from typing_extensions import AbstractSet" (PEP 585 syntax) from typing import Text # Y039 Use "str" instead of "typing.Text" from typing_extensions import Text # Y039 Use "str" instead of "typing_extensions.Text" -from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" -from typing_extensions import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" +from typing import NoReturn # Y069 Use "typing_extensions.Never" instead of "typing.NoReturn" +from typing_extensions import NoReturn # Y069 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" from typing import ByteString # Y057 Do not use typing.ByteString, which has unclear semantics and is deprecated from collections.abc import ByteString # Y057 Do not use collections.abc.ByteString, which has unclear semantics and is deprecated diff --git a/tests/never_vs_noreturn.pyi b/tests/never_vs_noreturn.pyi index c50ea380..0914f0ad 100644 --- a/tests/never_vs_noreturn.pyi +++ b/tests/never_vs_noreturn.pyi @@ -1,19 +1,19 @@ # isort: skip_file import typing -from typing import NoReturn # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" +from typing import NoReturn # Y069 Use "typing_extensions.Never" instead of "typing.NoReturn" import typing_extensions from typing_extensions import Never -from typing_extensions import NoReturn as NR # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" +from typing_extensions import NoReturn as NR # Y069 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" # NoReturn is now flagged at import time, so it should never be flagged elsewhere. x: NR def badfunc0(arg: NoReturn) -> None: ... -def badfunc1(*args: typing.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "typing.NoReturn" -def badfunc2(**kwargs: typing_extensions.NoReturn) -> None: ... # Y050 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" +def badfunc1(*args: typing.NoReturn) -> None: ... # Y069 Use "typing_extensions.Never" instead of "typing.NoReturn" +def badfunc2(**kwargs: typing_extensions.NoReturn) -> None: ... # Y069 Use "typing_extensions.Never" instead of "typing_extensions.NoReturn" def badfunc3(*, arg: NoReturn) -> None: ... def badfunc4() -> NoReturn: ...