On this page
Python 3.15.0 came out on October 9, 2026, a week late because of last-minute bugs in its biggest feature, lazy imports. I ran the new features against a Django 6.1 project to see what helps a web developer and what can bite. Everything below was tested on the last 3.15 release candidate in Docker, since the final image was not published yet.
Can you run Django on 3.15 yet?#
Not officially. The Django installation FAQ lists Python 3.12 to 3.14 for Django 6.0 and 6.1. The tracking ticket says Django 6.1 will be the first version to support 3.15, and on October 9 a maintainer wrote that they would wait a few days for CI and operating system maintainers before switching it on. A fresh Django 6.1.2 project with REST Framework installed and passed manage.py check on 3.15 in my test, but that is not the same as official support.
| Package | Python 3.15 |
|---|---|
| Django | Not yet; planned for the 6.1 series |
| Django REST Framework | Supported since 3.18.2 |
| psycopg | Supported since 3.3.6, with binary wheels |
| Pillow | Preview wheels in 12.3.0, not official support |
My advice: add 3.15 to your CI matrix today so you find problems early, and move production after Django announces support. If you are upgrading Django this week anyway, read about the October security release first.
Lazy imports#
PEP 810 adds a lazy keyword. A lazy import binds the name at once but loads the module only when the name is first used. Startup gets faster because modules you never touch in a given run are never loaded.
import sys
lazy import json
lazy from decimal import Decimal
print("json" in sys.modules) # False: nothing loaded yet
json.dumps({"ok": True}) # json is imported here
print("json" in sys.modules) # TrueYou can write lazy only at module level. It is a SyntaxError inside functions and class bodies, inside try blocks, and on star imports. If a lazy import fails, the error is raised at first use, and the traceback shows both the line that used the name and the import line.
Switching it on for a whole project#
You do not have to edit every import. Running Python with -X lazy_imports=all, or setting PYTHON_LAZY_IMPORTS=all, makes ordinary module-level imports lazy too, except inside try blocks. I timed manage.py check on a fresh Django 6.1.2 project with REST Framework, taking the median of 15 runs:
| Run | Median time | Modules loaded by django.setup() |
|---|---|---|
| Normal | 238 ms | 601 |
-X lazy_imports=all | 153 ms | 340 |
That is about 36 percent faster with no code changes. A real project with more apps and heavier libraries loads more at startup, so the saving may be larger. Management commands, cron jobs and test runs start many short processes, and that is where this pays off.
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = "blog"
def ready(self):
from . import signals # noqa: F401 inside a function, so always eagerTwo more tools help during a migration. sys.set_lazy_imports_filter() takes a function that can force specific imports to stay eager. And a module can set __lazy_modules__ = ["json"] above a plain import json: Python 3.15 makes that import lazy, and older versions ignore the variable, so one file works on both.
frozendict and sentinel#
Two new builtins replace small helpers that many projects carry around. frozendict is an immutable, hashable mapping, so you can use it as a cache key or a safe default for settings:
defaults = frozendict(debug=False, tz="UTC")
cache = {defaults: "ok"} # hashable, so usable as a key
local = defaults | {"tz": "Asia/Kolkata"} # returns a new frozendict
defaults["debug"] = True # TypeErrorIt is not a dict subclass, so isinstance(defaults, dict) is false; check for collections.abc.Mapping instead. Unlike MappingProxyType, it is a copy, not a live view of another dict. sentinel() creates a unique marker for "no value given", for the many places where None is a valid value:
MISSING = sentinel("MISSING")
def get_setting(name, default=MISSING):
if name in SETTINGS:
return SETTINGS[name]
if default is MISSING:
raise KeyError(name)
return defaultUnpacking in comprehensions#
PEP 798 lets you unpack inside a comprehension, which reads better than a nested loop or itertools.chain when you flatten paginated results:
pages = [["a", "b"], ["c"], ["d", "e"]]
rows = [*page for page in pages] # ['a', 'b', 'c', 'd', 'e']
merged = {**d for d in [{"a": 1}, {"b": 2}]} # {'a': 1, 'b': 2}UTF-8 is now the default#
Since PEP 686, open() without an encoding uses UTF-8 on every system. On Linux servers that changes little. On Windows developer machines it fixes a long-standing source of bugs, where reading a fixture or CSV file worked in CI and failed locally. Run your tests once with -X warn_default_encoding to find calls that relied on the old behavior, and use PYTHONUTF8=0 or encoding="locale" if you really need it.
What can break#
strptimewithout a year now raisesValueErrorwhen the format has a day, as indatetime.strptime("03-15", "%m-%d"). Search for date parsing of birthdays or recurring dates.sqlite3.connect()takes every argument after the database name as a keyword only.- Removed modules and APIs:
sre_compile,sre_constants,sre_parse,CGIHTTPRequestHandlerandLoader.load_module(). Old dependencies are the usual source. - Tests:
assertWarnsno longer swallows warnings that do not match, so a test suite may show new warnings. - Startup code in .pth files: PEP 829 adds
.startfiles for packages that run code at startup, and theimportlines in.pthfiles are on their way out over the next releases.
The JIT and free-threading#
Both are still opt-in. The experimental JIT is off by default; the release notes report a 7 to 8 percent geometric mean speedup on x86-64 Linux, with individual benchmarks ranging from about 15 percent slower to more than twice as fast. Free-threading is still a separate python3.15t build. For a typical Django app, where time goes into the database and the network, neither changes much yet. Lazy imports are the feature to try first.
How to test your project#
python3.15 -m venv .venv315
.venv315/bin/pip install -r requirements.txt
.venv315/bin/python -X warn_default_encoding -m pytest
.venv315/bin/python -X lazy_imports=all manage.py checkIf the last command works, run your test suite with -X lazy_imports=all too, and check that every signal handler still fires. If your API has no test suite yet, start with testing REST Framework APIs with pytest: an upgrade without tests is a guess.
Frequently asked questions#
Does Django support Python 3.15?
Not officially as of October 10, 2026. Django's FAQ lists Python 3.12 to 3.14 for Django 6.0 and 6.1, and the Django team plans to add 3.15 support in the 6.1 series. Test in CI now and upgrade production after the announcement.
How do I enable lazy imports in Python 3.15?
Write lazy import module or lazy from module import name at module level, or run Python with -X lazy_imports=all or PYTHON_LAZY_IMPORTS=all to make ordinary module-level imports lazy as well.
Do lazy imports make Django faster?
They speed up startup, not requests. In my test, manage.py check on a fresh Django 6.1.2 project went from 238 ms to 153 ms with -X lazy_imports=all, and Django loaded 340 modules instead of 601.
Why did my Django signals stop working with lazy imports?
Under lazy_imports=all, a module-level import used only for its side effect is never executed. Import signal modules inside AppConfig.ready(), where imports stay eager.
Comments
No comments yet. Be the first.