> ## Content Index
> Fetch the complete content index at: https://sudosecurity.org/llms.txt
> Use this file to discover other available public pages before exploring further.

# Python 3.15: Lazy Imports, Frozendict, and a Faster JIT
- URL: https://sudosecurity.org/python-3-15-lazy-imports-frozendict-and-a-faster-jit/
- Published: 2026-08-10T23:00:43.000Z
- Updated: 2026-08-10T23:00:43.000Z
- Description: Python 3.15 release candidate: explicit lazy imports, the new frozendict and sentinel types, the Tachyon profiler, and real JIT benchmark numbers.
- Author: Roel
- Tags: Programming, Programming Languages, Python, Software Development, cPython

Import fifteen modules at the top of a script and you pay for all fifteen before your program does anything, whether it ends up using twelve of them or two. That tax has shaped how Python developers write code for years: imports buried inside functions, `importlib` tricks to defer loading, entire style guides built around avoiding a cost that shouldn't exist in the first place. Python 3.15, now at its first release candidate as of August 4, finally removes the tax instead of asking you to work around it.

This isn't a quiet point release padded with deprecation notices. It ships a new immutable dictionary type, a statistical profiler fast enough to run in production, error messages that guess what JavaScript developers meant to type, and a JIT compiler with benchmark numbers attached to it for the first time since the JIT landed in 3.13\. Here's what's actually in it, and what's still missing.

## Lazy Imports Get a Real Keyword

