Skip to content

perf: Defer heavy imports on Python 3.15 with lazy imports - #2564

Open
karangattu wants to merge 3 commits into
add-python-3-15-supportfrom
perf-lazy-imports
Open

karangattu wants to merge 3 commits into
add-python-3-15-supportfrom
perf-lazy-imports

Conversation

@karangattu

@karangattu karangattu commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2519 — this PR targets add-python-3-15-support and will retarget to main automatically when #2519 merges.

What

Python 3.15 adoption follow-up from #2519:

  • Lazy imports (PEP 810) on Python 3.15+. __lazy_modules__ declarations defer the modules that dominate import shiny but are only needed for specific features:

    • narwhals and the render modules: shiny.render._data_frame, shiny.render._data_frame_utils._selection, shiny.render._data_frame_utils._types, shiny.render._render
    • shinychat: shiny.ui._chat, shiny.ui._markdown_stream (mirrored in shiny.express.ui)
    • the CLI/server stack (shiny._main): uvicorn, click, questionary, prompt_toolkit, watchfiles

    Every deferred module reifies transparently on first use. __lazy_modules__ is ignored on Python 3.11–3.14, so behavior there is unchanged.

  • Explicit UTF-8 for text file I/O (theme CSS/SCSS, shiny create templates, bundled Agent Skills, pytest generator data). These previously used the locale encoding, so non-ASCII content could mis-handle on Python 3.14 and older (e.g. cp1252 locales on Windows); 3.15 already defaults to UTF-8.

Measured effect

scripts/benchmark_startup.py (included in this PR): median wall-clock of fresh subprocesses, so the numbers include interpreter startup, on a warm filesystem cache — Apple M2 Pro (macOS), shown with min in parentheses.

scenario Python 3.12.15 Python 3.15.0, before Python 3.15.0, this PR
bare interpreter 42 ms 37 ms 37 ms
import shiny 686 ms (666) 308 ms (276) 143 ms (122) — −54%
from shiny import App, ui 692 ms (653) 303 ms (289) 142 ms (129) — −53%
app boot (App(ui.page_fluid(ui.h1("hi")), None)) 769 ms (703) 318 ms (283) 155 ms (132) — −51%

The Python 3.12 column is unaffected by this PR (the declarations are inert before 3.15); it's included for the cross-version comparison. Reproduce with:

uv run python scripts/benchmark_startup.py --runs 12

Notes and limitations

  • Reification moves import errors and import-time side effects for the deferred modules to first use (the traceback still points at both sites, so debugging is unaffected).
  • Deferral only helps code paths that never touch those features; an app that renders data frames or uses chat pays the same cost, just later.
  • Other 3.15-only features (sentinel, frozendict, PEP 798 unpacking in comprehensions, TypeForm) were deliberately not adopted: they would need per-version gating and don't have a current use in this codebase.

App start time (shiny run)

Time from launching python -m shiny run app.py --port N to the first HTTP 200 on /, for a minimal app (ui.input_text + @render.text). Median of 12 runs after 2 warmups, fresh subprocess per run, warm caches, Apple M2 Pro (macOS), same wheel for the two 3.15 environments (3.15 base = add-python-3-15-support tip).

environment min median
Python 3.12.15 321 ms 345 ms
Python 3.15.0, before this PR 325 ms 367 ms
Python 3.15.0 + this PR 232 ms 280 ms

On the full shiny run path, shiny._main (uvicorn/click/questionary) is needed by the CLI itself, so the saving comes from shinychat and the narwhals render stack no longer being loaded before the server is ready: 3.15 alone is roughly even with 3.12 here, while 3.15 + this PR is ~20% faster to first response (min: −28% vs 3.15 base). After import app, none of shinychat, narwhals, questionary, prompt_toolkit, uvicorn, watchfiles, click are in sys.modules on 3.15 + this PR; all of them are on 3.15 base and on 3.12.

`import shiny` currently loads narwhals, shinychat, and the CLI/server
stack (uvicorn, click, questionary, prompt_toolkit, watchfiles) even when
none of it is used. Declare the heavy modules in `__lazy_modules__` so
Python 3.15+ defers them until first use (PEP 810); the declaration is
ignored on Python 3.11-3.14, where behavior is unchanged.

Warm-cache `import shiny` on CPython 3.15.0 drops from ~310ms to ~140ms
(median, scripts/benchmark_startup.py). Reification-on-first-use is
covered by tests/pytest/test_lazy_imports.py (skipped before 3.15).
Pin the encoding for theme CSS/SCSS, `shiny create` templates, bundled
Agent Skills, and pytest generator data. These previously used the locale
encoding, so non-ASCII content could mis-handle on Python 3.14 and older
(e.g. cp1252 locales on Windows); Python 3.15 already defaults to UTF-8.
@karangattu
karangattu added this pull request to stack #2565 October 15, 2026 04:00

This branch has not been deployed

No deployments
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