Key points
- Python 3.15.0 was released on 9 October 2026 and the current patch release is 3.15.2. Version 3.15 will be supported until October 2031.
- Since 1 October 2026 Python 3.10 gets no security fixes. Applications on that version should be moved first, and 3.11 is supported only until October 2027.
- The most interesting additions are lazy imports (
lazy import), the built-infrozendictandsentineltypes,*and**unpacking in comprehensions and the Tachyon sampling profiler. - The biggest behaviour change:
open()without anencodingargument always uses UTF-8. On Windows servers this can change how files with national characters are read. - We recommend 3.15 for production only once every library in the application supports it. Django supports Python 3.15 from version 6.1.3, while Django 5.2 LTS stops at 3.14.
Python versions and how long they are supported
A new Python version comes out every October and is supported for 5 years. From 3.13 onwards, each version gets full bug fixes for the first 2 years and then security fixes only (previously the full bug-fix period lasted 18 months). This is the situation in November 2026:
| Version | Released | Status | End of support |
|---|---|---|---|
| 3.10 | October 2021 | unsupported | 1 October 2026 |
| 3.11 | October 2022 | security fixes only | October 2027 |
| 3.12 | October 2023 | security fixes only | October 2028 |
| 3.13 | October 2024 | security fixes only | October 2029 |
| 3.14 | October 2025 | full support | October 2030 |
| 3.15 | October 2026 | full support | October 2031 |
Dates from the Python Developer's Guide (devguide.python.org) and the release schedule PEPs.
Since 1 October 2026 this version gets no security fixes, and 3.10.22 is its final release. Libraries are dropping it too: Django 6.0 and later require Python 3.12. It is usually more sensible to move straight to 3.13 or 3.14 than to upgrade in two steps.
Lazy imports: faster application start-up
Large applications start slowly because at start-up Python loads every module imported at the top of a file, including the ones nobody will use in that run. Until now the workaround was importing inside functions, which made code harder to read. Python 3.15 adds the lazy soft keyword (PEP 810): the module is loaded only when the name is first used.
lazy import json
lazy from pathlib import Path
print("Starting up...") # json and pathlib are not loaded yet
data = json.loads('{"key": "value"}') # json is loaded here
p = Path(".") # and pathlib here
If a module cannot be loaded, the error is raised at first use, and the traceback shows both that place and the import line. lazy only works at module level: inside a function, a class or a try block it raises SyntaxError, and so does lazy from module import *.
Lazy imports can also be switched on for a whole application without code changes, with the -X lazy_imports=all option or the PYTHON_LAZY_IMPORTS environment variable. sys.set_lazy_imports_filter() limits this to selected modules, for example only your own, leaving third-party libraries alone. Code that must also run on older Python versions can list modules in __lazy_modules__ instead of using the keyword:
__lazy_modules__ = ["json", "pathlib"]
import json # lazy on 3.15, a normal import on older versions
import os # always loaded immediately
Where it pays off
- Command-line tools and scripts that use only part of their features in each run.
- Tests, scheduled jobs and management commands that start many times a day.
- Serverless functions, where cold-start time matters.
In application servers that run for weeks the gain is smaller, because code is loaded once. The documentation gives no figures, so measure your own application's start-up before and after the change, for example with python -X importtime.
frozendict and sentinel: new built-in types
frozendict (PEP 814) is an immutable dictionary. Nothing can be added to it after it is created. If its keys and values are hashable, it is hashable too, so it can be a key in another dictionary or in a cache. It preserves insertion order, but order does not matter when comparing.
>>> a = frozendict(x=1, y=2)
>>> a == frozendict(y=2, x=1)
True
>>> a["z"] = 3
TypeError: 'frozendict' object does not support item assignment
One thing to watch: frozendict is not a subclass of dict. Code that checks isinstance(x, dict) will not recognise it. The documentation recommends isinstance(x, (dict, frozendict)) or checking against collections.abc.Mapping. The json, pickle and copy modules support the new type.
sentinel (PEP 661) creates a unique marker value, for example "this argument was not passed", that can be told apart from None. It keeps its identity when copied, can be pickled and can be used in type annotations:
MISSING = sentinel("MISSING")
def next_value(default: int | MISSING = MISSING):
if default is MISSING:
...
Unpacking in comprehensions
Flattening a list of lists or merging several dictionaries used to need a double loop inside a comprehension or itertools.chain. From 3.15 (PEP 798), list, set and dict comprehensions and generator expressions accept * and **:
>>> lists = [[1, 2], [3, 4], [5]]
>>> [*L for L in lists]
[1, 2, 3, 4, 5]
>>> dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
>>> {**d for d in dicts}
{'a': 3, 'b': 2}
It is a small change, but code that processes data (query results, API responses, configuration) becomes shorter and easier to read. With dictionaries, as in a regular {**a, **b}, a later value overwrites an earlier one.
The Tachyon profiler: measuring performance without a restart
Python 3.15 gathers its profiling tools in the profiling package (PEP 799). profiling.tracing is the existing cProfile (the old name still works), and the new profiling.sampling, called Tachyon, is a sampling profiler. Instead of tracing every function call, which slows a program down, it records the call stack at regular intervals.
For application maintenance the key point is that Tachyon attaches to a running process by its PID, with no code changes and no restart. When a production system slows down, you can see which function it spends its time in, instead of trying to reproduce the problem on a test server.
# attach to a running process
python -m profiling.sampling attach 1234
# run a script and save an interactive flame graph
python -m profiling.sampling run --flamegraph script.py
The profiler measures wall-clock time (including waits for the database and network), CPU time, time holding the GIL or exception handling. Results can be saved as a flame graph, a line-level heat map or a Firefox Profiler file, or watched live in the terminal. There is also a mode for asynchronous code. Attaching to a running process needs privileges: on Linux, root, the CAP_SYS_PTRACE capability or a change to the ptrace_scope setting.
The old profile module is deprecated and will be removed in Python 3.17.
Typing and clearer error messages
Teams that use type annotations and tools such as mypy or Pyright get a few additions. A TypedDict can be closed (closed=True) or declare the type of extra keys (extra_items, PEP 728), which describes API responses, for example, more precisely. There are also TypeForm (PEP 747) and the @disjoint_base decorator (PEP 800).
class Point(TypedDict, extra_items=int):
x: int
y: int
p: Point = {"x": 1, "y": 2, "z": 3} # valid: extra keys must have int values
Error messages are more helpful again. When an attribute is available through another object, Python suggests the full path. When someone uses a method name from another language, it points to the Python equivalent:
>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
>>> (1, 2, 3).append(4)
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?
Performance: JIT, Windows and free-threading
- The JIT compiler is still experimental but has improved a lot. According to the documentation, on the pyperformance suite it is on average 7 to 8% faster on x86-64 Linux and 11 to 12% faster on macOS with Apple processors. Individual benchmarks range from about 15% slower to more than twice as fast. When building from source, the JIT is not included by default.
- Windows: the official 64-bit installers use the new tail-calling interpreter, which according to the documentation is on average 15 to 20% faster.
- Free-threading, Python without the GIL, is still a separate, optional build. In 3.15 it gained a stable ABI for C extensions (PEP 803), which makes it easier for libraries to ship packages. The overhead on single-threaded code is, according to the documentation, about 1% (macOS on ARM) to 8% (x86-64 Linux).
- Standard library: among other things, base64 encoding is 2 times faster and decoding 3 times faster.
In a typical web application most of the time goes to the database and the network anyway. A new Python version rarely fixes a slow application. A profiler that shows where the problem really is usually helps more.
What can break an upgrade
The most important behaviour change concerns text encoding. From 3.15, open() and other file operations without an encoding argument always use UTF-8 (PEP 686), whatever the system settings. On Linux usually nothing changes. On Windows the default used to be the locale's code page (for example cp1250 on Polish systems or cp1252 in Western Europe), so scripts that read exports from accounting systems or CSV files from Excel without an explicit encoding may garble national characters after the upgrade.
# always pass the encoding explicitly
with open("export.csv", encoding="cp1250") as f:
...
# warnings for places without an explicit encoding (since Python 3.10)
python -X warn_default_encoding -m pytest
The old behaviour can be restored with PYTHONUTF8=0, but fixing the code is better. Other changes that may need attention:
- The
sre_compile,sre_constantsandsre_parsemodules and theload_module()methods of the import system have been removed. http.server.CGIHTTPRequestHandlerandPurePath.is_reserved()have been removed.datetime.strptime()with a day of month (%d) but no year raisesValueError. This has produced a warning since 3.13.- All
sqlite3.connect()parameters except the database path are now keyword-only. unittest:assertWarns()no longer swallows warnings that do not match, so new messages may appear in test output.argparse: withadd_argument('-f', '-foo')the attribute name is nowfooinstead off.
Announced removals: the asyncio event loop policy system (set_event_loop_policy() and related APIs) goes in 3.16, and the profile module in 3.17. Deprecation warnings are best fixed as they appear, by running tests with -W error::DeprecationWarning.
In 3.15.0, simultaneous imports in several threads could deadlock. The bug was fixed in 3.15.2. We always put the latest patch release on servers, not the first release of a new version.
When and how to move to the new version
We move production systems to a new Python version a few months after its release, once every library in the application ships packages for it and the version itself has had its first fixes. In November 2026, 3.13 or 3.14 is entirely sufficient. Applications on 3.10 and older are urgent, and moving applications off 3.11 needs to be planned before October 2027.
- InventoryA list of applications, scripts and scheduled jobs with their Python version, system and owner. Scripts in the task scheduler and containers built from old images are easy to miss.
- DependenciesCheck that every library supports the target version and that none is abandoned. In Django applications the framework version decides: Django 6.1.3 and later support Python 3.15, while Django 5.2 LTS stops at 3.14 (more in our guide to Django 6).
- Tests on the new versionAutomated tests on the target version, with encoding and deprecation warnings turned on. Where tests are missing, we prepare a list of key scenarios to check by hand.
- Test environmentA new container image or an environment on a test server with a copy of the data. We also check background jobs, exports and integrations with other systems.
- Deployment with a rollback planThe previous version stays ready to restore. After deployment we watch logs, errors and response times.
- The Python version of every application and script recorded in one place.
- Applications on Python 3.10 and older moved first.
- Every library supports the target version and none is abandoned.
- An explicit
encoding=on everyopen()and CSV read. - Tests run with
-X warn_default_encodingand-W error::DeprecationWarning. - The latest Python patch release on servers (for 3.15, at least 3.15.2).
- Containers built from a current base image and rebuilt regularly.
Frequently asked questions
Should we move our application to Python 3.15 straight away?
Usually not straight away. The new features are handy, but in a production system it matters more that every library supports the new version. A sensible time is a few months after release. The exception is applications on unsupported versions: move those as soon as possible, even if only to 3.13 or 3.14.
Is Python 3.10 still safe?
It no longer gets security fixes. Support ended on 1 October 2026 and 3.10.22 is the final release. The application will keep running, but new vulnerabilities in the interpreter and standard library will not be patched.
Does Django work with Python 3.15?
Yes, from Django 6.1.3, released in November 2026. Django 5.2 LTS supports Python 3.10 to 3.14. Wagtail supports Python 3.15 from version 8.1.
Will the JIT speed up my application?
Better not to count on it. The JIT in Python 3.15 is experimental, the average gain in benchmarks is a few per cent and results vary widely between programs. In web applications most of the time usually goes to the database and the network.
Is free-threaded Python ready for production?
It is an officially supported but optional build. It makes sense for multi-threaded computation, provided that every C extension you use supports it. For a typical web application there is no reason to hurry.
Sources
- Python: What's New in Python 3.15
- Python.org: Python 3.15.0 release
- Python Developer's Guide: status of Python versions
- PEP 810: Explicit lazy imports
- PEP 686: Make UTF-8 mode default
- Python: profiling.sampling (Tachyon) documentation
- Django: supported Python versions
As of 6 November 2026. Performance figures come from the Python documentation; results in a specific application may differ.