[PEP 810](https://peps.python.org/pep-0810/?ref=sudosecurity.org) gives Python a `lazy` soft keyword that does what a decade of `importlib` workarounds tried to fake. Mark an import lazy, and Python builds a lightweight proxy instead of loading the module immediately. The real load happens on first access:

```python
lazy import json
lazy from pathlib import Path
 
print("Starting up...")  # json and pathlib not loaded yet
 
data = json.loads('{"key": "value"}')  # json loads here
p = Path(".")  # pathlib loads here

```

You get the readability of declaring every import at the top of the file without paying for modules your particular run never touches. If a lazily imported module fails to load, for example because it doesn't exist, Python raises the exception at the point of first use, not at import time, and the traceback names both the failed access and the original import line.

Two escape hatches exist for code you can't or don't want to annotate directly. The `-X lazy_imports` flag and `PYTHON_LAZY_IMPORTS` environment variable flip laziness on globally. For finer control, `sys.set_lazy_imports_filter()` takes a callable that decides module by module:

```python
import sys
 
def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")
 
sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")
 
import myapp.slow_module  # lazy (matches filter)
import json               # eager (does not match filter)

```

The restriction worth knowing: `lazy` only works at module scope. Try it inside a function, a class body, or a `try`/`except` block, and Python raises `SyntaxError`. Star imports and `__future__` imports can't be lazy either.

## A Dictionary You Can Hash

[PEP 814](https://peps.python.org/pep-0814/?ref=sudosecurity.org) adds `frozendict` to the `builtins` module: a dict that refuses modification after creation and, because of that, can serve as a dictionary key or live inside a set. It isn't a `dict` subclass. It inherits from `object` directly.

```python
>>> a = frozendict(x=1, y=2)
>>> a['z'] = 3
Traceback (most recent call last):
TypeError: 'frozendict' object does not support item assignment
>>> b = frozendict(y=2, x=1)
>>> hash(a) == hash(b)
True

```

`copy`, `decimal`, `json`, `marshal`, `pickle`, `pprint`, and `xml.etree.ElementTree` already accept it, and `eval()`/`exec()` will take a `frozendict` as `globals`. If your code currently checks `isinstance(arg, dict)` and needs to recognize the new type too, swap it for `isinstance(arg, (dict, frozendict))`, or reach further and check `isinstance(arg, collections.abc.Mapping)` to catch `MappingProxyType` in the same pass.

## sentinel() Retires a Twenty-Year-Old Hack

Every Python codebase has at least one `_MISSING = object()` line standing in for "no value was passed" in places where `None` is a legitimate answer. [PEP 661](https://peps.python.org/pep-0661/?ref=sudosecurity.org) turns that pattern into a builtin. `sentinel("NAME")` creates a unique object that compares only to itself via `is`, prints something readable instead of `<object object at 0x7f...>`, and can be pickled when it's importable by module and name. Small feature, but it closes a gap nearly every serious Python project has quietly patched over on its own.

## Tachyon: a Profiler for Systems You Can't Afford to Slow Down

[PEP 799](https://peps.python.org/pep-0799/?ref=sudosecurity.org) reorganizes Python's profiling tools under a new `profiling` namespace. The old deterministic tracer, `cProfile`, moves to `profiling.tracing` (with `cProfile` kept as an alias, so nothing breaks). The interesting part is what's new: `profiling.sampling`, a statistical sampling profiler with the internal name Tachyon.

Deterministic profilers instrument every function call, which is precise and also slow enough that you'd never point one at production. Tachyon periodically samples stack traces from a running process instead, at rates up to 1,000,000 Hz, with close to zero overhead. You can attach it to a PID that's already running, run it against a script from the start, point it at a module, or grab a one-shot stack dump of every thread to diagnose something that's hung right now. It profiles wall-clock time, CPU time, GIL-holding time, or exception-handling time depending on what you're chasing, and it exports to flame graphs, Firefox Profiler's Gecko format, or an interactive HTML heatmap down to the line level.

The pitch, in short: the profiler you'd actually reach for when something is slow in a system you can't restart.

## Frame Pointers, On by Default

[PEP 831](https://peps.python.org/pep-0831/?ref=sudosecurity.org) turns on frame pointers by default wherever the platform supports them, using `-fno-omit-frame-pointer` and `-mno-omit-leaf-frame-pointer`. The payoff shows up outside your code entirely: native stack unwinding gets faster and more reliable for system profilers, debuggers, crash analysis, and eBPF-based observability tools. The flags propagate through `sysconfig`, so extension modules built against Python's own build configuration inherit frame pointers automatically. One caveat worth flagging if you maintain a build system: a single native component compiled without frame pointers can break stack unwinding for the entire Python process, so third-party build backends need to preserve these flags rather than silently drop them.

## The JIT Finally Has Numbers Behind It

CPython's JIT debuted in 3.13 without much to show for itself, since the early revisions were about laying groundwork rather than delivering speed. 3.15 changes that, with a new tracing front end, register allocation, better generated machine code, and elimination of reference counts for some object classes.

| Platform      | JIT vs. no-JIT (geometric mean)                 | Best case                            | Worst case                                              |
| ------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------------- |
| x86-64 Linux  | 8-9% faster                                     | Over 100% speedup on some benchmarks | Up to \~15% slower on unpack\_sequence\-style workloads |
| AArch64 macOS | 12-13% faster over the tail-calling interpreter | Over 100% speedup on some benchmarks | Up to \~15% slower on unpack\_sequence\-style workloads |

Those numbers come from the [pyperformance](https://github.com/python/pyperformance?ref=sudosecurity.org) benchmark suite, comparing JIT builds against a standard CPython interpreter built with every other optimization already enabled. The JIT isn't a universal win yet. It's worth testing against your actual workload before flipping it on for anything you run in production, and the core team has attached formal performance guidelines the JIT now has to clear before it graduates from experimental status.

## Better Guesses When You Get the Method Name Wrong

Python's error messages keep getting sharper about what you probably meant. In 3.15, `AttributeError` suggestions now look through nested attributes, not just the object itself:

```python
container.area
# AttributeError: 'Container' object has no attribute 'area'.
# Did you mean '.inner.area' instead of '.area'?

```

When Levenshtein-distance matching comes up empty, the interpreter checks a table of method names common in other languages and translates:

```python
>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
 
>>> 'hello'.toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

```

Call a mutable method on something immutable, and the hint points you at the mutable type instead of just failing:

```python
>>> (1, 2, 3).append(4)
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?

```

`delattr()` gets the same fuzzy-matching treatment for typos on the way out as `getattr()` already had on the way in.

## Unpacking in Comprehensions

[PEP 798](https://peps.python.org/pep-0798/?ref=sudosecurity.org) extends the `*`/`**` unpacking syntax from function calls into comprehensions and generator expressions, replacing the usual `itertools.chain()` detour or an ugly nested comprehension:

```python
>>> lists = [[1, 2], [3, 4], [5]]
>>> [*L for L in lists]
[1, 2, 3, 4, 5]
 
>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
>>> {**d for d in dicts}
{'a': 3, 'b': 2}

```

It works in generator expressions too, including async ones, so `(*a async for a in agen())` flattens exactly the way you'd expect.

## The Incremental Garbage Collector Experiment Ends

Python 3.14.0 through 3.14.4 shipped a new incremental garbage collector aimed at cutting the pause time needed to collect garbage. In production, it did something else: users reported [significant memory pressure](https://github.com/python/cpython/issues/142516?ref=sudosecurity.org), in some cases dramatic. Python reverted to the generational collector from 3.13 starting in 3.14.5, and 3.15 ships with that same reverted collector, not the incremental one. The incremental collector may come back once the memory behavior gets fixed, but it isn't part of this release.

## Smaller Changes Worth Knowing About

- **UTF-8 is now the default encoding, everywhere.** [PEP 686](https://peps.python.org/pep-0686/?ref=sudosecurity.org) means `open('file.txt')` without an explicit `encoding` argument now assumes UTF-8 rather than deferring to whatever the operating system's locale happens to be. You can opt out with `PYTHONUTF8=0` or `-X utf8=0`, but for anyone shipping code that needs to run identically across Windows, Linux, and macOS, this closes a long-running source of subtle bugs.
- **`math.integer` collects the integer-only functions.** `gcd()`, `isqrt()`, `lcm()`, `comb()`, `perm()`, and `factorial()` move to a dedicated `math.integer` module under [PEP 791](https://peps.python.org/pep-0791/?ref=sudosecurity.org). The old `math.gcd()`\-style aliases still work but are soft-deprecated in favor of the new location.
- **The Stable ABI now covers free-threaded builds.** [PEP 803](https://peps.python.org/pep-0803/?ref=sudosecurity.org) introduces `abi3t`, letting C extensions target the Stable ABI while staying compatible with free-threaded ("no-GIL") CPython. Getting there requires real source changes, not a recompile with different headers, and as of this release the major build tools (Setuptools, meson-python, scikit-build-core, Maturin) don't support `abi3t` yet.

## What This Release Doesn't Cover

Python 3.15 doesn't touch the GIL removal timeline directly. Free-threaded builds keep maturing on their own schedule, and this release is about making the ecosystem around them (the Stable ABI work above) more usable, not about changing free-threading's default status. It also doesn't ship any changes to the packaging or dependency-resolution story; `pip`, `uv`, and friends move on their own release cycles. And despite the JIT's new numbers, this release doesn't promise the JIT will make your specific workload faster. The geometric mean hides a real spread, and some benchmarks get measurably slower.