Python 3.15 New Features Explained: Lazy Imports, Sentinel, and Frozen Dictionaries

Python 3.15 brings pragmatic improvements like Lazy Imports, Sentinel, and Frozen Dictionary, balancing performance and code clarity.
Python 3.15 has officially launched. Though without disruptive language changes, it delivers many solid improvements to everyday development experience. Core features include: Lazy Imports, which allow import statements to remain at the top of the file while deferring actual loading until first use, balancing code cleanliness and startup speed; a new Sentinel type, using named sentinel values to distinguish "missing" from "empty" semantics, superior to None and object() for type hints; unpacking operators in comprehensions to replace nested loops, simplifying list flattening and dictionary merging; and Frozen Dictionary (frozendict), providing an immutable mapping that is thus hashable, solving the long-standing pain point of dictionaries being unusable as cache keys. It also unifies default I/O encoding to UTF-8, improves the experimental JIT and error messages, and adds tools like a statistical sampling profiler. Overall, it's a pragmatic iteration.
Python 3.15 has been officially released. While this version doesn't bring any disruptive language changes, it delivers several solid improvements to everyday developer experience. A YouTube tech channel has put together a systematic overview of this update, covering Lazy Imports, the new Sentinel type, unpacking operators in comprehensions, and Frozen Dictionary features. For developers who rely on Python, even though coding agents can already handle a large volume of routine tasks, understanding the language's own new capabilities remains worthwhile — most models have a knowledge cutoff in their training data, and new version features are precisely the blind spots they haven't yet mastered.
Lazy Imports: Keep imports at the top, push the waiting to runtime
The import time for certain heavy libraries in Python can be considerable. Frameworks like TensorFlow and PyTorch may need 20 seconds or more to complete loading when a script starts. The traditional approach is to tuck import statements inside functions, triggering the import only when the function is actually called. While effective, this approach sacrifices code readability — import statements end up scattered everywhere, and if the same library is used by multiple functions, you have to write it repeatedly.
Python 3.15's Lazy Imports change the game. You can write import statements normally at the top of your file, and simply add the lazy keyword in front — the import behavior will be deferred until the module is first used. In a demonstration, a module simulating a 5-second load time, when imported the traditional way, forces the script to wait a full 5 seconds after startup before the first output appears; with lazy imports, the program immediately prints a greeting and only pays that 5-second wait when the relevant function is actually called.
The value of this feature lies in balancing code cleanliness with runtime efficiency. For command-line tools sensitive to startup speed or services requiring fast responses, lazy imports free developers from having to choose between readability and performance. Note that syntax highlighting support for this syntax is currently incomplete, and editors may not yet recognize it as a keyword.
Lazy Imports tie back to the inner workings of Python's import system itself. When the interpreter executes an import statement, it first checks sys.modules to see if the module is already cached; if not, it locates, loads, compiles, and executes the module code in order, then binds the module object to the namespace. This process is synchronous, and the parsing and initialization of heavy libraries on first import is often where the bulk of the time goes. Traditionally, deferring this overhead into the function body essentially postpones the module binding to a local scope. The lazy keyword sinks this deferral mechanism down into language-level syntax, with the interpreter triggering the actual import only upon first attribute access, while still ensuring the module object remains visible to later references. Therefore, lazy imports do not change import semantics — they only change when the import happens. There are boundaries too: code that depends on import side effects, or situations in type checking or reflection where module attributes need to be accessed early, may behave differently under deferred imports, so caution is warranted.
Sentinel: Giving explicit semantics to "missing" and "empty"
None carries too many meanings in Python: value missing, value not provided, value invalid, deliberately empty — all these different semantics are compressed into the same None. When None appears as a default value in a function signature, callers often can't tell which case it represents.
The Sentinel type introduced in Python 3.15 allows developers to define named sentinel values with clear semantics. For example, you can create MISSING = Sentinel("MISSING") and EMPTY = Sentinel("EMPTY") to represent two different states: "not provided" and "explicitly empty." Compared to the older practice of creating sentinels with object(), the new type performs better in type hint scenarios — using object as a type annotation means accepting any value, while a Sentinel instance provides precise type information and has more readable printed output.
In practice, this feature makes API design clearer. Suppose a Person class constructor accepts name and age parameters; age's default can be set to MISSING, and the type annotation can be written as int | Sentinel, clearly expressing "you can pass an integer here, or not pass anything." Compared to a vague None, this expression reduces the caller's guesswork.
Sentinel values are a common technique in API design for distinguishing "not provided" from "explicitly passed." Before language-level support existed, developers typically used object() instances, module-level constants, or custom classes as sentinels. The core requirement of such approaches is: the sentinel object must not equal any legitimate input value, and ideally only one instance should be created so it can be compared with is. The Sentinel type codifies this convention into the standard library, providing a stable representation and a clear type, enabling it to participate in type checking as part of a union type. It's important to distinguish that a sentinel is not the same as "nullable" or "a default value" — it expresses a state, typically used in conjunction with default parameters in function signatures, so that "not passed" and "passed an empty value" become two paths the program can distinguish. For library authors, this can significantly reduce vague documentation statements like "if None, that means..."
Unpacking operators in comprehensions: reducing nested loops
Flattening nested lists is a common operation in Python, and the classic approach requires two for loops: [x for y in my_list for x in y]. This requires defining the outer variable y first, then referencing it in the inner layer, which feels slightly counterintuitive in ordering.
Python 3.15 allows unpacking operators directly in comprehensions, simplifying to [x for x in my_list] combined with unpacking — producing exactly the same result as double loops but with more compact syntax. The same improvement applies to dictionary merging: previously requiring nested structures like {k: v for d in my_dicts for k, v in d.items()}, you can now unpack a sequence of dictionaries directly to get the merged result. The merge rule follows left-to-right order, with later keys overriding earlier ones of the same name.
Unpacking operators in comprehensions share the same lineage as Python's existing iterable unpacking syntax. *args in function calls, a, *rest = ... in assignments, and [*a, *b] in literals all belong to the same family of features, whose semantics are to expand elements of an iterable in order. Introducing it into comprehensions essentially allows the element source of a comprehension to be directly concatenated from multiple iterables, rather than being expanded layer by layer through multiple for clauses. Compared to nested loops, this style stays closer to the intent of "flattening several sequences into one" and avoids the naming burden of intermediate variables. Note the evaluation order: unpacking proceeds left to right, which in dictionary merging directly determines the direction in which same-named keys are overridden. For readers of the code, the semantics are more intuitive, but you should also confirm it is fully equivalent in ordering to the original nested loop.
Frozen Dictionary: combining immutability with hashability
Frozen Dictionary is one of the most practical features in this update. It is essentially an immutable dictionary — once created, assignment is not supported, and any modification attempt raises an exception.

