Python 3.15 is officially here.
Released on October 9, 2026, Python 3.15 introduces several changes that affect how you import modules, handle text, work with data structures, write comprehensions, debug errors, and profile application performance.
Some of these updates change how you write Python code. Others improve the tools you use to build and maintain applications.
Whether you're learning Python, maintaining an existing codebase, or developing production services, understanding these changes can help you decide which features are worth adopting for you.
Here are six Python 3.15 features you should know about, along with some practical examples and considerations for upgrading.
1. Lazy Imports: Load Modules When You Need Them
When Python imports a module, it needs to locate the module, load it, and execute its top-level code. In an application with a large dependency tree, the work can add up even when some modules aren't needed for the operation you're performing.
Python 3.15 introduces lazy imports through the lazy keyword; it allows you to defer loading an imported module until its name is first used.
Example:
lazy import json
lazy from pathlib import Path
print("Application starting...")
data = json.loads('{"name": "Python"}')
location = Path(".")
Unlike an ordinary import, a lazy import moves the actual loading of the module until the imported name is accessed.
This can be very useful in command-line applications, developer tools, and larger applications with optional functionality. If a particular execution path never uses an imported module, you may avoid loading it altogether.
However, lazy imports are not a general performance fix.
They can change when import-related errors occur and when module initialization code runs. You should also measure startup performance rather than assuming that every application will become faster.
When should you use lazy imports?
When an application has expensive dependencies that aren't needed immediately, they are worth investigating when startup time matters more than the simplicity of eager imports.
One of the benefits is that you can defer unnecessary import work without scattering conditional import statements throughout your code.
2. UTF-8 Is Now Python's Default Encoding
Text encoding is easy to overlook until an application reads a file incorrectly or displays unexpected characters.
Python's default text encoding could depend on the operating system and environment, which created potential inconsistencies when developers open files without explicitly specifying an encoding.
Python 3.15 makes UTF-8 the default encoding, following the change introduced by PEP 686.
Example:
# Python 3.15 uses UTF-8 by default when
# no encoding is specified.
with open("message.txt", "r") as file:
message = file.read()
print(message)
Python 3.15 uses UTF-8 by default when no encoding is specified. Explicitly setting encoding="utf-8" remains a good practice when you want the expected encoding to be clear in your code. It documents your assumptions and helps other developers understand how the data should be interpreted.
This is relevant when working with:
- Text files containing international characters.
- CSV files and exported reports.
- Files exchanged between different operating systems.
- User-generated content.
- Applications that process text from external services.
UTF-8 provides a consistent default, but it doesn't automatically detect or convert files stored in other encodings. If you receive legacy data encoded in a different format, you still need to handle it appropriately.
What does this mean for existing Python applications?
Most applications that already use UTF-8 explicitly should need little or no adjustment, but applications handling legacy text files may require testing.
The practical benefit is greater consistency across development environments and deployment systems.
3. Meet frozendict: A Built-In Immutable Dictionary
Python dictionaries are useful because they let you store and retrieve key-value pairs efficiently. Ordinary dictionaries are mutable. You can change their contents after creating them.
Consider this configuration:
settings = {
"theme": "dark",
"language": "en",
}
settings["theme"] = "light"
print(settings["theme"])
The output is:
light
Sometimes, you want a mapping that can't be modified after creation.
Python 3.15 introduces frozendict, a built-in immutable mapping type. It provides a way to represent key-value data whose mapping contents shouldn't change.
Example:
settings = frozendict({
"theme": "dark",
"language": "en",
})
print(settings["theme"])
Output:
dark
Attempting to assign a new value raises a TypeError:
settings["theme"] = "light"
This is useful when you want to make the immutability of a mapping explicit rather than relying on developers to avoid modifying an ordinary dictionary.
There is an important distinction, though: an immutable mapping doesn't automatically make its values immutable. If a value references a mutable list or another mutable object, that object may still be modified.
Python 3.15 gives you a built-in option for immutable mappings, reducing the need to immediately reach for a third-party library or a custom workaround.
4. Comprehensions Can Now Unpack Iterables
Python comprehensions provide a concise way to create lists, sets, and dictionaries from existing data.
Python 3.15 introduces unpacking support in comprehensions through PEP 798. This lets you use * and ** to unpack iterables and mappings directly into comprehension expressions.
Unpacking Iterables in List Comprehensions
Suppose you have a list of lists:
groups = [
[1, 2],
[3, 4],
[5],
]
Previously, you could flatten the nested lists with a nested comprehension:
numbers = [
number
for group in groups
for number in group
]
print(numbers)
Output:
[1, 2, 3, 4, 5]
Python 3.15 adds another way to express this transformation:
numbers = [*group for group in groups]
For a reliable working example of unpacking in a comprehension, consult the official Python 3.15 documentation.
Unpacking Mappings in Dictionary Comprehensions
Mapping unpacking can also help when combining dictionaries.
Consider these records:
records = [
{"name": "Ada"},
{"language": "Python"},
{"name": "Grace"},
]
combined = {**record for record in records}
print(combined)
Python 3.15's unpacking support makes it possible to express mapping combination within a comprehension.
When merging dictionaries, remember that duplicate keys need a defined resolution rule. As with ordinary dictionary construction, later values can replace earlier ones.
The goal isn't to make every comprehension shorter. It's to give you another way to express data transformations without introducing unnecessary intermediate structures.
When should you use unpacking in comprehensions?
Use it when the transformation becomes clearer with unpacking. If a nested comprehension or an explicit loop is easier to understand, keep it.
Readable Python is more useful than compact Python that requires the next developer to stop and decode the syntax.
5. Better Error Messages Make Debugging Easier
Debugging isn't always about finding a complicated algorithmic problem.
Sometimes, the issue is simply that you called a method that doesn't exist or accessed an attribute on the wrong object.
Python 3.15 continues to improve error messages, including suggestions that can help developers identify likely mistakes.
Example:
numbers = [1, 2, 3]
numbers.push(4)
Developers familiar with JavaScript might instinctively use push() to add an item to a list.
Python lists use append() instead.
Python can provide a helpful suggestion along these lines:
AttributeError: 'list' object has no attribute 'push'.
Did you mean: '.append'?
The precise wording of an error message can vary, but the idea is straightforward: the interpreter can sometimes suggest a likely alternative instead of leaving you to investigate the mistake from scratch.
These improvements can be useful when you're learning Python, working with unfamiliar APIs, or debugging code written by someone else.
They can also help experienced developers catch simple mistakes more quickly.
However, error suggestions aren't guaranteed to identify the correct solution. The interpreter can offer a likely match, but you still need to understand what the code is supposed to do.
Why does this matter?
Clearer diagnostics reduce the effort required to interpret common programming mistakes. That can make the development experience smoother, especially when you're working with unfamiliar methods or attributes.
6. Python's Profiling Tools Are Getting an Upgrade
When a Python application becomes slow, the first instinct might be to optimise a loop or rewrite a function.
Which isn't always the right approach. Before changing your code, you need to understand where the application spends its time. Profiling tools help you investigate that question.
Python 3.15 introduces profiling.tracing for deterministic function-call tracing and profiling.sampling for statistical sampling. The existing cProfile module remains available as an alias for backwards compatibility.
The update gives developers additional ways to investigate application performance.
Should You Upgrade to Python 3.15?
New features are useful, but upgrading an existing application requires more than just reading the release notes.
Before moving a project to Python 3.15, consider the following steps.
1. Check your dependencies.
Confirm that your frameworks, libraries, and native extensions support Python 3.15. Pay particular attention to packages that depend on compiled extensions or interpreter-specific behaviour.
2. Run your automated tests.
Test your application under the new interpreter before changing your production environment. Include integration tests for database access, file handling, and external services.
3. Review encoding assumptions.
If your application relies on platform-specific default encodings, check whether the UTF-8 default changes its behaviour and test against representative files, including any legacy data.
4. Measure startup and runtime performance.
If you're interested in lazy imports, compare startup times before and after the change. If you're investigating a slow application, use profiling tools to establish a baseline.
5. Check deployment compatibility.
Confirm that your hosting environment, container images, build tools, and deployment pipeline can support Python 3.15.
6. Introduce new syntax carefully.
Features such as unpacking in comprehensions can make code more concise, but new syntax may affect compatibility with older Python versions. Check the minimum interpreter version supported by your project.
For new projects, Python 3.15 may be worth evaluating early. For established applications, a controlled migration with adequate testing is the safer approach.
Conclusion
Python 3.15 introduces changes that affect how developers write, debug, and maintain Python applications.
Lazy imports provide another way to manage startup work. UTF-8 becomes the default encoding, and frozendict adds a built-in immutable mapping type. Comprehensions gain unpacking support, error messages become more helpful, and the profiling toolkit expands.
These features won't eliminate the need for good engineering practices. They give you additional options for solving familiar problems.
The best way to approach a new Python release isn't to adopt every feature immediately but to try to understand what changed, identify the features relevant to your work, and test them in the context of your applications.
Further reading
- What's New in Python 3.15
- PEP 810 — Explicit Lazy Imports
- PEP 814 — Add frozendict Built-in Type
- PEP 798 — Unpacking in Comprehensions
- PEP 799 — A Dedicated Profiling Package
This article is part of the ZEEKWRITES Python series, exploring Python, APIs, AI-assisted development, and the engineering ideas behind modern software.
Top comments (0)