Skip to content

fix: faster CLI on Python 3.15#2797

Open
henryiii wants to merge 7 commits into
pypa:mainfrom
henryiii:henryiii/chore/fasthelp
Open

fix: faster CLI on Python 3.15#2797
henryiii wants to merge 7 commits into
pypa:mainfrom
henryiii:henryiii/chore/fasthelp

Conversation

@henryiii

@henryiii henryiii commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

This makes running cibuildwheel --help or cibuildwheel --print-build-indentifiers around 3-4x faster on Python 3.15a7 by making imports lazy. If using uv on a first run (like with uvx), this likely is even more, since it doesn't pre-compile .pyc files by default.

I used my flake8-lazy package (post here) to come up with the
__lazy_modules__ lists, and GPT 5 (due to the 0x token usage) in VSCode's copilot to apply the lists to the files.

I restored from __future__ import annotations, but maybe I didn't need to, since they are not resolved on 3.14+. I might try without too later. I think it's fine, but #2799 and #2798 were exposed while trying this. Will minimize the diff after those.

Edit: used the new --apply feature of flake8-lazy to create this instead.

Benchmark 1: .venv/bin/python3.14 -m cibuildwheel -h
  Time (mean ± σ):     250.7 ms ±   5.2 ms    [User: 201.2 ms, System: 28.9 ms]
  Range (min … max):   243.1 ms … 258.9 ms    11 runs

Benchmark 1: .venv/bin/python3.15 -m cibuildwheel -h
  Time (mean ± σ):      59.4 ms ±   4.6 ms    [User: 48.0 ms, System: 8.5 ms]
  Range (min … max):    55.6 ms …  80.3 ms    49 runs
Benchmark 1: .venv/bin/python3.14 -m cibuildwheel --print-build-identifiers
  Time (mean ± σ):     249.9 ms ±   4.3 ms    [User: 199.5 ms, System: 27.4 ms]
  Range (min … max):   242.8 ms … 258.3 ms    11 runs

Benchmark 1: .venv/bin/python3.15 -m cibuildwheel --print-build-identifiers
  Time (mean ± σ):      64.8 ms ±   1.0 ms    [User: 53.5 ms, System: 9.0 ms]
  Range (min … max):    63.4 ms …  68.0 ms    41 runs

Here's the version with @hugovk's CPython fix (thanks to @hugovk!):

$ hyperfine --warmup 10 \
   "./python.exe -m cibuildwheel --help" \
   "PYTHON_LAZY_IMPORTS=none ./python.exe -m cibuildwheel --help"
Benchmark 1: ./python.exe -m cibuildwheel --help
  Time (mean ± σ):      56.2 ms ±   1.5 ms    [User: 46.5 ms, System: 8.6 ms]
  Range (min … max):    53.6 ms …  62.3 ms    50 runs

Benchmark 2: PYTHON_LAZY_IMPORTS=none ./python.exe -m cibuildwheel --help
  Time (mean ± σ):     173.9 ms ±   2.3 ms    [User: 151.8 ms, System: 19.9 ms]
  Range (min … max):   169.9 ms … 177.5 ms    17 runs

Summary
  ./python.exe -m cibuildwheel --help ran
    3.09 ± 0.09 times faster than PYTHON_LAZY_IMPORTS=none ./python.exe -m cibuildwheel --help

@hugovk

hugovk commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

henryiii#17 defers more imports to be 1.4x faster than this PR and 4x faster than upstream main.

@agriyakhetarpal

Copy link
Copy Markdown
Member

@henryiii should this land in rc2?

@henryiii

Copy link
Copy Markdown
Contributor Author

This can land soon, but no need to be in 4.0 exactly. It's not a breaking change or anything.

