Skip to main content

importcost

Find out which imports are actually costing you startup time, defer the ones that can be deferred, and stop the slow ones from coming back.

Python 3.15 adds lazy import (PEP 810). That's the easy part. The hard part is knowing which of your imports are worth deferring, which ones get loaded a millisecond later anyway, and which ones quietly break something because they had a side effect you forgot about. importcost answers all three by running your code, not by reading it.

pip install importcost

Where is my startup time going

$ 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

--tree gives you the nesting if you need to know who pulled in what. --json if you want to pipe it somewhere.

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.

Which imports should be lazy

$ 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; something depends on
                                   its import side effect
       -  safe     tomllib

418.3 ms of imports today. Deferring the safe ones skips 137 module(s), worth about 276.0 ms
at what they cost now.
3 import(s) clear the 0 ms bar.

The no-win and unsafe rows are the whole point. Static analysis will happily tell you all five of those imports can be deferred. Two of them can't, and you'd only find out from a production traceback.

Here's how each verdict is reached:

  • safe: the module stayed unloaded for the entire run, and the test command still passed with it deferred. The saving is what the modules it avoided actually cost in the ordinary profile, so it's measured rather than guessed at.
  • no-win: sys.set_lazy_imports_filter approved the deferral, but the module ended up in sys.modules before the process exited. Something else on the startup path needed it. You'd be adding a keyword for nothing.
  • unsafe: the test command passes normally and fails with this module deferred. When the whole set fails, importcost bisects to find which modules are responsible rather than making you delete entries one at a time.

The runtime checks need a 3.15 interpreter. uv python install 3.15 and pass --python. Without one you get static analysis only, and it says so.

Make the change

$ importcost apply src --target "import mypkg" --min-saving-ms 5 --write
updated src/mypkg/io.py: pandas
updated src/mypkg/http.py: requests

Prints a diff by default; --write edits in place. Only touches imports that cleared --min-saving-ms, so you don't end up with forty lazy keywords that buy you 3 ms total.

Two output styles:

--style lazy writes lazy import pandas. Needs 3.15.

--style lazy-modules writes a __lazy_modules__ set above the imports and leaves the import statements alone. That's PEP 810's own migration shim: it's ordinary syntax on 3.9, and it only does anything on 3.15+. Use it for a library that still supports old Pythons. The set comes out sorted and deduplicated so flake8-lazy doesn't complain about it.

Keep it from creeping back

The reason import time regresses isn't usually a bad commit. It's a dependency upgrade that adds an at-import metadata fetch, or a new logging integration that costs 50 ms on load. Nothing in that shows up in code 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 an import.lock next to your pyproject.toml recording exactly which modules get imported. Commit it. After that, a dependency that starts pulling in something new fails the check with a diff:

$ importcost check
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.hazmat, ... +13 more). Run 'importcost check --update' if this is intended.
     + cryptography, cryptography.fernet, cryptography.hazmat

Times vary by machine, so only the numeric budgets are machine-dependent; the module set isn't, which is why that's the part that gets 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

In GitHub Actions:

- run: pip install importcost
- run: importcost check

Or as a normal test, if you'd rather keep it with everything else:

def test_import_stays_cheap(import_budget):
    import_budget("import mypkg", max_ms=150, max_modules=200)

How this relates to the other tools

flake8-lazy is a linter and a good one. It finds imports that are unused at module scope and writes __lazy_modules__ for them. It doesn't measure anything or run your code, which its author is upfront about. Keep using it. importcost reads and writes the same __lazy_modules__ convention, and adds the measurement, the runtime verification, and the CI guard.

-X importtime and tuna show you where the time goes and leave the rest to you.

Notes and caveats

profile and check work on 3.10+. audit and apply need a 3.15 interpreter for the runtime pass; below that they fall back to static analysis and warn.

The audit's safety check is only as good as the command you give --test. If your test suite doesn't touch the code path that relies 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 single import, and that overhead is about the same size as the saving on a small target, so subtracting the two just gives you noise. Counting the modules that were genuinely skipped, at what they cost when they ran, is both stable and closer to what you'll see after the change lands.

Verification is scoped to the file the import came from, not to the module name globally. Writing lazy import x in one file doesn't defer x for the rest of the program, and neither does the check. Otherwise auditing a package would defer the package itself and cheerfully report that everything got faster.

Imports inside if TYPE_CHECKING: are skipped: they already cost nothing. Wildcard imports, __future__ imports, and imports inside try/except ImportError are skipped because PEP 810 doesn't allow deferring them. Names in __all__ are skipped as a conservative default.

Annotations count as import-time uses unless the file has from __future__ import annotations. On 3.14+ with PEP 649 that's stricter than it needs to be; pass the flag if it's costing you candidates.

importcost has no runtime dependencies on 3.11+ and enforces its own import budget in CI. A startup-time tool that takes 200 ms to start isn't a good look.

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.0.tar.gz (88.0 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.0-py3-none-any.whl (39.4 kB view details)

Uploaded Python 3

File details

Details for the file importcost-0.1.0.tar.gz.

File metadata

  • Download URL: importcost-0.1.0.tar.gz
  • Upload date:
  • Size: 88.0 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.0.tar.gz
Algorithm Hash digest
SHA256 1622ccb29f6bb5a87721cab8c3729cc310c8f6ed30e1c57843da83de4acf1c83
MD5 15c4b4d496531d72cf4af2f10e02a558
BLAKE2b-256 5422c376423dce426ab73de8bab3fc297acd54f24bf95b232c5e693de8288feb

See more details on using hashes here.

Provenance

The following attestation bundles were made for importcost-0.1.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: importcost-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 39.4 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a06c085276c16c95f146941ca436e78e7b9fc5c74ca68260dc76133dbda644fe
MD5 5561746e65f5c4431bdadca031f147a3
BLAKE2b-256 f1588c2f9e060a062834659a7c6e25332688a439a03fbcff15f321a1e8d7f8eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for importcost-0.1.0-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

0.1.1

2 files

This release

0.1.0 This release

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