Skip to content

Defer more imports for even faster CLI#17

Open
hugovk wants to merge 1 commit intohenryiii:henryiii/chore/fasthelpfrom
hugovk:main
Open

Defer more imports for even faster CLI#17
hugovk wants to merge 1 commit intohenryiii:henryiii/chore/fasthelpfrom
hugovk:main

Conversation

@hugovk
Copy link
Copy Markdown

@hugovk hugovk commented Mar 25, 2026

This builds on and updates pypa#2797.

We can avoid a lot more imports by using TYPE_CHECKING=False instead of importing it from typing, and move lots of other typing imports under if TYPE_CHECKING:. Also __future__ import annotations lets us move many more under this guard.

This PR is 1.4x faster than pypa#2797 and 4x faster than upstream main. Using Python 3.15.0a7 from official macOS installer.

hyperfine --warmup 10 \
     -n "upstream"   --prepare "git checkout upstream/main"           "python3.15 -m cibuildwheel --help" \
     -n "Henry's PR" --prepare "git checkout henryiii/chore/fasthelp" "python3.15 -m cibuildwheel --help" \
     -n "Hugo's PR"  --prepare "git checkout main"                    "python3.15 -m cibuildwheel --help"
Benchmark 1: upstream
  Time (mean ± σ):     178.9 ms ±   5.2 ms    [User: 155.4 ms, System: 21.7 ms]
  Range (min … max):   171.4 ms … 188.1 ms    16 runs

Benchmark 2: Henry's PR
  Time (mean ± σ):      61.7 ms ±   4.9 ms    [User: 50.4 ms, System: 9.7 ms]
  Range (min … max):    56.7 ms …  76.4 ms    33 runs

Benchmark 3: Hugo's PR
  Time (mean ± σ):      44.7 ms ±   1.7 ms    [User: 36.6 ms, System: 7.1 ms]
  Range (min … max):    42.5 ms …  53.8 ms    52 runs

Summary
  Hugo's PR ran
    1.38 ± 0.12 times faster than Henry's PR
    4.01 ± 0.19 times faster than upstream

Summary by Sourcery

Defer type-related imports and enable postponed annotations across the codebase to reduce startup overhead and speed up the CLI.

Enhancements:

  • Use from __future__ import annotations and guard most typing and collections.abc imports with a TYPE_CHECKING flag to avoid importing them at runtime.
  • Trim lazy module dependency lists to exclude typing and collections.abc where no longer needed, reducing unnecessary import work during CLI startup.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 25, 2026

Reviewer's Guide

Optimizes CLI startup by deferring many typing- and collections-related imports to TYPE_CHECKING-only blocks, enabling use of from future import annotations and tightening lazy module lists across the codebase while updating annotations to use native PEP 604/PEP 585 syntax.

Flow diagram for CLI startup with deferred imports and TYPE_CHECKING guards

flowchart TD
    start["Start: python -m cibuildwheel --help"]
    import_main["Import cibuildwheel.__main__"]
    load_lazy_list["Initialize __lazy_modules__ lists in imported modules"]
    exec_main["Parse arguments and prepare to show help"]

    check_type_checking["Evaluate TYPE_CHECKING = False in modules"]
    skip_tc["Skip TYPE_CHECKING blocks (no typing or collections.abc imports)"]

    need_platforms["Access platform specific help and defaults"]
    import_platforms["Import cibuildwheel.platforms and selected platform modules"]

    lazy_not_triggered["Most __lazy_modules__ entries not imported during --help"]
    show_help["Render and print help text"]
    end_node["Exit"]

    start --> import_main --> load_lazy_list --> exec_main
    exec_main --> check_type_checking --> skip_tc
    exec_main --> need_platforms --> import_platforms

    import_platforms --> lazy_not_triggered
    skip_tc --> lazy_not_triggered
    lazy_not_triggered --> show_help --> end_node

    subgraph deferred_imports["Deferred imports"]
        typing_block["typing, collections.abc, Path, Protocol, Self, etc. under if TYPE_CHECKING"]
        heavy_runtime["bashlex, tomllib, dependency_groups, filelock, elftools, etc. via __lazy_modules__"]
    end

    skip_tc --> typing_block
    lazy_not_triggered --> heavy_runtime
Loading

File-Level Changes

