Skip to main content

importcost

Measure what your imports cost, defer the safe ones, and stop the slow ones coming back.

PyPI Python CI License

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

importcost-0.1.1.tar.gz (88.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

importcost-0.1.1-py3-none-any.whl (39.7 kB view details)

Uploaded Python 3

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

Hashes for importcost-0.1.1.tar.gz
Algorithm Hash digest
SHA256 63aec1ffd44e81afca2cffb18ac125684bb204db4b357877545e81096d63ea94
MD5 58bbb9487332823079e01d7595c06980
BLAKE2b-256 1a4388ee062cd02b7403316f2dce78b0c132e9c1bfc3c7565419060213310b79

See more details on using hashes here.

Provenance

The following attestation bundles were made for importcost-0.1.1.tar.gz:

Publisher: release.yml on aviseth/importcost

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for importcost-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 96f323dff9fc9007a43caca764fb512fed5fc1de33130118b989e32ace8767c1
MD5 cd520ab21c3b328669b814678349cb5b
BLAKE2b-256 30b13715f165e0a251b2e8f5359c5461c2457d98741a10182654c5936895608a

See more details on using hashes here.

Provenance

The following attestation bundles were made for importcost-0.1.1-py3-none-any.whl:

Publisher: release.yml on aviseth/importcost

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page