DEV Community

Cover image for Python 3.15 lazy imports took my cold start from 512 ms to 32 ms
Neeraj Sharma
Neeraj Sharma

Posted on AI-assisted

Python 3.15 lazy imports took my cold start from 512 ms to 32 ms

Python 3.15.0 came out on 9 October. The release notes list a dozen headline changes, but for anyone running Python behind an HTTP endpoint, a CLI or a serverless function, one matters more than the rest: PEP 810, explicit lazy imports.

There were no 3.15 builds available through my usual tooling yet, so I built it from the python.org source tarball (SHA-256 checked against the release page) and measured what lazy imports do to a realistic handler.

The test

A small handler of the kind every backend has: it imports boto3, httpx, Jinja2, Rich and SQLAlchemy at the top, but the health check path only needs json.

import json
import sys
import boto3
import httpx
from jinja2 import Environment
from rich.console import Console
from sqlalchemy import create_engine


def handler(event):
    if event["path"] == "/health":
        return {"status": 200, "body": json.dumps({"ok": True})}
    s3 = boto3.client("s3", region_name="us-east-1")
    return {"status": 200, "body": s3.meta.region_name}


if __name__ == "__main__":
    print(handler({"path": sys.argv[1]}), len(sys.modules))
Enter fullscreen mode Exit fullscreen mode

The lazy version changes five lines and nothing else:

lazy import boto3
lazy import httpx
lazy from jinja2 import Environment
lazy from rich.console import Console
lazy from sqlalchemy import create_engine
Enter fullscreen mode Exit fullscreen mode

I ran each case 25 times as a fresh process on a 2 vCPU Linux container, with the bytecode cache already warm, and took the median.

Process start to response, median of 25 runs on Python 3.15.0

Case Median Modules loaded
Eager imports, /health 512 ms 608
lazy keyword, /health 32 ms 57
-X lazy_imports=all, /health 23 ms 53
Eager imports, /s3 614 ms 613
lazy keyword, /s3 361 ms 404

The health check went from 512 ms to 32 ms, 16 times faster, because Python no longer loads 551 modules it never uses on that path.

Where the time was going

python -X importtime shows the culprits. boto3 alone takes about 210 ms cumulative to import, and SQLAlchemy about 155 ms, before your code runs a single line.

python -X importtime handler.py /health 2> imports.log
sort -t'|' -k2 -n -r imports.log | head
Enter fullscreen mode Exit fullscreen mode

The /s3 row shows the limit. That path really does use boto3, so it still pays for boto3. It dropped from 614 ms to 361 ms only because httpx, Jinja2, Rich and SQLAlchemy stayed unloaded. Lazy imports move the cost to the first use. They do not delete it.

Modules in sys.modules after the request

Three ways to turn it on

  1. The keyword. lazy import x or lazy from x import y, per import. Explicit and easy to review.
  2. Globally. python -X lazy_imports=all or PYTHON_LAZY_IMPORTS=all makes every import lazy. I checked both with sys.get_lazy_imports(), which returns "all". Good for an experiment in staging, risky as a default on day one.
  3. Across versions. A module can set __lazy_modules__ = ["boto3", "sqlalchemy"] and keep plain import statements, so the same file still runs on 3.14.

For a middle ground, combine lazy_imports=all with sys.set_lazy_imports_filter(). The filter is only consulted for imports that would be lazy, and returning False keeps a module eager, so you can make everything lazy except the modules whose import side effects you rely on.

What will bite you

  • Errors move. A missing module now raises at first use, not at startup. A typo in a rarely used code path can hide until production hits that path. Keep an import smoke test in CI.
  • Import side effects stop running. Plugin registration, model registries, monkeypatching libraries and logging configuration that happens at import time will not happen until something touches the module. Keep those imports eager.
  • Not everything can be lazy. lazy from x import * and lazy from __future__ import ... are syntax errors.

The other change you will hit: UTF-8 by default

PEP 686 makes UTF-8 the default encoding for open() and friends when no encoding is given, whatever the system locale says. That changes behaviour on machines whose locale was not UTF-8, which mostly means Windows. Find the affected calls before upgrading:

python -X warn_default_encoding -m pytest
# EncodingWarning: 'encoding' argument not specified
Enter fullscreen mode Exit fullscreen mode

If you need the old behaviour while you fix things, -X utf8=0 restores the locale encoding. With LC_ALL=C and locale coercion off, that gave me ASCII again, which is exactly the kind of difference that corrupts a file quietly.

Upgrade checklist

  • Add 3.15 to the CI matrix and run the suite with -X warn_default_encoding.
  • Check binary wheels exist for your compiled dependencies. When I checked on 11 October, pydantic-core and psycopg-binary already had cp315 wheels on PyPI, and numpy and orjson did not:
pip download --no-deps --only-binary=:all: --python-version 3.15 \
  --platform manylinux_2_28_x86_64 --implementation cp numpy -d /tmp/whl
Enter fullscreen mode Exit fullscreen mode
  • Profile startup with -X importtime and mark the heaviest third-party imports lazy first.
  • Try PYTHON_LAZY_IMPORTS=all in staging and watch for side-effect breakage before making anything global.

Does it save money?

A little, and latency matters more. AWS Lambda has billed the init phase since August 2025, so a slow import is now billed time as well as a slow first response. But the arithmetic is modest: one million cold starts a month, 0.48 seconds saved each, at 1 GB of memory, is 480,000 GB-seconds, about 8 dollars at the x86 rate of 0.0000166667 per GB-second. The real win is a first request that answers in tens of milliseconds instead of half a second, for users and for health checks with tight timeouts.

This is the kind of change I look for when I review a Python backend's cloud cost and latency at Axionry. If you are starting a new AI product build, 3.15 is worth targeting from day one.

Sources: Python 3.15.0 release, What's new in Python 3.15, PEP 810, PEP 686, AWS: Lambda standardizes billing for the INIT phase, AWS Lambda pricing. Benchmarks run on a 2 vCPU Linux container, 11 October 2026. I used an AI assistant while drafting this.

Top comments (0)