Change Details Files
Adopt future.annotations and TYPE_CHECKING-only typing imports to avoid runtime import overhead.
  • Add from future import annotations at the top of most core modules.
  • Introduce a module-level TYPE_CHECKING flag (set to False at runtime) and move typing-heavy imports, protocol types, and typing-only aliases into guarded blocks.
  • Switch annotations from quoted/legacy typing forms to native PEP 585 containers and PEP 604 unions where possible.
cibuildwheel/architecture.py
cibuildwheel/oci_container.py
cibuildwheel/platforms/macos.py
cibuildwheel/selector.py
cibuildwheel/platforms/linux.py
cibuildwheel/platforms/windows.py
cibuildwheel/util/cmd.py
cibuildwheel/logger.py
cibuildwheel/platforms/pyodide.py
cibuildwheel/util/helpers.py
cibuildwheel/platforms/android.py
cibuildwheel/environment.py
cibuildwheel/extra.py
cibuildwheel/projectfiles.py
cibuildwheel/resources/testing_temp_dir_file.py
cibuildwheel/schema.py
cibuildwheel/__main__.py
cibuildwheel/frontend.py
cibuildwheel/util/python_build_standalone.py
cibuildwheel/util/resources.py
cibuildwheel/util/file.py
cibuildwheel/venv.py
cibuildwheel/options.py
cibuildwheel/resources/_cross_venv.py
cibuildwheel/util/packaging.py
unit_test/options_test.py
cibuildwheel/platforms/__init__.py
unit_test/get_platform_test.py
cibuildwheel/platforms/ios.py
unit_test/options_toml_test.py
Tighten and simplify lazy_modules to avoid eagerly importing typing and collections.abc.
  • Remove typing- and collections.abc-related entries from lazy_modules lists where they are only needed for type checking.
  • Drop other modules from lazy_modules when now only referenced in TYPE_CHECKING blocks.
cibuildwheel/architecture.py
cibuildwheel/oci_container.py
cibuildwheel/platforms/macos.py
cibuildwheel/selector.py
cibuildwheel/platforms/linux.py
cibuildwheel/platforms/windows.py
cibuildwheel/util/cmd.py
cibuildwheel/logger.py
cibuildwheel/platforms/pyodide.py
cibuildwheel/util/helpers.py
cibuildwheel/platforms/android.py
cibuildwheel/environment.py
cibuildwheel/extra.py
cibuildwheel/projectfiles.py
cibuildwheel/resources/testing_temp_dir_file.py
cibuildwheel/schema.py
cibuildwheel/__main__.py
cibuildwheel/frontend.py
cibuildwheel/util/python_build_standalone.py
cibuildwheel/util/resources.py
cibuildwheel/util/file.py
cibuildwheel/venv.py
cibuildwheel/options.py
cibuildwheel/util/packaging.py
Adjust runtime imports and type hints to align with the new deferred-import strategy.
  • Remove direct imports of typing.Protocol, Any, Self, IO, Literal, etc. at runtime and import them only in TYPE_CHECKING blocks.
  • Replace uses of cibuildwheel.typing imports at runtime with TYPE_CHECKING-only imports, updating signatures that reference those aliases.
  • Ensure logger, CLI entrypoint, platform modules, and helpers continue to type check correctly while doing less work during CLI startup.
cibuildwheel/oci_container.py
cibuildwheel/logger.py
cibuildwheel/util/helpers.py
cibuildwheel/util/resources.py
cibuildwheel/options.py
cibuildwheel/platforms/android.py
cibuildwheel/platforms/linux.py
cibuildwheel/platforms/windows.py
cibuildwheel/platforms/pyodide.py
cibuildwheel/environment.py
cibuildwheel/extra.py
cibuildwheel/projectfiles.py
cibuildwheel/schema.py
cibuildwheel/__main__.py
cibuildwheel/frontend.py
cibuildwheel/util/python_build_standalone.py
cibuildwheel/util/file.py
cibuildwheel/venv.py
cibuildwheel/resources/_cross_venv.py
cibuildwheel/util/packaging.py
cibuildwheel/platforms/__init__.py
cibuildwheel/platforms/ios.py
unit_test/options_test.py
unit_test/get_platform_test.py
unit_test/options_toml_test.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Defining TYPE_CHECKING = False instead of importing TYPE_CHECKING from typing means type checkers will treat those guarded blocks as regular runtime code (or ignore the guard entirely); consider reverting to from typing import TYPE_CHECKING so tools correctly understand if TYPE_CHECKING: blocks even if that costs a small import.
  • In places where TYPE_CHECKING is used for control flow (e.g. unit_test/get_platform_test.py skipping on non-Windows), replacing typing.TYPE_CHECKING with a local TYPE_CHECKING = False changes the semantics under type checkers; keep using the standard typing.TYPE_CHECKING constant when you need environment-sensitive logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Defining `TYPE_CHECKING = False` instead of importing `TYPE_CHECKING` from `typing` means type checkers will treat those guarded blocks as regular runtime code (or ignore the guard entirely); consider reverting to `from typing import TYPE_CHECKING` so tools correctly understand `if TYPE_CHECKING:` blocks even if that costs a small import.
