What's New in Python 3.15
A tour of Python 3.15's headline changes: lazy imports, frozendict, sentinel, UTF-8 by default, unpacking in comprehensions, the profiling package, Tachyon, and more.
Python 3.15 was released on 9 October 2026. It is a “batteries included” release in the best sense: a handful of genuinely new language features, a new immutable mapping type, a new sentinel idiom, UTF-8 everywhere by default, and a serious upgrade to the profiling story. This post walks through the changes that matter most, with runnable examples verified against the CPython 3.15 source tree.
Lazy imports (PEP 810)
Large applications pay for imports they never use. Python 3.15 adds an
explicit lazy soft keyword that defers loading until the name is first
accessed, while keeping the import at the top of the file:
lazy import json
lazy from pathlib import Path
print("Starting up...") # neither module is loaded yet
data = json.loads('{"key": "value"}') # json loads here
p = Path(".") # pathlib loads hereThe lazy keyword works with both import and from ... import.
Failure is deferred too: if a lazily imported module does not exist,
the exception is raised at the point of first use, with a traceback that
points at both the access site and the original import statement.
There are restrictions. Lazy imports are only allowed at module scope:
inside a function, class body, or try/except/finally block they
raise SyntaxError. Star imports and future imports cannot be lazy:
lazy from module import * # SyntaxError
lazy from __future__ import annotations # SyntaxErrorFor code that must stay compatible with older Pythons, a module can
declare __lazy_modules__:
__lazy_modules__ = ["json", "pathlib"]
import json # treated as lazy
import os # still eagerGlobal control is available through -X lazy_imports=all|normal, the
PYTHON_LAZY_IMPORTS environment variable, and
sys.set_lazy_imports() / sys.get_lazy_imports(). A filter callable
can decide per import:
import sys
def myapp_filter(importing, imported, fromlist):
return imported.startswith("myapp.")
sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")The proxy object is exposed as types.LazyImportType.
frozendict (PEP 814)
A new immutable mapping joins the builtins. frozendict is not a
subclass of dict – it inherits directly from object – and is
hashable when its keys and values are hashable:
>>> 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)
>>> a == b
True
>>> hash(a) == hash(b)
TrueComparison is order-independent. copy, decimal, json, marshal,
pickle, pprint, xml.etree.ElementTree and others now accept
frozendict; eval/exec accept it for globals, and type and
str.maketrans accept it for the dict argument.
Code that checks isinstance(arg, dict) should be widened to
isinstance(arg, (dict, frozendict)), or to
isinstance(arg, collections.abc.Mapping).
sentinel (PEP 661)
A concise way to create unique sentinel values:
from builtins import sentinel
MISSING = sentinel("MISSING")Sentinel objects preserve identity under copy.copy, can be used in type
expressions with |, and are picklable when reachable as a module
attribute by name. They give a standard answer to the long-standing
“how do I spell a unique default value” question.
UTF-8 by default (PEP 686)
I/O without an explicit encoding now uses UTF-8 instead of the locale
encoding:
open("flying-circus.txt") # UTF-8, regardless of the system localeFor maximum compatibility across versions, keep passing encoding=
explicitly. The opt-in encoding warning (available since earlier
versions) helps find affected call sites. To restore the old behaviour,
set PYTHONUTF8=0 or pass -X utf8=0.
Unpacking in comprehensions (PEP 798)
List, set and dict comprehensions, plus generator expressions, now
support * and ** unpacking:
lists = [[1, 2], [3, 4], [5]]
[*L for L in lists] # [1, 2, 3, 4, 5]
sets = [{1, 2}, {2, 3}, {3, 4}]
{*s for s in sets} # {1, 2, 3, 4}
dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
{**d for d in dicts} # {"a": 3, "b": 2}This is a direct alternative to nested comprehensions,
itertools.chain and itertools.chain.from_iterable. Async generator
expressions are supported too.
Profiling: the profiling package and Tachyon
Python’s profiling tools moved under a single namespace (PEP 799):
profiling.tracing (the old cProfile) and profiling.sampling
(Tachyon). cProfile remains as a backwards-compatible alias; the
profile module is deprecated and will be removed in 3.17.
Tachyon is a statistical sampling profiler that attaches to a running process by PID without restarting it, with sampling rates up to 1,000,000 Hz. It supports wall-clock, CPU, GIL-holding and exception-handling modes, thread-aware and async-aware profiling, and many output formats: pstats, collapsed stacks, interactive HTML flame graphs, Gecko format, heatmaps, and a live TUI.
python -m profiling.sampling run script.py --flamegraph
python -m profiling.sampling attach 12345 --mode cpu --collapsed
python -m profiling.sampling dump 12345 --async-awareStable ABI for free-threaded builds (PEP 803, 793, 820)
C extensions targeting the stable ABI can now be compiled for
free-threaded builds (abi3t), making a single wheel compatible with
free-threaded CPython. This needs API introduced in earlier versions
(negative basicsize, PyObject_GetTypeData) and the new
PyModExport_* hook plus PySlot. Build tools such as meson-python,
scikit-build-core and Maturin already support abi3t.
Frame pointers by default (PEP 831)
CPython is now built with -fno-omit-frame-pointer and
-mno-omit-leaf-frame-pointer on supporting platforms, giving system
profilers, debuggers and eBPF tools an unbroken native stack. The flags
propagate through sysconfig, so extension modules inherit them.
Package startup files (PEP 829)
.pth files’ import lines are silently deprecated. Python 3.15
introduces .start files containing pkg.mod:callable entry points
that are located and called at startup. When a matching .start file
exists, import lines in .pth files are ignored. site.StartupState
batches startup processing so all path extensions apply before any
startup code runs.
Improved error messages
AttributeError now suggests attributes reachable through a member:
container.area
# AttributeError: 'Container' object has no attribute 'area'.
# Did you mean '.inner.area' instead of '.area'?For builtin types, unknown names are matched against a table of common
methods from other languages and the Python equivalent is suggested
(push -> append, toUpperCase -> upper). delattr failures
get “did you mean” suggestions too.
Other notable changes
bytearray.take_bytes(n=None)moves bytes out of a buffer without copying, useful for protocol parsing and buffering.- New
math.integermodule exposing integer mathematical functions. asyncio.TaskGroup.cancel()for early termination of a task group.sliceis now a generic type;memoryviewsupports complex types.base64gainedpad,padded,wrapcol,ignorecharsandcanonicalparameters across its functions.- The interactive REPL has coloured tab completion and suggests module
attributes in
from ... import. - Per-module import locks are now acquired hierarchically, fixing a
classic
pkg.sub/pkg.sub.moddeadlock.
Porting notes
__cached__is no longer set or consulted by the import system; use__spec__.cached.profileis deprecated; migrate toprofiling.- Relying on the locale default encoding is now a portability hazard:
pass
encoding=explicitly or setPYTHONUTF8=0during migration.
Wrapping up
Python 3.15’s theme is “make the common cases cheaper and the hard cases possible”: deferred imports for startup, an immutable mapping for shared state, a standard sentinel idiom, UTF-8 as the unambiguous default, and a profiler you can attach to a live process. Upgrading is mostly a matter of running your test suite and fixing the handful of encoding and import assumptions the new defaults expose.