The immediate benefit of immutability is hashability. Regular dictionaries, being mutable, cannot serve as keys in another dictionary, nor can they be processed by hash(). This problem is particularly prominent in caching scenarios. Imagine a function that calls an external weather API, with request parameters including city, date, time, and more — each request takes 15 seconds and costs 10 cents. If the parameters could be used directly as a cache key, identical requests could return cached results immediately. But a regular dictionary as a key triggers unhashable type: 'dict'.

Frozen Dictionary solves this pain point. After converting the request parameter dictionary to a frozendict, it can be used directly as a cache key. Compared to past workarounds like serializing the dictionary to a string or manually constructing a tuple key, this approach is more natural and less error-prone.
Hashability in Python is tightly bound to "immutability." hash() relies on an object's hash value remaining constant throughout its lifetime, so mutable containers like list, dict, and set do not implement __hash__; only immutable objects like tuple, frozenset, strings, and numbers can serve as dictionary keys or set elements. Frozen Dictionary follows exactly the same logic as frozenset relative to set: providing a mapping type whose content is immutable and therefore safely hashable. Note that hashability requires all its keys and values to be hashable themselves — if a frozendict contains mutable values like lists, construction and hashing will still fail. Moreover, hashability does not equal thread safety, nor does it guarantee deep immutability — nested mutable structures can still be modified, and cache key design needs clear expectations about this.
Other notable minor updates
Beyond the core features above, Python 3.15 also includes several changes worth noting:
- Default encoding unified to UTF-8: All I/O operations default to UTF-8, definitively resolving the previously inconsistent encoding behavior on some systems.
- JIT compiler performance improvements: The experimental just-in-time compilation feature gains significant performance improvements, though still in the experimental stage.
- Further error message improvements: Continuing the trend of recent years, error messages in specific scenarios are more specific and more instructive.
- Statistical sampling profiler: A new statistical analysis tool has been added, suitable for performance profiling scenarios.
- Typed dictionary extra item types: You can now specify types for extra items in a typed dictionary, achieving more complete type checking coverage.
Summary
Python 3.15 positions itself as a pragmatic iteration. Lazy imports and Frozen Dictionary solve long-standing practical pain points, while Sentinel and comprehension unpacking make code expression more precise. For teams heavily reliant on Python, these improvements add up to tangible gains in development experience and runtime efficiency. The full changelog can be found in the official "What's New in Python 3.15" documentation — details not covered in the video can all be found there.
One-line Summary
Python 3.15 brings pragmatic improvements like Lazy Imports, Sentinel, and Frozen Dictionary, balancing performance and code clarity.
Paragraph Summary
Python 3.15 has officially launched. Though without disruptive language changes, it delivers many solid improvements to everyday development experience. Core features include: Lazy Imports, which allow import statements to remain at the top of the file while deferring actual loading until first use, balancing code cleanliness and startup speed; a new Sentinel type, using named sentinel values to distinguish "missing" from "empty" semantics, superior to None and object() for type hints; unpacking operators in comprehensions to replace nested loops, simplifying list flattening and dictionary merging; and Frozen Dictionary (frozendict), providing an immutable mapping that is thus hashable, solving the long-standing pain point of dictionaries being unusable as cache keys. It also unifies default I/O encoding to UTF-8, improves the experimental JIT and error messages, and adds tools like a statistical sampling profiler. Overall, it's a pragmatic iteration.
Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.