October 9, 2026·6 min read·engineeringpythonimportslanguage-design

Python 3.15 makes deferred work and missing values explicit

Python 3.15 adds lazy import syntax, sentinel values and an immutable mapping. Each gives a common convention a runtime implementation with specific limits.

A command-line tool can load much of its dependency graph before it knows which command the user wants. Python 3.15 addresses that problem with explicit lazy imports. The October 9 release also adds built-in sentinels and frozendict. These additions put several common conventions into the language, with defined behavior where handwritten approximations have accumulated limitations.

The import change is especially relevant to my reasons for using Python. Its libraries often supply the hard part of a problem, but obtaining that benefit should not require loading every optional subsystem for every invocation. Explicit control over loading is useful when the dependency graph is larger than the work requested by a particular command.

The release does not make ordinary imports lazy by default. PEP 810 introduces an opt-in form whose different execution timing is visible in source. That matters because importing a module can execute code. Deferring it changes when errors and side effects occur, even if the imported module ultimately provides the same objects.

Lazy imports move execution to first use

The new syntax prefixes an import with the soft keyword lazy. A binding appears in the importing module, but loading and executing the target module waits until the imported name is used. The following example marks the JSON import for deferred execution.

lazy import json

def encode_record(record):
    return json.dumps(record)

The function definition does not itself require using the imported name. Calling encode_record() reaches json.dumps, which requires the module. That distinction lets a program define several operations without immediately paying every operation's import cost. The benefit depends on which operations a particular invocation reaches.

PEP 810 describes the implementation in terms of lazy proxy objects. The initial binding holds a placeholder. First use resolves the import and replaces the binding with the actual object. This avoids changing ordinary dictionary lookup throughout the runtime to support laziness. The design confines special behavior to the imported binding and its resolution.

Laziness is local to the marked import. It does not recursively turn the target module's ordinary imports into lazy imports. Once that module loads, its own eager imports retain their normal behavior. This gives source readers a more bounded question than a global rule that might change every import deep inside a dependency.

The placement restrictions also encode the intended use. PEP 810 allows the syntax at module scope and disallows it inside functions, class bodies, or try blocks. Star imports and future imports cannot use it. A function can already defer an ordinary import by containing the statement, while a try block commonly relies on catching an import error at the statement itself.

The lazy from form binds separate lazy names. Using one name loads the module and resolves that name. Other imported names remain proxies until accessed, although loading the module again is unnecessary. This is more precise than treating every name imported from a module as one inseparable binding.

Import-time behavior remains behavior

Moving module execution changes the moment at which an import error appears. A dependency unavailable on the machine may no longer fail during initial module loading. The failure can arrive when the application first uses the imported binding. Error handling that assumes all import failures occur at startup therefore needs a different understanding of the lazy path.

Side effects also move. A module that registers a handler when imported cannot perform that registration until it executes. Marking the import lazy is appropriate only when that later timing agrees with the application's intended behavior. Syntax makes the decision visible, but it cannot determine whether another subsystem depended on the earlier side effect.

This is why explicit syntax is valuable to me. Python's protocol model already makes much implicit behavior convenient. A change to execution timing deserves an indication at the place that requests it. The new form lets a library author choose individual boundaries without asking consumers to guess how the entire dependency graph behaves.

For code that must still parse on older interpreters, the PEP includes __lazy_modules__ as a transition mechanism. Python 3.15 can use that declaration to identify potentially lazy imports, while older interpreters leave ordinary imports eager. That provides a different compatibility path from introducing new syntax into a file that an older parser must read.

A missing value can have its own identity

PEP 661 addresses another common convention. None often represents an omitted argument, but sometimes None is a valid argument value. The traditional MISSING = object() idiom creates a distinct object, yet its representation and behavior under copying or serialization are poor fits for a public default value.

The proposal's history starts with the representation of defaults in traceback.print_exception(). A raw object representation made the displayed signature long and uninformative. The broader discussion exposed different sentinel implementations across the standard library, with inconsistent typing and copying behavior. Python 3.15 supplies a built-in sentinel type for the cases that need a dedicated missing value.

MISSING = sentinel("MISSING")

def choose_timeout(value=MISSING):
    if value is MISSING:
        return 30
    return value

Here an omitted value selects the example's thirty-second default, while an explicit None remains None. The number is an application policy in the example, not a Python default. Identity comparison expresses which case the caller selected without relying on truthiness.

Each call to sentinel() creates a new object, even if two calls use the same name. A shared sentinel must therefore be created once and reused. The name supplies its representation rather than acting as a process-wide registry key. That keeps creation consistent with the old unique-object idiom while improving the object's public behavior.

Copying or deep-copying a sentinel returns that same object. Pickling preserves identity for sentinels importable by module and name. A local sentinel that cannot be retrieved that way is not picklable. These conditions make the serialization contract specific enough to reason about when a missing value crosses a process boundary.

The typing design also recognizes the named sentinel itself in type expressions. A signature can distinguish an integer from the sentinel, and an identity check can narrow that union. This closes a gap in which the runtime convention was straightforward but its type annotation was much less precise.

An immutable mapping preserves the mapping protocol

PEP 814 adds frozendict, an immutable mapping with familiar dictionary-style construction. It preserves insertion order and implements the mapping protocol. It does not inherit from dict, which avoids exposing dictionary mutation methods through the inheritance relationship.

The immutability is shallow. Freezing a mapping prevents replacing its entries, but it does not freeze objects stored as values. A list inside a frozendict remains a list. This is essential to interpreting both its sharing guarantees and its suitability as a hashable value.

A frozendict is hashable when its keys and values are hashable. That supports using appropriate mappings as dictionary keys or cached-function arguments. A mapping containing a list cannot gain hashability merely because the outer container is immutable. The constraint follows the values that contribute to equality and hashing.

Union creates a new immutable mapping, with the right operand supplying values for overlapping keys. Even |= creates a new mapping and rebinds the variable rather than modifying the original object. Another reference to the original still observes its original entries. This is a useful distinction for code that shares configuration values.

The proposal also separates this type from earlier immutable-mapping designs. Its constructor makes a shallow copy of dictionary contents, and iteration retains insertion order. It does not claim a constant-time conversion that empties a mutable dictionary or supplies a general deep-freeze operation. Those would be additional semantics.

The release also changes an existing default

Python 3.15's UTF-8 default, specified by PEP 686, differs from the opt-in additions. Existing code that omitted a text encoding can change behavior. The proposal retains explicit encoding="locale" for operations that genuinely require locale encoding, as well as a way to disable UTF-8 mode for compatibility.

That gives adoption two different kinds of work. Lazy imports and the new value types can be introduced where their contracts suit the program. The encoding change needs attention wherever an old omitted argument carried an assumption about input data. The release supplies both new vocabulary for explicit decisions and a changed default for existing ones.

The experimental JIT also improves in this release, but these language changes matter independently of a performance result. A missing argument can now have a named identity, a mapping can reject entry mutation, and an import can declare deferred execution. Each has a runtime contract that library authors can use directly, including the limits on what the contract guarantees.