
Python 3.15.0rc1 dropped on August 4th, and it brings three features the community has been waiting years for: lazy imports that slash startup times by up to 70%, a built-in frozendict that finally retires a 10-million-downloads-per-week third-party package, and a new high-frequency sampling profiler that ships with the stdlib. The final release is October 1. You can test all of this today.
Lazy Imports: 70% Startup Time Reductions, With Caveats
PEP 810 lands the lazy soft keyword, which defers module loading until the imported name is first accessed. For any Python application with a slow cold start—CLIs, large web frameworks, data science notebooks—this is the most impactful change in this release.
# Before: json loads at import time, even if --help exits in 10ms
import json
import numpy as np
from pathlib import Path
# After: modules load only when first used
lazy import json
lazy import numpy as np
lazy from pathlib import Path
Under the hood, lazy import json creates a types.LazyImportType proxy. The module goes into sys.lazy_modules rather than sys.modules, and the actual load happens on first attribute access. From that point on, it behaves identically to a regular import.
Real-world numbers back the hype. Meta and Hudson River Trading both reported 50–70% startup time reductions and 30–40% memory savings on large codebases during the beta cycle. For a CLI tool that imports a dozen heavy modules but exits quickly on --help, this is transformative.
Now for the gotchas, because there are real ones. lazy is module scope only—you’ll get a SyntaxError if you try to use it inside a function, class body, or try/except block. Star imports (lazy from x import *) are not allowed. Neither are future imports.
The trickier footgun: anything that relies on module-level side effects at import time will break. Plugin systems that self-register by running code at the top level of a module are the most common case. If your plugin discovery loop imports a module and immediately queries a registry that the module populates at import, you’ll get nothing—the registry runs on first attribute access, not when lazy import is called.
Tooling support is still catching up. mypy has an open issue for PEP 810 support and currently reports lazy imports as syntax errors. isort similarly needs updates. If you’re relying on those tools, test carefully before adopting lazy imports broadly. For backwards compatibility with older Python versions, use the __lazy_modules__ module-level variable to list modules that should be lazily loaded—it’s a no-op on Python < 3.15.
frozendict: The End of a Decade-Long Workaround
PEP 814 adds frozendict as a built-in type. The third-party frozendict package has pulled north of 10 million downloads per week for years because Python had no stdlib answer for an immutable, hashable dictionary. It does now.
from functools import lru_cache
# Immutable config that can't be accidentally mutated
config = frozendict(host="db.prod", port=5432, ssl=True)
# It's hashable—use it as a dict key or lru_cache argument
@lru_cache(maxsize=128)
def get_connection(db_config: frozendict):
return connect(**db_config)
conn = get_connection(config) # works
# Mutation raises TypeError
config["host"] = "staging" # TypeError: 'frozendict' object does not support item assignment
The hashability is the killer feature. Regular dicts can’t be dict keys or set members. types.MappingProxyType was the previous workaround, but it wasn’t a true dict-like type and felt like a hack. frozendict inherits from object directly, is hashable when its contents are hashable, and ignores insertion order during comparison.
Common use cases: configuration objects passed between threads (no locks needed), function default arguments that shouldn’t share state across calls, and anywhere you want to return a mapping from a function and enforce that callers don’t modify it. The third-party frozendict package didn’t go anywhere, but for new code there’s no longer a reason to reach for it.
Tachyon: A Sampling Profiler That Doesn’t Require a Separate Install
The new profiling.sampling module ships a high-frequency statistical profiler running at up to 1,000,000 Hz. No pip install, no sudo for most platforms, no separate tool to remember.
# Profile a script, get an interactive HTML flame graph
python3.15 -m profiling.sampling run --flamegraph output.html myscript.py
# Attach to a running process
python3.15 -m profiling.sampling attach 12345
# Profile CPU time only (not wall time)
python3.15 -m profiling.sampling run --mode cpu myscript.py
Modes include wall time, CPU time, GIL contention, and exception-handling overhead. Output formats include collapsed stacks, interactive HTML via D3.js, and Firefox Profiler format. There’s also a programmatic API via from profiling.sampling import Sampler for embedding profiling in test suites or benchmarks. This won’t replace dedicated tools like py-spy for production tracing, but it removes the barrier for developers who just need to understand why a script is slow without installing anything extra.
JIT and Free-Threaded ABI: The Slower Story
The JIT compiler delivers 8–9% geometric mean speedup on x86-64 Linux and 12–13% on AArch64 (Apple Silicon), up from Python 3.14’s baseline. Real but incremental. PEP 803 (abi3t) introduces a stable ABI for free-threaded builds, meaning C extension authors can ship a single wheel that works across free-threaded Python versions. Mostly invisible to pure Python developers, but essential for the extension ecosystem to converge on free-threaded support.
Test It Today
RC1 is feature-complete—only bug fixes between now and October 1. It’s safe to run against your test suite.
pyenv update
pyenv install 3.15.0rc1
python3.15 -m venv venv315
source venv315/bin/activate
pip install -r requirements.txt
python -X importtime -c "import your_main_module"
Use -X importtime to see which modules are taking the most time before switching them to lazy imports. RC2 is scheduled for September 1. The full Python 3.15 changelog has everything else that landed.