@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch from 564a09c to 57b1173 Compare May 17, 2026 18:50
@henryiii henryiii marked this pull request as ready for review May 18, 2026 06:05
@henryiii henryiii requested a review from Copilot May 18, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR significantly speeds up the cibuildwheel CLI startup time (3-4x faster on Python 3.15a7) by leveraging Python 3.15's lazy imports feature (PEP 810). It does this by:

  1. Adding __lazy_modules__ lists to most modules (generated by the author's flake8-lazy tool) to mark which imports should be deferred until first use.
  2. Replacing from typing import TYPE_CHECKING with a module-level TYPE_CHECKING = False constant, so that the typing module itself can be lazily imported.
  3. Moving type-only imports (e.g. Self, Literal, Sequence, Mapping, Any, etc.) into if TYPE_CHECKING: blocks, and adding from __future__ import annotations to files that need it so runtime annotation evaluation isn't needed. As a side effect, Self references in Architecture and a few other classes are replaced with the concrete class name (and some @classmethods become @staticmethods).
  4. Adding a ruff rule to ban typing.TYPE_CHECKING imports in favor of the new TYPE_CHECKING = False pattern.

Changes:

  • Add __lazy_modules__ lists and reorganize imports throughout the package for PEP 810 lazy loading.
  • Replace from typing import TYPE_CHECKING with TYPE_CHECKING = False, and ban the old form via ruff.
  • Rework Architecture/EnableGroup/OCIPlatform to drop Self typing in favor of concrete class names (converting some classmethods to staticmethods in Architecture).

Reviewed changes

Copilot reviewed 38 out of 38 changed files in this pull request and generated no comments.

Show a summary per file
File Description
pyproject.toml Ban typing.TYPE_CHECKING imports via ruff banned-api rule.
cibuildwheel/main.py Add lazy modules list; move type-only imports into TYPE_CHECKING.
cibuildwheel/architecture.py Lazy imports; replace Self-typed classmethods with staticmethods referencing Architecture directly.
cibuildwheel/audit.py Add __lazy_modules__ list.
cibuildwheel/bashlex_eval.py Add __lazy_modules__ list.
cibuildwheel/ci.py Add __lazy_modules__ list.
cibuildwheel/environment.py Lazy imports; defer typing/collections.abc imports.
cibuildwheel/errors.py Add __lazy_modules__ list.
cibuildwheel/extra.py Lazy imports; defer typing imports.
cibuildwheel/frontend.py Lazy imports; defer typing/Sequence imports.
cibuildwheel/logger.py Lazy imports; switch to TYPE_CHECKING = False.
cibuildwheel/oci_container.py Lazy imports; drop Self return type on OCIPlatform.native.
cibuildwheel/options.py Lazy imports; defer typing imports.
cibuildwheel/platforms/*.py Lazy imports across all platform modules.
cibuildwheel/projectfiles.py Lazy imports; defer pathlib/typing.
cibuildwheel/resources/* Add __lazy_modules__ lists in resource scripts.
cibuildwheel/schema.py Lazy imports.
cibuildwheel/selector.py Lazy imports; replace Self with EnableGroup.
cibuildwheel/util/*.py Lazy imports across util modules.
cibuildwheel/venv.py Lazy imports; defer Sequence import.
unit_test/* Use TYPE_CHECKING = False pattern in tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@henryiii

Copy link
Copy Markdown
Contributor Author

Lazy:

Benchmark 1: .venv/bin/python -m cibuildwheel --help
  Time (mean ± σ):      32.6 ms ±   2.9 ms    [User: 24.9 ms, System: 5.3 ms]
  Range (min … max):    30.3 ms …  55.9 ms    84 runs
Benchmark 1: .venv/bin/python -m cibuildwheel --print-build-identifiers
  Time (mean ± σ):      41.9 ms ±   1.8 ms    [User: 32.7 ms, System: 7.1 ms]
  Range (min … max):    40.1 ms …  55.4 ms    66 runs

Before this PR (still on 3.15):

Benchmark 1: .venv/bin/python -m cibuildwheel --help
  Time (mean ± σ):     118.8 ms ±   0.9 ms    [User: 100.8 ms, System: 15.7 ms]
  Range (min … max):   117.6 ms … 121.0 ms    24 runs
Benchmark 1: .venv/bin/python -m cibuildwheel --print-build-identifiers
  Time (mean ± σ):     120.0 ms ±   0.9 ms    [User: 101.8 ms, System: 16.0 ms]
  Range (min … max):   118.5 ms … 122.7 ms    24 runs

@agriyakhetarpal

Copy link
Copy Markdown
Member

🏎️🏎️🏎️

@joerick

joerick commented May 18, 2026

Copy link
Copy Markdown
Contributor

Hmm. I'm not fully convinced on this...

cibuildwheel isn't really a tool where the speed of startup is a big factor. If this was a low-cost change, maybe it would be worth it, but, Github says this PR is +626 -131.

On top of that, how do we ensure the values of __lazy_modules__ or the if TYPE_CHECKING: stuff doesn't go out of date? It feels to me that if this isn't built into an autoformatter this will almost immediately be wrong - and subtly wrong, in that the code will work fine but the imports will no longer be lazy.

I guess I feel that it's quite a lot of extra lines for fairly negligible user benefit, and more maintainer work to maintain and review these extra annotations.

@henryiii

Copy link
Copy Markdown
Contributor Author

This feels much snappier on the command line when asking for help. And it also means that anything we don't use isn't imported.

The TYPE_CHECKING is guaranteed by the formatter. The __lazy_modules__ comes from (my) flake8-lazy, which I didn't add to pre-commit. I can pull this out into a separate PR, to keep this one focused and smaller.

Another option is:

class AllLazy:
    @staticmethod
    def __contains__(_: str) -> bool:
        return True


__lazy_modules__ = AllLazy()

This makes everything lazy, so if we needed non-lazy imports, that would be a problem, but I don't think we do.

A lot of this extra text will go away once we support 3.15+ only, as then it becomes a lazy keyword.

@henryiii

Copy link
Copy Markdown
Contributor Author

I think we can get away without many of the typing changes. Only TYPE_CHECKING is triggering the import, on 3.14+ annotations are lazy so don't trigger lazy imports in 3.15.

@henryiii

Copy link
Copy Markdown
Contributor Author

With only #2864 and uvx flake8-lazy --apply cibuildwheel/**.py and no other changes, you get 42ms for both --help and --print-build-identifiers. I'd be fine with that - it feels nice and snappy.

Comment thread cibuildwheel/architecture.py Outdated
@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch from 0699040 to bc01bb2 Compare May 19, 2026 03:34
@agriyakhetarpal agriyakhetarpal mentioned this pull request May 19, 2026
@mayeut

mayeut commented May 23, 2026

Copy link
Copy Markdown
Member

If we move to enforcing ruff TC rule as would be done on Python 3.14 by default, this patch would be +332 lines instead of +443 (and faster). Enforcing that rule is done in #2866 and maybe we'd want to merge it even if we're years away from dropping Python 3.13.

@henryiii, are you planning to add some configuration to flake8-lazy ? I think from the readme the answer is no but "like a list of modules that can't be lazy imported" would be nice to reduce the patch here.

It seems python always imports some modules so maybe those could be excluded by default (likely subject to change so maybe not a good idea).

module list
.nox/tests-3-15/bin/python -v -IS nothing.py 2>&1 | grep destroy | sort | grep -v 'destroy _' | grep -v '\._' | sed 's|# destroy ||g'
builtins
codecs
encodings
encodings.aliases
encodings.utf_8
marshal
posix
sys
sys.monitoring
time
zipimport

Running PYTHON_LAZY_IMPORTS=all .nox/tests-3-15/bin/python -v -m cibuildwheel --help 2>&1 | grep destroy gives somewhat the exclusion list that would be useful here. This would allow to reduce patching even more resulting in roughly +200 lines.
It might need to be re-evaluated once in a while though, unless we can give flake8-lazy a command-line to run returning the list of modules to exclude.

module list

raw:

PYTHON_LAZY_IMPORTS=all .nox/tests-3-15/bin/python -v -m cibuildwheel --help 2>&1 | grep destroy | sort | grep -v 'destroy _' | grep -v '\._'  | sed 's|# destroy ||g'
abc
annotationlib
argparse
ast
builtins
bz2
cibuildwheel
cibuildwheel.architecture
cibuildwheel.environment
cibuildwheel.errors
cibuildwheel.frontend
cibuildwheel.oci_container
cibuildwheel.options
cibuildwheel.selector
cibuildwheel.util
cibuildwheel.util.helpers
cibuildwheel.util.packaging
cibuildwheel.util.resources
codecs
collections
compression
compression.zstd
contextlib
copyreg
dataclasses
encodings
encodings.aliases
encodings.utf_8
encodings.utf_8_sig
enum
errno
fcntl
functools
genericpath
gettext
grp
importlib
importlib.util
io
keyword
locale
lzma
marshal
ntpath
operator
os
pathlib
posix
posixpath
pwd
re
reprlib
runpy
shutil
site
stat
sys
sys.monitoring
textwrap
time
tomllib
types
typing
zipimport
zlib

curated:

argparse
cibuildwheel
cibuildwheel.architecture
cibuildwheel.environment
cibuildwheel.errors
cibuildwheel.frontend
cibuildwheel.oci_container
cibuildwheel.options
cibuildwheel.selector
cibuildwheel.util
cibuildwheel.util.helpers
cibuildwheel.util.packaging
cibuildwheel.util.resources
codecs
collections
contextlib
functools
io
os
pathlib
re
runpy
shutil
sys
textwrap
time
tomllib
typing

@henryiii

Copy link
Copy Markdown
Contributor Author

I think from the readme the answer is no

No, the focus is making it work well out of the box, but not against configuration forever. I'd be a lot happier with it if flake8 didn't require a new, specific-to-flake8-only file...

I think ignoring things that get imported anyway is likely fine for a default.

@joerick

joerick commented May 23, 2026

Copy link
Copy Markdown
Contributor

If we can make this tool-assisted, so I can effectively ignore the __lazy_modules__ bit, that would help me. Absolutely once we can use the lazy keyword, we should.

I still feel that the __lazy_modules__ thing is an increase in ugliness that's kinda unfriendly to (human) contributors, that isn't justified by the microbenchmark improvement. But I'm happy to be outvoted if I'm the only one :)

I'm better inclined to the if TYPE_CHECKING: changes, as it also helps prevent circular import problems, and there's no duplication.

@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch from 5af2aa4 to 8dbaa59 Compare May 28, 2026 05:01
Comment thread cibuildwheel/platforms/android.py Outdated
@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch 2 times, most recently from c3b650c to c715167 Compare May 29, 2026 19:06
Comment thread .pre-commit-config.yaml Outdated
mayeut

This comment was marked as outdated.

@henryiii

Copy link
Copy Markdown
Contributor Author

Switched to using auto fixer mode rather than the flake8 runner (0.8 now respects black/ruff style formatting). With henryiii/flake8-lazy#73 we could have config, too.

@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch 2 times, most recently from 899fb7a to 8ff54de Compare June 1, 2026 22:32
henryiii and others added 7 commits June 9, 2026 21:40
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Assisted-by: ClaudeCode:claude-sonnet-4.6
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
Assisted-by: ClaudeCode:claude-sonnet-4-6
@henryiii henryiii force-pushed the henryiii/chore/fasthelp branch from 8ff54de to cd79b86 Compare June 10, 2026 01:42

@mayeut mayeut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm ok with the current state of the PR.

One minor comment:

  • we could leverage flake8-lazy configuration to reduce the " increase in ugliness"

I started to work on something that addresses the comment if there's interest. It comes at the expense of a new nox session to rebuild the config and one ugly long line in the config.

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.

6 participants