importcost
Measure what your imports cost, defer the safe ones, and stop the slow ones coming back.
Python 3.15 adds lazy import (PEP 810), which defers a
module until something touches it. Deciding what to apply it to is the hard part: you cannot tell
by reading code whether deferring an import saves anything, and you cannot tell whether it breaks
something. importcost answers both by running your code.
Installation
pip install importcost
Requires Python 3.10+. audit and apply need a Python 3.15 interpreter for their runtime pass
and fall back to static analysis below that, with a warning.
Quick start
importcost profile "import mypkg"
import mypkg 418.3 ms of imports across 261 modules (wall 471.2 ms, median of 5)
self ms cumul ms module
61.4 181.9 pandas
38.2 38.2 pandas._libs.tslibs.timestamps
22.7 94.1 requests
19.8 19.8 numpy.core._multiarray_umath
9.1 31.4 rich.console
This is -X importtime with the interpreter's own baseline subtracted, run five times and
median-ed, because a single run of anything on a laptop is noise. --tree shows the nesting,
--json pipes it somewhere.
A target can be an import statement, a module, a script, or a console script:
importcost profile "import pandas"
importcost profile -m mypkg.cli
importcost profile ./scripts/run.py
importcost profile mypy
Deciding what to defer
importcost audit src --target "import mypkg" --test "pytest -q"
saves ms verdict module why
181.9 safe pandas
94.1 safe requests
- no-win rich.console loaded anyway during startup, so deferring it changes nothing
- unsafe mypkg.plugins deferring this breaks the test command
- safe tomllib
418.3 ms of imports today. Deferring the safe ones skips 137 modules, worth about 276.0 ms.
3 imports clear the 0 ms bar.
| Verdict | How it is reached |
|---|---|
safe |
The module stayed unloaded for the whole run and the test command still passed with it deferred |
no-win |
The filter approved the deferral but the module ended up in sys.modules anyway, so something else on the startup path needed it |
unsafe |
The test command passes normally and fails with this module deferred |
not-imported |
Never imported on this path, so there is nothing to win |
The no-win and unsafe rows are the point. Static analysis will happily tell you all five of
those imports can be deferred. Two of them cannot, and you would find out from a production
traceback.
When the whole proposed set fails, importcost bisects to find which modules are responsible, so the report names them rather than making you delete entries one at a time.
Applying it
importcost apply src --target "import mypkg" --min-saving-ms 5 --write
Prints a diff by default. Only touches imports that cleared --min-saving-ms, so you do not end
up with forty lazy keywords buying 3 ms in total.
| Style | Output | Requires |
|---|---|---|
--style lazy |
lazy import pandas |
Python 3.15 |
--style lazy-modules |
A __lazy_modules__ set above the imports |
Nothing; valid syntax on 3.9+, active on 3.15+ |
lazy-modules is PEP 810's own migration shim, for a library that still supports old Pythons. The
set comes out sorted and deduplicated so flake8-lazy stays
quiet about it.
Budgets in CI
Import time rarely regresses from a bad commit. It regresses when a dependency upgrade adds an at-import metadata fetch, and nothing about that shows up in review.
[tool.importcost]
target = "import mypkg"
max_import_ms = 150
max_modules = 200
importcost check # ok import mypkg 118.4 ms, 173 modules
importcost check --update # also writes import.lock
import.lock records exactly which modules get imported. Commit it. After that, a dependency that
starts pulling in something new fails the check with a diff:
fail import mypkg 204.7 ms, 189 modules
import time 204.7 ms is over the 150 ms budget by 54.7 ms
imported modules no longer match import.lock (16 new: cryptography, cryptography.fernet, ...)
+ cryptography, cryptography.fernet, cryptography.hazmat
Times vary by machine; the module set does not, which is why that is the part pinned.
Several entry points with different budgets:
[tool.importcost]
trials = 7
[[tool.importcost.budget]]
target = "import mypkg"
max_import_ms = 150
[[tool.importcost.budget]]
target = "-m mypkg.cli"
max_import_ms = 400
Or as an ordinary test:
def test_import_stays_cheap(import_budget):
import_budget("import mypkg", max_ms=150, max_modules=200)
Configuration
Keys under [tool.importcost] in pyproject.toml.
| Key | Type | Default | Meaning |
|---|---|---|---|
target |
string | none | What to measure, for the single-budget shorthand |
max_import_ms |
number | none | Fail check above this import time |
max_modules |
integer | none | Fail check above this module count |
trials |
integer | 5 |
Runs per measurement, median taken |
python |
string | current | Interpreter to measure with |
lock |
string | "import.lock" |
Path to the lock file |
[[tool.importcost.budget]] |
array of tables | none | Several targets, each with its own limits |
Command reference
| Command | What it does |
|---|---|
importcost profile <target> |
Show where import time goes, as a table or --tree |
importcost audit <paths> --target T |
Give every import a verdict, with --test to prove safety |
importcost apply <paths> --target T |
Rewrite the imports the audit approved, --write to edit |
importcost check |
Enforce budgets and diff against import.lock |
Every command takes --json.
How it compares
| Tool | Measures cost | Finds candidates | Verifies the win | Verifies safety | Applies | CI guard |
|---|---|---|---|---|---|---|
-X importtime |
yes | no | no | no | no | no |
tuna |
yes | no | no | no | no | no |
flake8-lazy |
no | yes | no | no | yes | no |
| importcost | yes | yes | yes | yes | yes | yes |
flake8-lazy is a good linter and worth keeping.
importcost reads and writes the same __lazy_modules__ convention and adds the measurement, the
runtime verification and the CI guard.
Notes
The audit's safety check is only as good as the command given to --test. If your suite does not
touch the code path relying on an import side effect, neither will importcost.
Savings are priced from the eager profile rather than by subtracting the two runs. Verifying a proposal means running the interpreter with a Python-level filter callback on every import, and on a small target that overhead is the same size as the saving, so subtracting gives noise. Counting the modules genuinely skipped, at what they cost when they ran, is stable and closer to what you will see.
Verification is scoped to the file an import came from, not to the module name globally. Writing
lazy import x in one file does not defer x for the rest of the program, and neither does the
check. Otherwise auditing a package would defer the package itself and report that everything got
faster.
Imports inside if TYPE_CHECKING: are skipped since they already cost nothing. Wildcard imports,
__future__ imports and imports inside try/except ImportError are skipped because PEP 810
does not allow deferring them. Names in __all__ are skipped as a conservative default. So is
import a, b, because the statement can only be deferred as a unit but only the first module
would be verified.
Annotations count as import-time uses unless the file has from __future__ import annotations. On
3.14+ with PEP 649 that is stricter than necessary; --assume-lazy-annotations relaxes it.
Module names are worked out by walking up through __init__.py files, which comes out short under
a namespace package. When every candidate returns not-imported but the target demonstrably
imports some of them, importcost says so rather than reporting a confident zero.
No runtime dependencies on 3.11+, and CI runs importcost check on importcost.
Contributing
Bug reports and pull requests are welcome. uv sync then uv run pytest. The runtime tests need
Python 3.15: uv python install 3.15.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file importcost-0.1.1.tar.gz.
File metadata
- Download URL: importcost-0.1.1.tar.gz
- Upload date:
- Size: 88.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63aec1ffd44e81afca2cffb18ac125684bb204db4b357877545e81096d63ea94
|
|
| MD5 |
58bbb9487332823079e01d7595c06980
|
|
| BLAKE2b-256 |
1a4388ee062cd02b7403316f2dce78b0c132e9c1bfc3c7565419060213310b79
|
Provenance
The following attestation bundles were made for importcost-0.1.1.tar.gz:
Publisher:
release.yml on aviseth/importcost
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
importcost-0.1.1.tar.gz -
Subject digest:
63aec1ffd44e81afca2cffb18ac125684bb204db4b357877545e81096d63ea94 - Sigstore transparency entry: 2580020367
- Sigstore integration time:
-
Permalink:
aviseth/importcost@6186319ca4cec80a3cfd8d0a38ff6a86961e2619 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/aviseth
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6186319ca4cec80a3cfd8d0a38ff6a86961e2619 -
Trigger Event:
push
-
Statement type:
File details
Details for the file importcost-0.1.1-py3-none-any.whl.
File metadata
- Download URL: importcost-0.1.1-py3-none-any.whl
- Upload date:
- Size: 39.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96f323dff9fc9007a43caca764fb512fed5fc1de33130118b989e32ace8767c1
|
|
| MD5 |
cd520ab21c3b328669b814678349cb5b
|
|
| BLAKE2b-256 |
30b13715f165e0a251b2e8f5359c5461c2457d98741a10182654c5936895608a
|
Provenance
The following attestation bundles were made for importcost-0.1.1-py3-none-any.whl:
Publisher:
release.yml on aviseth/importcost
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
importcost-0.1.1-py3-none-any.whl -
Subject digest:
96f323dff9fc9007a43caca764fb512fed5fc1de33130118b989e32ace8767c1 - Sigstore transparency entry: 2580020380
- Sigstore integration time:
-
Permalink:
aviseth/importcost@6186319ca4cec80a3cfd8d0a38ff6a86961e2619 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/aviseth
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6186319ca4cec80a3cfd8d0a38ff6a86961e2619 -
Trigger Event:
push
-
Statement type: