Skip to content

feat: Add support Python 3.15 and lazy imports - #211

Open
Daverball wants to merge 5 commits into
mainfrom
feat/py315
Open

Daverball wants to merge 5 commits into
mainfrom
feat/py315

Conversation

@Daverball

@Daverball Daverball commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Python 3.15 adds lazy imports which provide a new way to reduce the import overhead of type hints. It is however not as reliable, since inspecting __annotations__ will always trigger the lazy imports for symbols used in annotations, even if a format like annotationlib.Format.FORWARDREF is used and there is no way to inspect it in a different way that doesn't trigger the imports.

Nevertheless we want to recognize lazy imports and not push people away from them, so for the purposes of TC001, TC002 and TC003 these imports are ignored, just like existing imports inside type checking blocks are. On the contrary we will never flag lazy imports with TC004 or intend to add a similar rule for lazy imports, since there's both many other reasons why you may want the import to be lazy (like breaking import cycles, without putting the import into a function) and no clear indication for when deferred annotations will be inspected, so we mostly couldn't tell when the lazy imports will be triggered anyways. We instead defer the management of lazy imports to other existing linters like flake8-lazy and ruff, which both provide dedicated rules for managing them and should work well hand in hand with flake8-type-checking in this mode.

For good interoperability between flake8-lazy and flake8-type-checking we support their f-string based way of putting relative imports into __lazy_modules__, since otherwise there's no reliable way to map relative imports to the absolute names used in __lazy_modules__, without adding a lot of code, configuration options and overhead to determine the parent module(s) of the file we're linting.

Unlike flake8-lazy, we have the luxury of flagging relative imports as lazy way more eagerly, since the potential cost of a small number of false negatives for TC001 is quite small. So we try to map relative imports to any absolute names in __lazy_modules__ that could match and also support some other esoteric ways to construct the absolute name using expressions, like using + instead of an f-string or storing the result of __spec__.parent into a temporary variable.

There are a couple of new settings related to lazy imports. Please refer to the README for when and how to use them.

This also drops support for Python 3.10 which will reach its EOL at the end of October (this also seemed like a good opportunity for us to refactor some our AST traversal code into match statements, which are well suited for this task).


This also adds zizmor as a security linter for our GitHub Actions workflows. I used actions-up to update and pin the workflows to their current latest version in order to comply with zizmor's recommendations.

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.04142% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.3%. Comparing base (9a76be3) to head (57501d1).

Files with missing lines Patch % Lines
flake8_type_checking/checker.py 96.9% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main    #211     +/-   ##
=======================================
+ Coverage   96.5%   97.3%   +0.7%     
=======================================
  Files          4       4             
  Lines        968    1004     +36     
=======================================
+ Hits         935     977     +42     
+ Misses        33      27      -6     
Files with missing lines Coverage Δ
flake8_type_checking/constants.py 100.0% <100.0%> (ø)
flake8_type_checking/plugin.py 100.0% <100.0%> (ø)
flake8_type_checking/checker.py 97.0% <96.9%> (+0.7%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Daverball
Daverball requested a review from sondrelg September 24, 2026 07:37
@Daverball
Daverball force-pushed the feat/py315 branch 2 times, most recently from 00bd6a8 to e7bd178 Compare September 24, 2026 09:51
@Daverball

Copy link
Copy Markdown
Collaborator Author

@sondrelg Do you think you could take a quick look at what's being proposed here? (I don't think this needs an in-depth line-by-line review, but if you want to do that, I recommend looking at the individual commits separately, to get a cleaner diff) That way we could have this ready in time for the release of Python 3.15

Since the only adverse outcome for detecting relative lazy imports via
`__lazy_modules__` too eagerly is a false negative for that specific
import, we can be a lot more forgiving and match both any absolute
import names that could map to this relative import and also match other
esoteric ways to write relative imports using expressions other than the
conventions defined by `flake8-lazy`.

This improves interoperability with ruff, which currently does not
support `flake8-lazy`'s relative import conventions and generally
allows users to be more free in how they handle relative lazy imports
with `__lazy_modules__`.
@Daverball

Copy link
Copy Markdown
Collaborator Author

@sondrelg Pinging you one more time. Python 3.15 has been released, so if I don't get any feedback from you within the next few weeks I will exercise my own judgment. It seems better to have something working ready sooner rather than later, so early adopters can keep relying on flake8-type-checking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant