Measure and enforce import-time behavior in Python projects
Project description
importguard
Measure and enforce import-time behavior in Python projects.
Stop slow imports and sneaky side effects from shipping to production.
The Problem
Python imports can quietly become a performance and reliability trap:
- Slow startup — Your CLI or server cold-start gets slower because importing your package pulls in heavy modules (
pandas,torch,boto3) or runs expensive initialization - Hidden side effects — Importing a module accidentally reads config, makes network calls, hits the filesystem, or initializes logging globally
- Hard to catch in review — These regressions are subtle and often only noticed when users complain
importguard makes these issues measurable and CI-enforceable.
Installation
pip install importguardpy
Quick Start
Check import time for any module
$ importguard check requests
✓ requests imported in 45ms
Top 5 slowest imports:
1. urllib3.util.ssl_ 12ms
2. urllib3.util 8ms
3. requests.adapters 7ms
4. charset_normalizer 6ms
5. requests.models 5ms
Enforce a time budget
$ importguard check mypkg --max-ms 200
✓ mypkg imported in 127ms (budget: 200ms)
$ importguard check mypkg --max-ms 100
✗ FAIL: mypkg imported in 127ms (budget: 100ms)
Ban imports at the top level
$ importguard check mypkg.cli --ban pandas --ban torch
✗ FAIL: mypkg.cli imports banned module: pandas
Get reliable timing with --repeat
Import timing can be noisy. Use --repeat to run multiple times and report the median:
$ importguard check mypkg --max-ms 150 --repeat 5
Running 5 iterations...
✓ mypkg imported in 127ms (median of 5 runs: 118ms, 127ms, 132ms, 124ms, 129ms)
This is especially useful in CI where timing variance can cause flaky failures.
Pin Python interpreter with --python
Useful for testing against specific Python versions or when using pyenv/virtualenvs in CI:
$ importguard check mypkg --python /usr/local/bin/python3.11 --max-ms 200
✓ mypkg imported in 134ms (using Python 3.11.7)
Configuration
Create .importguard.toml in your project root:
[importguard]
# Global budget for the main package
max_total_ms = 200
# Per-module budgets
[importguard.budgets]
"mypkg" = 150
"mypkg.cli" = 100
"mypkg.api" = 80
# Banned top-level imports per module
[importguard.banned]
"mypkg.cli" = ["pandas", "numpy", "torch"]
"mypkg" = ["boto3", "tensorflow"]
Then run:
$ importguard check mypkg --config .importguard.toml
CLI Reference
importguard check <module> [options]
Arguments:
module Python module to check (e.g., mypkg, mypkg.cli)
Options:
--max-ms MS Fail if import exceeds this threshold
--ban MODULE Ban a module from being imported (can repeat)
--config FILE Path to .importguard.toml config file
--top N Show top N slowest imports (default: 10)
--json Output results as JSON (for CI parsing)
--quiet Only output on failure
--fail-on-warning Exit non-zero on warnings, not just errors
--repeat K Run K times in fresh subprocesses, report median (reduces noise)
--python PATH Use specific Python interpreter (e.g., /usr/bin/python3.11)
CI Integration
GitHub Actions
name: Import Guard
on: [push, pull_request]
jobs:
import-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -e .
pip install importguardpy
- name: Check import performance
run: |
importguard check mypkg --config .importguard.toml --fail-on-warning --repeat 3
CI Best Practices:
- Use
--repeat 3or--repeat 5to reduce timing noise and prevent flaky failures - Use
--python $(which python)to explicitly control which interpreter is tested - Each repeat runs in a fresh subprocess to avoid caching effects
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: importguard
name: importguard
entry: importguard check mypkg --max-ms 200
language: system
pass_filenames: false
always_run: true
Why Results Differ on CI?
Import times can vary significantly between your local machine and CI. Here's why and how to handle it:
Common Causes
| Factor | Local | CI | Impact |
|---|---|---|---|
| CPU | Fast desktop/laptop | Shared VM cores | 2-5x slower |
| Disk | SSD/NVMe | Network storage | Variable latency |
| Caching | Warm .pyc files |
Cold start | First run slower |
| Python version | May differ | Matrix testing | Different stdlib |
| Dependencies | Pinned versions | Fresh install | Version variance |
Recommendations
- Use
--repeat 3or--repeat 5— Takes median to smooth out noise - Set CI budgets 2-3x higher than local measurements
- Use separate config sections for CI vs local:
# .importguard.toml
[importguard]
max_total_ms = 200 # Local development
# Override in CI with environment-specific config
# or use: importguard check mypkg --max-ms 500
- Pin Python version in CI to match local:
- uses: actions/setup-python@v5
with:
python-version: '3.11.7' # Exact version
- Warm up before measuring (optional):
- name: Warm up Python cache
run: python -c "import mypkg" || true
- name: Check import performance
run: importguard check mypkg --repeat 3
Debugging CI Failures
# Get detailed JSON output for investigation
importguard check mypkg --json > import-report.json
# Compare top imports between local and CI
importguard check mypkg --top 20
How to Keep Imports Fast
1. Lazy Imports
Move heavy imports inside functions that need them:
# ❌ Bad: imports pandas at module load
import pandas as pd
def process_data(path):
return pd.read_csv(path)
# ✅ Good: imports pandas only when needed
def process_data(path):
import pandas as pd
return pd.read_csv(path)
2. Optional Dependencies
Use try/except for optional heavy dependencies:
# ✅ Good: graceful degradation
try:
import torch
HAS_TORCH = True
except ImportError:
HAS_TORCH = False
def train_model(data):
if not HAS_TORCH:
raise ImportError("torch required: pip install mypkg[ml]")
# ... use torch
3. Deferred Imports with __getattr__
For library authors, use module-level __getattr__:
# mypkg/__init__.py
def __getattr__(name):
if name == "heavy_module":
from . import heavy_module
return heavy_module
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
4. Split Entry Points
Separate your CLI from your library:
mypkg/
├── __init__.py # Lightweight, fast to import
├── core.py # Core functionality
├── cli.py # CLI-only, can import Click/Rich
└── optional/
└── ml.py # Heavy ML dependencies
# pyproject.toml
[project.scripts]
mypkg = "mypkg.cli:main" # CLI imports heavy deps
5. Avoid Import Side Effects
# ❌ Bad: runs on import
config = load_config() # Network call!
logger = setup_logging() # Creates files!
# ✅ Good: explicit initialization
_config = None
def get_config():
global _config
if _config is None:
_config = load_config()
return _config
6. Profile Before Optimizing
Use importguard to find the actual culprits:
# Find the slowest imports
importguard check mypkg --top 20
# Ban known-heavy modules from your fast path
importguard check mypkg.cli --ban pandas --ban torch --ban tensorflow
Python API
from importguard import check_import, ImportResult
# Basic timing
result: ImportResult = check_import("mypkg")
print(f"Import took {result.total_ms:.1f}ms")
# Check against rules
result = check_import(
"mypkg.cli",
max_ms=100,
banned=["pandas", "torch"]
)
if not result.passed:
for violation in result.violations:
print(f"FAIL: {violation}")
# Reduce noise with repeated measurements
result = check_import(
"mypkg",
repeat=5, # Run 5 times, report median
max_ms=150
)
print(f"Median: {result.median_ms:.1f}ms across {result.num_runs} runs")
# Use specific Python interpreter
result = check_import(
"mypkg",
python_path="/usr/local/bin/python3.11"
)
How It Works
importguard uses Python's -X importtime flag to capture detailed timing data for every module imported during the load of your target module. It then:
- Parses the import tree and timing data
- Identifies the slowest imports
- Checks for banned modules in the import chain
- Compares against your configured budgets
- Reports violations with actionable output
All checks run in an isolated subprocess to avoid polluting your current environment.
Use Cases
CLI Startup Time
Keep your CLI snappy. Users notice when mycli --help takes 2 seconds.
Library Hygiene
Ensure import mypkg doesn't force users to install or load optional heavy dependencies.
Serverless Cold Starts
Lambda and Cloud Functions cold starts are dominated by import time. Catch regressions before they hit prod.
Monorepo Guardrails
Prevent accidental cross-module imports that pull in the kitchen sink.
Roadmap
- Import timing measurement
- Time budget enforcement
- Banned import detection
- Baseline comparison (diff vs main branch)
- Side-effect detection (network calls, file writes during import)
- HTML report generation
- VS Code extension
Contributing
Contributions welcome! Please open an issue first to discuss what you'd like to change.
git clone https://github.com/AryanKumar1401/importguard
cd importguard
pip install -e ".[dev]"
pytest
License
MIT
importguard — unit tests, but for import behavior
Project details
Release history Release notifications | RSS feed
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 importguardpy-0.1.0.tar.gz.
File metadata
- Download URL: importguardpy-0.1.0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdd9d8846b22e5e5b129f39f59c74fab9c9795330c0f1467b9c52c57019ee6e3
|
|
| MD5 |
780b2a565be4fba71d0f56dd6aa81b97
|
|
| BLAKE2b-256 |
db97318a09a189a15df56af0ecd56ca48e40e6643af754fb38dab249b1b5c78d
|
Provenance
The following attestation bundles were made for importguardpy-0.1.0.tar.gz:
Publisher:
publish.yml on AryanKumar1401/importguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
importguardpy-0.1.0.tar.gz -
Subject digest:
cdd9d8846b22e5e5b129f39f59c74fab9c9795330c0f1467b9c52c57019ee6e3 - Sigstore transparency entry: 785798355
- Sigstore integration time:
-
Permalink:
AryanKumar1401/importguard@03cd81b1491e6171b5270c7e11b5145c88a563f9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/AryanKumar1401
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@03cd81b1491e6171b5270c7e11b5145c88a563f9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file importguardpy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: importguardpy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49f4e8b9f6ed4c62e4a8ee48c0a62f9e24683705b37e5c38372113847ba45cf7
|
|
| MD5 |
93ac9f357b2459a1f22eecb23bc854c8
|
|
| BLAKE2b-256 |
59118cbf2a0661d300cf85a9e112e55db5b3936f2da480c2702ae7b9936e2df2
|
Provenance
The following attestation bundles were made for importguardpy-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on AryanKumar1401/importguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
importguardpy-0.1.0-py3-none-any.whl -
Subject digest:
49f4e8b9f6ed4c62e4a8ee48c0a62f9e24683705b37e5c38372113847ba45cf7 - Sigstore transparency entry: 785798366
- Sigstore integration time:
-
Permalink:
AryanKumar1401/importguard@03cd81b1491e6171b5270c7e11b5145c88a563f9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/AryanKumar1401
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@03cd81b1491e6171b5270c7e11b5145c88a563f9 -
Trigger Event:
release
-
Statement type: