Repository: python/typeshed
Pull request #13761: Import names from typing directly rather than importing module
Author: login: Avasam
Created: 2025-04-02T00:15:14Z | Merged: 2025-04-12T17:10:09Z

Import names from `typing` and `typing_extensions` directly rather than importing module. (except where aliasing `Self as _Self` or `TypeAlias as _TypeAlias` would cause issues for flake8-pyi's [Y015](https://github.com/PyCQA/flake8-pyi/blob/main/ERRORCODES.md#Y015) and [Y034](https://github.com/PyCQA/flake8-pyi/blob/main/ERRORCODES.md#Y034))
And avoid aliasing said modules.

This was prompted by this comment: https://github.com/python/typeshed/pull/11204#discussion_r2022774951 where I realized we had some inconsistencies.


Diff:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6ed7dede571a..bf2fbc3f7488 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,7 +22,7 @@ repos:
         name: Run ruff on the test cases
         args:
           - "--exit-non-zero-on-fix"
-          - "--select=FA,I,RUF100"
+          - "--select=FA,I,ICN001,RUF100"
           - "--no-force-exclude"
           - "--unsafe-fixes"
         files: '.*test_cases/.+\.py$'
diff --git a/pyproject.toml b/pyproject.toml
index 63e187ac1dcd..5d6bd434156b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,6 +83,8 @@ select = [
     "FURB169", # Compare the identities of `{object}` and None instead of their respective types
     "FURB177", # Prefer `Path.cwd()` over `Path().resolve()` for current-directory lookups
     "FURB187", # Use of assignment of `reversed` on list `{name}`
+    # Used for lint.flake8-import-conventions.aliases
+    "ICN001", # `{name}` should be imported as `{asname}`
     # Autofixable flake8-use-pathlib only
     "PTH201", # Do not pass the current directory explicitly to `Path`
     "PTH210", # Invalid suffix passed to `.with_suffix()`
@@ -217,6 +219,8 @@ ignore = [
     "PLC0414", # Import alias does not rename original package
 ]
 "*_pb2.pyi" = [
+    # Special autogenerated typing --> typing_extensions aliases
+    "ICN001", # `{name}` should be imported as `{asname}`
     # Leave the docstrings as-is, matching source
     "D", # pydocstyle
     # See comment on black's force-exclude config above
@@ -226,6 +230,11 @@ ignore = [
 [tool.ruff.lint.pydocstyle]
 convention = "pep257" # https://docs.astral.sh/ruff/settings/#lint_pydocstyle_convention
 
+[tool.ruff.lint.flake8-import-conventions.aliases]
+# Prevent aliasing these, as it causes false-negatives for certain rules
+typing_extensions = "typing_extensions"
+typing = "typing"
+
 [tool.ruff.lint.isort]
 split-on-trailing-comma = false
 combine-as-imports = true
diff --git a/stdlib/@tests/test_cases/check_re.py b/stdlib/@tests/test_cases/check_re.py
index b6ab2b0d59d2..dee87b474fe2 100644
--- a/stdlib/@tests/test_cases/check_re.py
+++ b/stdlib/@tests/test_cases/check_re.py
@@ -2,18 +2,18 @@
 
 import mmap
 import re
-import typing as t
+from typing import AnyStr, Match, Optional
 from typing_extensions import assert_type
 
 
 def check_search(str_pat: re.Pattern[str], bytes_pat: re.Pattern[bytes]) -> None:
-    assert_type(str_pat.search("x"), t.Optional[t.Match[str]])
-    assert_type(bytes_pat.search(b"x"), t.Optional[t.Match[bytes]])
-    assert_type(bytes_pat.search(bytearray(b"x")), t.Optional[t.Match[bytes]])
-    assert_type(bytes_pat.search(mmap.mmap(0, 10)), t.Optional[t.Match[bytes]])
+    assert_type(str_pat.search("x"), Optional[Match[str]])
+    assert_type(bytes_pat.search(b"x"), Optional[Match[bytes]])
+    assert_type(bytes_pat.search(bytearray(b"x")), Optional[Match[bytes]])
+    assert_type(bytes_pat.search(mmap.mmap(0, 10)), Optional[Match[bytes]])
 
 
-def check_search_with_AnyStr(pattern: re.Pattern[t.AnyStr], string: t.AnyStr) -> re.Match[t.AnyStr]:
+def check_search_with_AnyStr(pattern: re.Pattern[AnyStr], string: AnyStr) -> re.Match[AnyStr]:
     """See issue #9591"""
     match = pattern.search(string)
     if match is None:
diff --git a/stdlib/@tests/test_cases/typing/check_regression_issue_9296.py b/stdlib/@tests/test_cases/typing/check_regression_issue_9296.py
index 34c5631aeb1a..23beaa87ae05 100644
--- a/stdlib/@tests/test_cases/typing/check_regression_issue_9296.py
+++ b/stdlib/@tests/test_cases/typing/check_regression_issue_9296.py
@@ -1,15 +1,15 @@
 from __future__ import annotations
 
-import typing as t
+from typing import Any, KeysView, TypeVar
 
-KT = t.TypeVar("KT")
+KT = TypeVar("KT")
 
 
-class MyKeysView(t.KeysView[KT]):
+class MyKeysView(KeysView[KT]):
     pass
 
 
-d: dict[t.Any, t.Any] = {}
+d: dict[Any, Any] = {}
 dict_keys = type(d.keys())
 
 # This should not cause an error like `Member "register" is unknown`:
diff --git a/stdlib/_typeshed/__init__.pyi b/stdlib/_typeshed/__init__.pyi
index 99d21b67360a..a503637998d0 100644
--- a/stdlib/_typeshed/__init__.pyi
+++ b/stdlib/_typeshed/__init__.pyi
@@ -3,7 +3,6 @@
 # See the README.md file in this directory for more information.
 
 import sys
-import typing_extensions
 from collections.abc import Awaitable, Callable, Iterable, Sequence, Set as AbstractSet, Sized
 from dataclasses import Field
 from os import PathLike
@@ -23,7 +22,7 @@ from typing import (
     final,
     overload,
 )
-from typing_extensions import Buffer, LiteralString, TypeAlias
+from typing_extensions import Buffer, LiteralString, Self as _Self, TypeAlias
 
 _KT = TypeVar("_KT")
 _KT_co = TypeVar("_KT_co", covariant=True)
@@ -329,9 +328,9 @@ class structseq(Generic[_T_co]):
     # The second parameter will accept a dict of any kind without raising an exception,
     # but only has any meaning if you supply it a dict where the keys are strings.
     # https://github.com/python/typeshed/pull/6560#discussion_r767149830
-    def __new__(cls, sequence: Iterable[_T_co], dict: dict[str, Any] = ...) -> typing_extensions.Self: ...
+    def __new__(cls, sequence: Iterable[_T_co], dict: dict[str, Any] = ...) -> _Self: ...
     if sys.version_info >= (3, 13):
-        def __replace__(self, **kwargs: Any) -> typing_extensions.Self: ...
+        def __replace__(self, **kwargs: Any) -> _Self: ...
 
 # Superset of typing.AnyStr that also includes LiteralString
 AnyOrLiteralStr = TypeVar("AnyOrLiteralStr", str, bytes, LiteralString)  # noqa: Y001
diff --git a/stdlib/typing_extensions.pyi b/stdlib/typing_extensions.pyi
index 3799f4e666e9..bad5fae880c0 100644
--- a/stdlib/typing_extensions.pyi
+++ b/stdlib/typing_extensions.pyi
@@ -1,7 +1,6 @@
 import abc
 import enum
 import sys
-import typing
 from _collections_abc import dict_items, dict_keys, dict_values
 from _typeshed import IdentityFunction, Incomplete, Unused
 from collections.abc import (
@@ -57,6 +56,7 @@ from typing import (  # noqa: Y022,Y037,Y038,Y039,UP035
     Tuple as Tuple,
     Type as Type,
     TypedDict as TypedDict,
+    TypeVar as _TypeVar,
     Union as Union,
     _Alias,
     cast as cast,
@@ -195,10 +195,10 @@ __all__ = [
     "CapsuleType",
 ]
 
-_T = typing.TypeVar("_T")
-_F = typing.TypeVar("_F", bound=Callable[..., Any])
-_TC = typing.TypeVar("_TC", bound=type[object])
-_T_co = typing.TypeVar("_T_co", covariant=True)  # Any type covariant containers.
+_T = _TypeVar("_T")
+_F = _TypeVar("_F", bound=Callable[..., Any])
+_TC = _TypeVar("_TC", bound=type[object])
+_T_co = _TypeVar("_T_co", covariant=True)  # Any type covariant containers.
 
 class _Final: ...  # This should be imported from typing but that breaks pytype
 
diff --git a/stubs/click-default-group/click_default_group.pyi b/stubs/click-default-group/click_default_group.pyi
index 5c73c4dd8db4..83541f3cfbbf 100644
--- a/stubs/click-default-group/click_default_group.pyi
+++ b/stubs/click-default-group/click_default_group.pyi
@@ -1,5 +1,5 @@
-import typing as t
 from _typeshed import Incomplete
+from collections.abc import Sequence
 
 import click
 
@@ -23,7 +23,7 @@ class DefaultCommandFormatter:
     formatter: click.HelpFormatter
     mark: str
     def __init__(self, group: click.Group, formatter: click.HelpFormatter, mark: str = ...) -> None: ...
-    def write_dl(self, rows: t.Sequence[tuple[str, str]], col_max: int = 30, col_spacing: int = -2) -> None: ...
+    def write_dl(self, rows: Sequence[tuple[str, str]], col_max: int = 30, col_spacing: int = -2) -> None: ...
     def __getattr__(self, attr: str) -> Incomplete: ...
     # __getattr__ used to ala-derive from click.HelpFormatter:
     # indent_increment: int
diff --git a/stubs/click-log/click_log/options.pyi b/stubs/click-log/click_log/options.pyi
index e1310ab8ea44..f5fa67d96eac 100644
--- a/stubs/click-log/click_log/options.pyi
+++ b/stubs/click-log/click_log/options.pyi
@@ -1,12 +1,11 @@
 import logging
-import typing as t
+from collections.abc import Callable
+from typing import Any, TypeVar
 from typing_extensions import TypeAlias
 
 import click
 
-_AnyCallable: TypeAlias = t.Callable[..., t.Any]
-_FC = t.TypeVar("_FC", bound=_AnyCallable | click.Command)
+_AnyCallable: TypeAlias = Callable[..., Any]
+_FC = TypeVar("_FC", bound=_AnyCallable | click.Command)
 
-def simple_verbosity_option(
-    logger: logging.Logger | str | None = None, *names: str, **kwargs: t.Any
-) -> t.Callable[[_FC], _FC]: ...
+def simple_verbosity_option(logger: logging.Logger | str | None = None, *names: str, **kwargs: Any) -> Callable[[_FC], _FC]: ...
diff --git a/stubs/click-web/click_web/web_click_types.pyi b/stubs/click-web/click_web/web_click_types.pyi
index b0e9e378fa5a..d3a0d9bcc116 100644
--- a/stubs/click-web/click_web/web_click_types.pyi
+++ b/stubs/click-web/click_web/web_click_types.pyi
@@ -1,17 +1,19 @@
 import re
-from typing import ClassVar
+from typing import ClassVar, TypeVar
 
 import click
 
+_T = TypeVar("_T")
+
 class EmailParamType(click.ParamType):
     EMAIL_REGEX: ClassVar[re.Pattern[str]]
-    def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> str | None: ...
+    def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> str: ...
 
 class PasswordParamType(click.ParamType):
-    def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> str | None: ...
+    def convert(self, value: _T, param: click.Parameter | None, ctx: click.Context | None) -> _T: ...
 
 class TextAreaParamType(click.ParamType):
-    def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> str | None: ...
+    def convert(self, value: _T, param: click.Parameter | None, ctx: click.Context | None) -> _T: ...
 
 EMAIL_TYPE: EmailParamType
 PASSWORD_TYPE: PasswordParamType
diff --git a/stubs/corus/corus/third/WikiExtractor.pyi b/stubs/corus/corus/third/WikiExtractor.pyi
index b8ee370c8a26..bda32a5d62a1 100644
--- a/stubs/corus/corus/third/WikiExtractor.pyi
+++ b/stubs/corus/corus/third/WikiExtractor.pyi
@@ -1,4 +1,3 @@
-import typing
 from _typeshed import Incomplete
 from collections.abc import Generator
 from math import (
@@ -14,6 +13,7 @@ from math import (
     tan as tan,
     trunc as trunc,
 )
+from typing import TypeVar
 
 PY2: Incomplete
 text_type = str
@@ -52,7 +52,7 @@ quote_quote: Incomplete
 spaces: Incomplete
 dots: Incomplete
 
-_T = typing.TypeVar("_T")
+_T = TypeVar("_T")
 
 class Template(list[_T]):
     @classmethod


Reviews:
user: login: Avasam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T00:15:54Z
user: login: Avasam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T00:34:16Z
user: login: Avasam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-02T00:35:36Z
user: login: srittau
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T07:05:45Z

Review comments:
diff_hunk: @@ -2,18 +2,18 @@
 
 import mmap
 import re
-import typing as t
+from typing import AnyStr, Match
path: stdlib/@tests/test_cases/check_re.py
user: login: Avasam
body: I'm assuming that importing `Match` from `typing` rather than `re` here is intended for the test.
created_at: 2025-04-02T00:15:53Z
author_association: COLLABORATOR
diff_hunk: @@ -212,6 +216,11 @@ ignore = [
 [tool.ruff.lint.pydocstyle]
 convention = "pep257" # https://docs.astral.sh/ruff/settings/#lint_pydocstyle_convention
 
+[tool.ruff.lint.flake8-import-conventions.aliases]
+# Prevent aliasing these, as it causes false-negatives for certain rules
+typing_extensions = "typing_extensions"
+typing = "typing"
+
path: pyproject.toml
user: login: Avasam
body: This prevents `import typing as t`
created_at: 2025-04-02T00:34:16Z
author_association: COLLABORATOR
diff_hunk: @@ -1,17 +1,19 @@
 import re
-import typing as t
+from typing import TypeVar
 
 import click
 
+_T = TypeVar("_T")
+
 class EmailParamType(click.ParamType):
     EMAIL_REGEX: re.Pattern[str]
-    def convert(self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None) -> t.Any: ...
+    def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> str: ...
 
 class PasswordParamType(click.ParamType):
-    def convert(self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None) -> t.Any: ...
+    def convert(self, value: _T, param: click.Parameter | None, ctx: click.Context | None) -> _T: ...
 
 class TextAreaParamType(click.ParamType):
-    def convert(self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None) -> t.Any: ...
+    def convert(self, value: _T, param: click.Parameter | None, ctx: click.Context | None) -> _T: ...
path: stubs/click-web/click_web/web_click_types.pyi
user: login: Avasam
body: @PyHedgehog since you added this stub in #13208
Was there a specific reason to have these as `Any`?
created_at: 2025-04-02T00:35:36Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-02T00:37:20Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-12T15:51:08Z
body: According to [mypy_primer](https://github.com/hauntsaninja/mypy_primer), this change has no effect on the checked open source code. 🤖🎉
author_association: CONTRIBUTOR