- In places where `TYPE_CHECKING` is used for control flow (e.g. `unit_test/get_platform_test.py` skipping on non-Windows), replacing `typing.TYPE_CHECKING` with a local `TYPE_CHECKING = False` changes the semantics under type checkers; keep using the standard `typing.TYPE_CHECKING` constant when you need environment-sensitive logic.

## Individual Comments

### Comment 1
<location path="cibuildwheel/architecture.py" line_range="23" />
<code_context>
 from cibuildwheel.util.helpers import strtobool
 from cibuildwheel.util.resources import read_all_configs

+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from collections.abc import Generator, Iterable, Sequence
</code_context>
<issue_to_address>
**issue:** Use `typing.TYPE_CHECKING` instead of a plain `TYPE_CHECKING = False` so type-checkers can see the guarded imports.

Overriding `TYPE_CHECKING` as a plain boolean makes the guarded block invisible to static type checkers as well as at runtime. Most tools special‑case `typing.TYPE_CHECKING` only, so the imports inside these blocks (e.g. `PlatformName`, `Set`, `Literal`, etc.) won’t be recognized and related annotations may degrade to `Any`. This pattern appears in multiple modules in this change. Please restore `from typing import TYPE_CHECKING` and use `if TYPE_CHECKING:` without redefining it so imports remain type‑checker visible but don’t run at runtime.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@henryiii
Copy link
Copy Markdown
Owner

henryiii commented Mar 25, 2026

Lazy imports should work on deferred annotations, though, and those are present in 3.15+ (3.14+). So the TYPE_CHECKING trick is useful, and any time you can move something like TypeVar into a TYPE_CHECKING block should be helpful, but it shouldn't otherwise speed things up (unless deferred annotations are triggering lazy imports, which I haven't checked checked, they do not cause an import)?

@hugovk
Copy link
Copy Markdown
Author

hugovk commented Mar 26, 2026

I think it's actually mostly from the from __future__ import annotations, meaning names used as types are ignored.

Whereas without it they'll be evaluated, triggering more lazy imports to be reified.

@henryiii
Copy link
Copy Markdown
Owner

They are not evaluated in 3.14+, and I checked to make sure they don't trigger a lazy import (they don't). They are a bit slower to set up than just leaving them as strings, though, so it could just be that savings.

@henryiii
Copy link
Copy Markdown
Owner

This is what I tried:

─────┬────────────────────────────────────────────────────────
     │ File: docs/examples/__init__.py   <EMPTY>
─────┴────────────────────────────────────────────────────────
─────┬────────────────────────────────────────────────────────
     │ File: docs/examples/__main__.py
─────┼────────────────────────────────────────────────────────
   1 │ from .b import g
   2 │
   3 │ print(g())
─────┴────────────────────────────────────────────────────────
─────┬────────────────────────────────────────────────────────
     │ File: docs/examples/a.py
─────┼────────────────────────────────────────────────────────
   1 │ __lazy_modules__ = ["pathlib"]
   2 │
   3 │ import sys
   4 │ from pathlib import Path
   5 │
   6 │ def f() -> Path:
   7 │     return Path()
   8 │
   9 │ assert "pathlib" not in sys.modules
─────┴────────────────────────────────────────────────────────
─────┬────────────────────────────────────────────────────────
     │ File: docs/examples/b.py
─────┼────────────────────────────────────────────────────────
   1 │ __lazy_modules__ = [f"{__spec__.parent}.a"]
   2 │
   3 │ import sys
   4 │
   5 │ from .a import f
   6 │
   7 │ def g() -> str:
   8 │     return str(f())
   9 │
  10 │ assert f"{__spec__.parent}.a" not in sys.modules
─────┴────────────────────────────────────────────────────────

The pathlib one wouldn't pass if they got triggered.

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.

2 participants