Python 3.15: 3 Powerful New Features Every Developer Should Know
Learn Python 3.15 lazy imports, frozendict, and sentinel values with practical, runnable examples and tips for production applications.

On this page
Python 3.15 introduces three features that solve problems developers have worked around for years: lazy imports, immutable dictionaries, and dedicated sentinel values. Let's see what they change, why they matter, and when you should use them.
Prerequisite: Install Python 3.15 or later and verify with
python --version. The examples below require 3.15, unless explicitly stated otherwise.
1. Lazy imports: only load code when you need it (PEP 810)
A normal import loads and executes a module when Python reaches the import statement. That can slow down command-line programs and applications with many optional dependencies.
Before Python 3.15
# eager.py
import json
print("App started")
print(json.loads('{"topic": "python"}'))Python 3.15: explicit lazy imports
# lazy_demo.py
lazy import json
import sys
print("Before use:", "json" in sys.modules)
result = json.loads('{"topic": "python"}')
print("After use:", "json" in sys.modules)
print(result)In a fresh Python process where json hasn't already been imported, the first check is False and the second is True. An imported module may already be in sys.modules because of the interpreter or another dependency, so that check is best treated as a teaching demonstration rather than a universal assertion.
You can also use lazy from pathlib import Path. The module loads when the imported name is first used.
Practical example: an optional reporting command
Create two files in the same directory:
# reports.py
print("Loading reporting tools...")
def generate_report():
return "Report generated!"# app.py (Python 3.15+)
lazy import reports
import sys
command = sys.argv[1] if len(sys.argv) > 1 else "help"
if command == "report":
print(reports.generate_report())
else:
print("Usage: python app.py report")Run python app.py help: the reporting module isn't needed and its top-level print doesn't run. Run python app.py report: Python loads the module and generates the report.
When to use it: optional features, CLI startup, and large dependency trees. Watch out: delayed imports also delay import errors and module side effects. The lazy keyword is allowed only at module scope, not inside functions, classes, or try blocks. It doesn't work with star imports. Existing imports are still eager unless you opt in.
Backward compatibility: For source files that must run on earlier Python versions, you can declare __lazy_modules__ = ["reports"] before a regular import reports. Python 3.15 treats the listed import as lazy; older versions ignore the declaration and import eagerly.
2. frozendict: protect mappings from accidental changes (PEP 814)
A normal dictionary can be changed after creation. That is useful for many tasks, but dangerous for shared configuration.
settings = {"model": "gemini", "temperature": 0.2}
settings["temperature"] = 0.9 # AllowedPython 3.15 adds a built-in immutable mapping called frozendict:
settings = frozendict({
"model": "gemini",
"temperature": 0.2,
})
print(settings["model"])
# settings["temperature"] = 0.9 # TypeErrorPractical example: safe AI app settings
DEFAULT_CONFIG = frozendict({
"model": "gemini",
"max_output_tokens": 1024,
"temperature": 0.2,
})
def describe_config(config):
return f"{config['model']} / {config['temperature']}"
print(describe_config(DEFAULT_CONFIG))
# Create a new mapping when you need a different value:
experiment_config = frozendict({**DEFAULT_CONFIG, "temperature": 0.7})
print(describe_config(experiment_config))
print(DEFAULT_CONFIG["temperature"]) # Still 0.2Important nuance: frozendict is shallowly immutable. If a value is a list, that list can still change:
config = frozendict({"features": ["search"]})
config["features"].append("summarize") # Allowed!If you need deeply immutable configuration, use immutable nested values (for example, tuples and other frozendict instances). A frozendict is hashable only when all its keys and values are hashable. It is not a subclass of dict; accept collections.abc.Mapping when your API supports general mapping objects.
When to use it: fixed configuration, constants, caching keys with hashable contents, and immutable default arguments. It is not a security boundary or automatic deep freeze.
3. sentinel: represent a truly missing value (PEP 661)
What if None is a legitimate input? You need a separate marker meaning “the caller didn't provide a value.”
The ambiguity
def update_profile(nickname=None):
if nickname is None:
print("Was nickname omitted, or explicitly set to None?")Python 3.15: a named sentinel
MISSING = sentinel("MISSING")
def update_profile(nickname=MISSING):
if nickname is MISSING:
return "Keep existing nickname"
if nickname is None:
return "Clear nickname"
return f"Set nickname to {nickname}"
print(update_profile()) # Keep existing nickname
print(update_profile(None)) # Clear nickname
print(update_profile("Sri")) # Set nickname to SriPractical example: partial API updates
UNSET = sentinel("UNSET")
def update_user(user, *, display_name=UNSET):
updated = user.copy()
if display_name is not UNSET:
updated["display_name"] = display_name
return updated
current = {"display_name": "Sri"}
print(update_user(current)) # unchanged
print(update_user(current, display_name=None)) # explicitly cleared
print(update_user(current, display_name="Code With Sri"))When to use it: PATCH-style APIs, optional function arguments, configuration overlays, and data-processing pipelines. Compare with is, not ==. Define the sentinel once at module level and reuse it. Python 3.15's sentinel also has better representation and typing support than a bare object() marker.
Which feature should you use?
| Problem | Python 3.15 solution |
|---|---|
| Optional dependency makes startup slow | lazy import |
| Shared configuration gets accidentally modified | frozendict |
Need to distinguish omitted from None | sentinel |
Quick exercises
- Make a
csvreporting module load only when areportcommand is selected. - Convert an AI model configuration dictionary into a
frozendict, then test whether a nested list is still mutable. - Write
update_settings(timeout=UNSET)so omitted means “leave unchanged” andNonemeans “reset to default.”
Conclusion
Python 3.15 isn't just about speed. These additions make everyday Python code more expressive: load later, protect configuration, and model missing values precisely. Start with a small script, test behavior, and adopt the features where they solve a real problem.
Which feature will you use first—lazy imports, frozendict, or sentinel? Share your answer in the comments!
References
Keep learning with Sri
More practical tutorials and experiments on the channel.