up-type-checking-lazy-import (SMP306)¶
Flags an if TYPE_CHECKING: block that holds only imports and no else, where Python 3.15 can spell each import as a
lazy import. The guard is then only needed for names that are not imported at runtime, which lazy covers.
Why is this bad?¶
The guard keeps the imports out of the running program, so the names they bind exist only for type checkers, and code
that reads them at runtime fails. A lazy import binds the name at module level and runs the import on first use
(PEP 810), so the guard is no longer needed and a name used at runtime resolves.
Example¶
Example: fix
pyproject.toml
1[project]
2name = "demo"
3requires-python = ">=3.15"
Before
1# Demo module.
2from typing import TYPE_CHECKING
3
4if TYPE_CHECKING:
5 from collections.abc import Sequence
6
7
8def first(values: Sequence[int]) -> int:
9 return values[0]
After
1# Demo module.
2
3lazy from collections.abc import Sequence
4
5
6def first(values: Sequence[int]) -> int:
7 return values[0]
Configuration¶
The Python version the code targets, taken from
target-versionin[tool.ruff], falling back toproject.requires-python; see Python versionExample: configuration
pyproject.toml1[tool.ruff] 2target-version = "py314"
1from typing import TYPE_CHECKING 2 3if TYPE_CHECKING: 4 from collections.abc import Sequence
What it does¶
Active on¶
if TYPE_CHECKING:(ortyping.TYPE_CHECKING) at module level, with a body ofimportandfrom ... importstatements only and noelseorelif. The report sits on the condition. The fix is unsafe: alazyimport loads the module on first use, so a name a program never touches costs nothing, while a name it does touch runs the import then. An import cycle or a module missing at runtime, such as_typeshed, then surfaces at that point instead of never.Example: fix
pyproject.toml1[project] 2name = "demo" 3requires-python = ">=3.15"
Before
1# Demo module. 2from typing import TYPE_CHECKING 3 4if TYPE_CHECKING: 5 from os import PathLike 6 7 8def path(value: PathLike) -> None: 9 pass
After
1# Demo module. 2 3lazy from os import PathLike 4 5 6def path(value: PathLike) -> None: 7 pass
Exceptions¶
Blocks with an
else, anelif, or other statements: the rule needs a block that only imports.Example: not reported
pyproject.toml1[project] 2name = "demo" 3requires-python = ">=3.15"
1from typing import TYPE_CHECKING 2 3if TYPE_CHECKING: 4 from os import PathLike 5else: 6 PathLike = str
Imports of
from __future__andfrom x import *:lazyhas no form for them.Example: not reported
pyproject.toml1[project] 2name = "demo" 3requires-python = ">=3.15"
1from typing import TYPE_CHECKING 2 3if TYPE_CHECKING: 4 from os import *
Blocks inside another statement, such as a
tryor a function:lazyimports are not allowed there, so the rule does not look at them.Example: not reported
pyproject.toml1[project] 2name = "demo" 3requires-python = ">=3.15"
1from typing import TYPE_CHECKING 2 3try: 4 if TYPE_CHECKING: 5 from os import PathLike 6except ImportError: 7 pass
Fix¶
The fix is unsafe, as described under “Active on” above. It replaces the block with one lazy import per statement,
and, when TYPE_CHECKING is then unused, removes its from typing import TYPE_CHECKING line. A block with a comment in
it is reported without a fix, because the fix would drop the comment. A block importing _typeshed is too.
Example: fix
pyproject.toml
1[project]
2name = "demo"
3requires-python = ">=3.15"
Before
1# Demo module.
2from typing import TYPE_CHECKING
3
4if TYPE_CHECKING:
5 from os import PathLike
After
1# Demo module.
2
3lazy from os import PathLike