Python 3.15 Practical New Features: Lazy Imports, Frozen Dicts, and a Sampling Profiler

Python 3.15 delivers pragmatic improvements: lazy imports, frozen dict, sentinel values, and a low-overhead sampling profiler.
Python 3.15 focuses on pragmatic improvements: the `lazy` import keyword enables top-level dependency declarations with on-demand loading; built-in `frozen dict` fills a nearly two-decade gap in immutable mappings; a standard `sentinel` type formalizes the "no value provided" pattern; the new `profiling` package introduces the low-overhead Tachyon sampling profiler with attach-to-process support; and UTF-8 becomes the default encoding, eliminating platform inconsistencies. On the type system front, TypedDict gains extra-item type constraints and `TypeForm` helps library authors describe APIs more precisely. The experimental JIT and free threading also continue to mature.
Python 3.15 brings a set of pragmatic improvements. There's no single earth-shattering change, but several features will meaningfully affect day-to-day development — from lazy imports that reduce startup overhead, to the long-awaited immutable dictionary, to a built-in standard sentinel value. This article walks through the new capabilities you'll actually use, along with the design trade-offs behind them.
Lazy Imports
Large Python applications often spend a noticeable amount of time at startup loading modules, many of which aren't needed for any given execution. The traditional workaround was to move expensive imports inside functions — for example, import pandas inside a data analysis function. This defers loading, but at the cost of scattering dependency declarations throughout the code, making it impossible to tell at a glance what a script actually needs.
Python 3.15 resolves this tension with the new lazy keyword. You simply write lazy import pandas at the top of the file — dependencies remain clearly visible, but Python only actually loads the module the first time it's used.
There's an important trade-off to understand: with lazy imports, any import errors or side effects that would normally surface at startup are instead deferred until first use. That's why this isn't the default behavior — but at least the option now exists.
Frozen Dict
Python has always had mutable lists paired with read-only tuples, and mutable sets paired with read-only frozensets — yet dictionaries never had a "frozen" counterpart. Python 3.15 finally closes that gap with the built-in frozen dict immutable mapping.

It's built directly into the language with no import required. You construct a frozen dict just like a regular dictionary, but any attempt to modify it raises a TypeError. When both keys and values are hashable, the frozen dict itself is hashable too, making it usable as a cache key or even as a configuration key in another dictionary.
One important detail: frozen dict is not a subclass of dict. If your code needs to check whether an object supports mapping behavior, use Mapping from collections.abc rather than checking for the concrete dict type.
Before this, Python developers who needed an immutable mapping typically had to wrap a regular dictionary with types.MappingProxyType to get a read-only view — but that proxy is neither hashable nor directly serializable, making for a rough experience. The third-party frozendict library filled this gap, but required an extra dependency. The built-in frozenset has existed since Python 2.4, yet dictionaries had no equivalent — an asymmetry that frustrated the community for nearly two decades. With frozen dict in the standard library, patterns like configuration objects, function default arguments, and functools.lru_cache keys now have a natural, dependency-free expression.
Standard Sentinel Values
When you need to express "this argument was not provided," None isn't always sufficient — None itself might be a valid value. Developers have traditionally created their own missing = object() as a sentinel, but object wasn't designed for this purpose, carries a lot of irrelevant information, and is awkward to use.

Python 3.15 introduces a dedicated built-in sentinel type. It has a readable string representation, preserves identity through copying, supports pickling when defined at module scope, and can be used in type expressions. This gives APIs a standard, readable way to express "no value was provided here." When checking against a sentinel, always use is rather than ==.
Unpacking in Comprehensions
This is a small but elegant improvement. You can now use unpacking syntax directly inside a list comprehension to flatten a nested list — handling a "list of lists" in a single line. It works not just for list comprehensions but for dictionary comprehensions as well. While type checkers like Pylance currently flag it as unsupported, it runs correctly in Python 3.15. The more concise expression is a genuine quality-of-life improvement over previous approaches.
The New profiling Package and Sampling Profiler
Python 3.15 adds a profiling package that organizes the built-in performance analysis tools under one roof. profiling.tracing is the new home for deterministic call tracing (cProfile is retained for compatibility), but the more interesting addition is profiling.sampling, also known as Tachyon.

