Free Python reference · JavaScript timeline
Python 3.15: New ways to defer work and model values
Explicit lazy imports, comprehension unpacking, sentinel values, immutable mappings, and UTF-8 defaults expand the language toolkit.
Released · Editorial review October 25, 2026
Introduced in Python 3.15 · Syntax
Explicit lazy imports
The lazy soft keyword marks an import whose module loading is deferred until the imported binding is first used. It lets a module declare dependencies in one place while avoiding work for dependencies that are never needed on a particular path.
This changes when import side effects and failures occur. An unavailable module can raise its error at first use rather than at the declaration. Keep eager imports when initialization order or early failure is important, and measure startup before claiming an improvement.
The example prints a startup message before using JSON. It demonstrates the syntax and ordinary result, without claiming a timing difference. Import mode and environment settings can affect loading behavior.
Earlier approach
def parse_payload(text): import json return json.loads(text)
Context fragment; read it with the example below.
Worked example
Minimum Python 3.15 · Run locally
lazy import json print("before use") payload = json.loads('{"count": 4}') print(payload["count"])
Expected output
before use
4
Example checked with CPython 3.15.0.
Common mistake
A lazy dependency still has to be installed if execution uses it. Delayed loading does not remove import side effects or make import errors disappear.
Try it yourself
Move the JSON use into a function that is never called. Explain what work can now be avoided and where an import failure would surface.
Read more: Python 3.15 release notes · PEP 810
Introduced in Python 3.15 · Syntax
Unpacking inside comprehensions
Unpacking can appear in a comprehension's output position. An unpacked iterable contributes its elements to a list or set, and an unpacked mapping contributes entries to a dictionary.
Use this for one-level flattening or combining a sequence of mapping fragments. It is not recursive flattening: an element that is itself a collection remains an element unless another operation expands it.
In dictionary construction, later entries for the same key win. That conflict policy should be intentional when data comes from different sources. The older nested-comprehension spelling is still useful and supports earlier Python versions.
Earlier approach
flattened = [item for group in groups for item in group]
Context fragment; read it with the example below.
Worked example
Minimum Python 3.15 · Run locally
groups = [[1, 2], [], [3]] print([*group for group in groups]) fragments = [{"a": 1}, {"a": 2, "b": 3}] print({**fragment for fragment in fragments})
Expected output
[1, 2, 3]
{'a': 2, 'b': 3}
Example checked with CPython 3.15.0.
Common mistake
An unpacked object must have the appropriate iterable or mapping behavior. Dictionary collisions are resolved rather than reported as errors.
Try it yourself
Add [[4, 5]] as another group and predict the remaining nested value. Then write an equivalent pre-3.15 comprehension.
Read more: Python 3.15 release notes · PEP 798
Introduced in Python 3.15 · Built-in type
A sentinel distinguishes missing from None
The built-in sentinel creates a unique marker value with a readable name. A marker is useful when None is a legitimate user value and therefore cannot also mean “argument omitted.”
Compare a marker with is to ask about identity. Do not replace this with a truthiness test: None, zero, and empty text may all be valid supplied values with distinct meanings.
The example distinguishes an omitted argument from an explicitly supplied None. Create the sentinel once and reuse that object. Naming two separately created markers the same way is not a reason to treat them as the same marker.
Earlier approach
MISSING = object() # A unique marker, but with a less descriptive repr.
Context fragment; read it with the example below.
Worked example
Minimum Python 3.15 · Run locally
MISSING = sentinel("MISSING") def describe(value=MISSING): if value is MISSING: return "not supplied" return repr(value) print(describe()) print(describe(None)) print(describe(0))
Expected output
not supplied
None
0
Example checked with CPython 3.15.0.
Common mistake
A fresh sentinel inside each comparison will not be the marker originally stored as the default. Reuse the named marker and test identity.
Try it yourself
Call describe with an empty string. Explain why using if not value would collapse cases the contract keeps separate.
Read more: Python 3.15 release notes · PEP 661
Introduced in Python 3.15 · Built-in type
An immutable mapping with frozendict
frozendict creates a mapping whose entries cannot be added, removed, or rebound after construction. It is a distinct built-in type rather than a dict subclass. Lookup still works using keys, making it useful for fixed configuration snapshots.
Immutability is shallow. A list stored as a value can still be changed, and the mapping is hashable only when its keys and values are hashable. Use immutable nested values if you want to use the mapping as a dictionary key or set member.
The example checks a fixed scalar value, catches a rejected update, and compares two mappings with different insertion orders. Equality compares contents rather than insertion order.
Worked example
Minimum Python 3.15 · Run locally
settings = frozendict({"mode": "safe", "retries": 2}) print(settings["mode"]) try: settings["retries"] = 3 except TypeError: print("entries cannot be rebound") print(settings == frozendict({"retries": 2, "mode": "safe"}))
Expected output
safe
entries cannot be rebound
True
Example checked with CPython 3.15.0.
Common mistake
An immutable outer mapping does not freeze lists or dictionaries stored inside it. Do not assume every frozendict is hashable.
Try it yourself
Create a frozendict with a list value. Predict what happens when you append to the list and when you try to hash the mapping.
Read more: Python 3.15 release notes · PEP 814
Introduced in Python 3.15 · Language behavior
UTF-8 becomes the default text encoding
Python 3.15 enables UTF-8 mode by default. Text operations that previously relied on a locale-dependent default therefore become more consistent across environments. This matters when moving scripts between machines with different locale settings.
A default does not identify the encoding of an arbitrary existing file. Pass an explicit encoding when reading a known legacy format or writing a file with an external contract. UTF-8 mode can also be disabled through startup configuration; inspect the environment when diagnosing a difference.
The example performs an explicit in-memory UTF-8 round trip. It is portable and avoids files. This verifies the byte representation, not every platform's standard-stream configuration.
Worked example
Minimum Python 3.15 · Run locally
import sys print("UTF-8 mode:", bool(sys.flags.utf8_mode)) text = "café" encoded = text.encode("utf-8") print(encoded.hex()) print(encoded.decode("utf-8"))
Expected output
UTF-8 mode: True
636166c3a9
café
UTF-8 mode is enabled in the tested default CPython 3.15 invocation. It can be disabled at startup. The explicit encode/decode round trip is portable and predates this release; file and terminal defaults remain environment-sensitive.
Example checked with CPython 3.15.0.
Common mistake
Do not assume an old Windows-encoded file becomes UTF-8 because Python has changed its default. The file bytes still need the correct decoder.
Try it yourself
Explain why reading legacy bytes with the new default might fail. Which encoding would you specify if the external format requires Latin-1?
Read more: Python 3.15 release notes · PEP 686
Recap
Explicit lazy imports, comprehension unpacking, sentinel values, immutable mappings, and UTF-8 defaults expand the language toolkit. Prefer the clearest form that works on your project's supported Python versions.
Read the full Python 3.15 release notes →