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 →