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))
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
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.
| 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
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.
Three ways to turn it on
-
The keyword.
lazy import xorlazy from x import y, per import. Explicit and easy to review. -
Globally.
python -X lazy_imports=allorPYTHON_LAZY_IMPORTS=allmakes every import lazy. I checked both withsys.get_lazy_imports(), which returns"all". Good for an experiment in staging, risky as a default on day one. -
Across versions. A module can set
__lazy_modules__ = ["boto3", "sqlalchemy"]and keep plainimportstatements, 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
loggingconfiguration 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 *andlazy 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
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
- Profile startup with
-X importtimeand mark the heaviest third-party importslazyfirst. - Try
PYTHON_LAZY_IMPORTS=allin 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)