mypyc compiles typed Python to C extensions that import like ordinary modules — until the wheel does not match the runtime, a dynamic pattern escapes compilation, or cold start grows from giant .so loads. Used well, it accelerates hot pure-Python modules without a rewrite. Used carelessly, it breaks imports on one AMI while laptops stay green.
⚡ TL;DR: Compile a small allowlist of typed modules. Measure import time and p99. Publish platform wheels; keep
.pyfallbacks. Avoid compiling glue that uses heavy dynamism. Pair with PyO3 for true native hot loops when mypyc plateaus.
Allowlist modules that are actually hot
# mypyc.ini (illustrative)
[mypy]
python_version = 3.12
strict = True
# Only compile these — not the whole monorepo
# setup.cfg / maturin-style packaging lists:
# mypyc_modules =
# billing.tax
# billing.proration
# billing/tax.py — keep types precise for mypyc
from __future__ import annotations
def tax_cents(amount_cents: int, bps: int) -> int:
# ✅ Simple typed arithmetic compiles well
return (amount_cents * bps) // 10_000
❌ Compiling modules full of getattr, dynamic imports, and ORM magic — you pay compile pain for little gain.
Import-time and correctness gates
# scripts/check_mypyc_import.py
import importlib, os, time
def timed_import(name: str) -> float:
t0 = time.perf_counter()
importlib.import_module(name)
return time.perf_counter() - t0
if __name__ == "__main__":
# ✅ Fail CI if compiled import regresses badly vs baseline artifact
sec = timed_import(os.environ["MODULE"])
print(f"import_s={sec:.4f}")
if sec > float(os.environ.get("MAX_IMPORT_S", "0.25")):
raise SystemExit("import_too_slow")
Run unit tests against both pure-Python and mypyc-built wheels in CI. Prefer identical results over microbenchmark bragging rights.
Packaging reality
| Risk | Control |
|---|---|
| ABI skew (3.11 wheel on 3.12) | Tag wheels; fail deploy on mismatch |
| Missing musllinux wheel | Build both manylinux + musllinux |
| Debug nightmare | Keep uncommitted .py readable; map lines in crashes |
| Oversized extension | Split allowlist; don’t compile tests |
# ✅ Build in CI matching runtime
python -m pip install mypy mypyc build
python setup.py build_ext --inplace # or your maturin/setuptools flow
pytest -q
auditwheel repair dist/*.whl # linux
When to stop and use PyO3 instead
If flamegraphs still show C-level need (tight numeric loops, custom parsing), mypyc may plateau. Escalate that one function to Rust via PyO3 rather than forcing more of the app through mypyc.
Closing checklist
✅ Dos
– ✅ Allowlist a few typed hot modules
– ✅ Dual-test pure Python and compiled wheels
– ✅ Gate import latency in CI
– ✅ Publish wheels for every prod platform
– ✅ Keep readable fallbacks and rollback tags
❌ Don’ts
– ❌ Don’t mypyc the entire monorepo on day one
– ❌ Don’t deploy mismatched Python minor ABIs
– ❌ Don’t expect gains from highly dynamic code
– ❌ Don’t skip musllinux if you run Alpine/ECS scratch
– ❌ Don’t treat compile success as a performance win without metrics
Related reading
- Python PyO3 Extensions: Port Hot Paths Without Full Rewrite Risk
- CPython Specialize Stats: Why Hot Functions Stop Adaptive Instructions
- Node Module Compile Cache: Faster Cold Starts for Heavy CLIs
- Lambda Versioned Layers: Native Node Addons Without ABI Surprises
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