Traditional profilers record every call and return, providing extremely detailed information but adding overhead to every single call. A sampling profiler works differently — rather than tracking continuously, it periodically captures call stack snapshots, keeping overhead very low. Tachyon can profile a script or attach to an already-running Python process via its process ID.
Running it on macOS requires sudo for elevated permissions. When the script you're profiling repeats the same operation thousands of times, sampling is entirely sufficient — you don't need data on every individual call. It supports CPU sampling, can focus more narrowly on exceptions, and produces rich profiling output. Because it requires OS-level permissions, it's best suited as an occasional observability tool.
The fundamental difference between a sampling profiler and a tracing profiler comes down to the observation model. A tracing profiler hooks into every function call and return event, yielding precise call counts and cumulative timing — but this "always-on surveillance" slows the program down, sometimes by several times. A sampling profiler instead acts like a periodic shutter, capturing the current call stack at fixed intervals (e.g., once per millisecond) and estimating hot spots by counting how often each function appears. Overhead is typically just a few percent. In production environments or long-running services, a sampling profiler is almost always the only viable option, since it doesn't materially change program behavior. Tachyon's ability to attach to a running process is particularly valuable — it means you can diagnose a process handling real traffic in real time, with no restart and no code changes required.
UTF-8 as the Default Encoding
Python 3.15 defaults to UTF-8 when no encoding is specified, and no longer depends on the system locale. This means code that reads or writes files no longer needs to explicitly include encoding='utf-8'. Before 3.15, such code could behave differently across machines because the default encoding was locale-dependent. Standardizing on UTF-8 eliminates yet another source of platform-specific surprises.
That said, the author recommends still writing encoding='utf-8' explicitly for file formats that have a well-defined encoding — doing so documents your assumption in the code, and explicit is always better than implicit.
Type System Enhancements
TypedDict now supports declaring a type for "extra items." For example, an API response class might have a status field typed as an integer, with all additional keys constrained to string values. A type checker will then flag any attempt to add a boolean as an extra item. This is very useful for scenarios where you need to constrain the types of dynamic keys.

Also new is TypeForm, aimed primarily at library authors — it comes into play when an API needs to accept type expressions such as list[int] or str | None. Most application developers will never need it, but libraries dealing with validation, serialization, or dependency injection can use it to describe their APIs more precisely.
JIT, Free Threading, and Other Improvements
The experimental JIT compiler isn't new, but it received significant improvements in 3.15. It now follows actual execution paths through a new tracing front-end, understands more operations, and incorporates optimizations like basic register allocation. It's worth noting that speedups are highly workload- and build-specific — this shouldn't be read as "Python is 10% faster." The encouraging news is that the JIT is becoming capable of optimizing a broader range of everyday Python code.
Free threading continues to advance, primarily affecting extension authors. A new stable API target called ABI3T has been added to support GIL-free C extensions, lowering the barrier for extensions to support GIL-free Python.
Other miscellaneous improvements include friendlier error messages for common mistakes shared across languages, and frame pointers enabled by default on supported platforms, improving profiling, debugging, and crash analysis. These aren't major updates, but they're the kind of detail that makes Python more polished over time.
Free threading refers to the ability to remove the GIL (Global Interpreter Lock) and allow multiple threads to truly execute Python bytecode in parallel — an experimental feature introduced as an opt-in build since Python 3.13. The GIL has long been a core bottleneck for multi-threaded parallel computation in Python: even on multi-core machines, threads constrained by the GIL can only execute Python code one at a time. Removing it is hugely significant for CPU-bound multi-threaded pure Python workloads, but it means significant adaptation work for C extensions — shared state that previously relied on the GIL for implicit protection must now be guarded with explicit locks. The addition of the ABI3T stable API target is specifically designed to let extension authors support both GIL and GIL-free builds with lower maintenance overhead, and is a critical step in moving the ecosystem toward free threading.
Conclusion
Overall, Python 3.15 is a solid, well-considered improvement over the previous release — no dramatic upheaval, but genuine care at every turn. If you had to pick the two most practically useful additions, frozen dict and lazy imports are probably the features that will benefit everyday development the most. Which one are you most looking forward to?
Related articles

Apple HomeView Hands-On Review: Is the $300 Smart Home Hub Worth It?
Apple HomeView hands-on: a $300 smart home hub with facial recognition for quick account switching, but no App Store or voice recognition. Is the wall-mounted version worth it? Should you buy the first generation?

Meta Muse Wonder Launch: What It Means for Meta's Return to the Frontier AI Race
Meta releases Muse Wonder and WonderFull, benchmarks reach the frontier, community feedback is polarized. An in-depth look at Meta's return to the frontier AI race, its personal agent strategy, and Zuckerberg's different path.

Apple Home Smart Home Lineup Leaked: LG and Schneider Team Up, UWB Door Lock on Board
Apple Home ecosystem gains new LG and Schneider devices: UWB smart lock with proximity unlock, 2.5K dual-camera doorbell, privacy indoor camera, and AirPods-like seamless pairing.