Python 3.15 New Features: Lazy Imports, frozendict & More

Python 3.15 new features banner: lazy imports, frozendict, UTF-8 default, Tachyon profiler and sentinel
Python 3.15 New Features Animated banner showing the main Python 3.15 features: lazy imports, frozendict, unpacking in comprehensions, UTF-8 by default, the Tachyon sampling profiler and the sentinel built-in, all connected to a central Python 3.15 node. DEVDOJO · BACKEND · PYTHON Python 3.15 New Features Lazy imports, frozendict, UTF-8 by default, Tachyon profiler & more — with code lazy import json lazy import frozendict UTF-8 Tachyon sentinel [*it for …] 3.15

Python 3.15 new features are finally landing in a stable release: the final version is scheduled for October 9, 2026, and Release Candidate 3 is already out. This is one of the most developer-friendly Python releases in years. You get explicit lazy imports for faster startup, a built-in frozendict, unpacking inside comprehensions, UTF-8 as the default text encoding, a brand-new sampling profiler called Tachyon, and a proper sentinel built-in. In this guide we will go through each feature with small, runnable examples, look at what can break when you upgrade, and finish with a practical upgrade checklist for your backend projects.

What is a PEP? A PEP (Python Enhancement Proposal) is the official design document for a new Python feature. Each one has a number — for example, PEP 810 is the proposal for lazy imports. Click any PEP number in this article to read the original proposal.
Quick Summary
  • Lazy imports (PEP 810): write lazy import json and the module loads only when you first use it. Great for CLIs and big apps.
  • frozendict (PEP 814): an immutable, hashable dict — use it as a cache key or a safe config object.
  • Unpacking in comprehensions (PEP 798): [*row for row in rows] flattens lists in one clean line.
  • UTF-8 by default (PEP 686): open() now uses UTF-8 everywhere, including Windows.
  • Tachyon profiler (PEP 799): python -m profiling.sampling run app.py with flame graphs and live view.
  • Also new: sentinel() built-in, TypedDict extra_items, smarter error messages and a faster JIT.

Python 3.15 Release Timeline: When Can You Use It?

Python follows a yearly release cycle (PEP 602): roughly 17 months of development and a new feature version every October. According to the official release schedule in PEP 790, Python 3.15 went through eight alphas, four betas and three release candidates before the final release.

Python 3.15 release timeline Timeline: development started May 2025, alphas from October 2025 to April 2026, betas May to July 2026, release candidates August to early October 2026, final release October 9, 2026, then bugfix updates for two years and security fixes until about October 2031. Dev startsMay 2025 Alpha 1–8Oct 2025 – Apr 2026 Beta 1–4May – Jul 2026feature freeze RC 1–3Aug – Oct 2, 2026 3.15.0 finalOct 9, 2026 Supportto ~Oct 2031
Figure 1: The Python 3.15 release timeline (source: PEP 790). Bugfix releases come for about two years, then security-only fixes until around October 2031.
Note: If you are reading this before October 9, 2026, use the release candidate for testing only. The release candidates are feature-complete, so everything in this article will be in the final version.

How to Try Python 3.15 Today

You do not need to replace your system Python. Pick one of these isolated options. The CSS tabs below switch between them (no JavaScript involved).

# Install uv first: https://docs.astral.sh/uv/
uv self update
uv python install 3.15
uv venv --python 3.15 .venv315
source .venv315/bin/activate      # Windows: .venv315\Scripts\activate
python --version

Fastest option. If only a release candidate is available, ask for it explicitly, e.g. uv python install 3.15.0rc3.

# Before the final release use the rc tag; afterwards use python:3.15-slim
docker run --rm -it python:3.15-rc-slim python

# Run your project's tests in a throwaway container
docker run --rm -v "$PWD":/app -w /app python:3.15-rc-slim \
  sh -c "pip install -r requirements.txt && python -m pytest"

Best for CI and for checking that your dependencies install cleanly.

pyenv update
pyenv install --list | grep "3.15"
pyenv install 3.15.0       # or the latest 3.15.0rcN
pyenv local 3.15.0
python --version

Good if you already manage multiple versions with pyenv (or pyenv-win on Windows).

# Download the installer from python.org/downloads
# Windows (Python install manager):
py install 3.15
py -3.15 --version

Official installers from python.org. On Windows, the 64-bit build now uses the faster tail-calling interpreter, and the macOS installer includes a free-threaded build.

1. Lazy Imports in Python 3.15 (PEP 810)

This is the headline feature. Large applications often spend hundreds of milliseconds at startup just importing modules they may never use in that run — think of a CLI where --help loads pandas, boto3 and your whole ORM. With PEP 810, you can mark an import as lazy. Python creates a lightweight placeholder, and the real import happens only when the name is first used.

cli.py
lazy import json
lazy from pathlib import Path
lazy import csv

import sys   # normal (eager) import still works


def export(rows, out):
    # csv is actually imported here, on first use
    with Path(out).open("w", newline="") as f:
        csv.writer(f).writerows(rows)


if __name__ == "__main__":
    if "--help" in sys.argv:
        print("usage: cli.py data.json out.csv")   # json/csv never loaded
        sys.exit(0)
    data = json.loads(Path(sys.argv[1]).read_text())
    export(data, sys.argv[2])
Eager vs lazy import startup Comparison of two startup timelines. Eager: all modules are imported before main runs, so startup is long. Lazy: only a placeholder is created at startup, main starts quickly, and each module loads only when first used. Eager import import pandas import boto3 import orm main() runs startup cost paid up front, even for –help Lazy import proxies main() runs pandas (on use) boto3 and orm are never loaded if this run doesn’t touch them time →
Figure 2: With lazy imports, your program reaches main() sooner and only pays for the modules it actually uses.

Rules you must know about lazy imports

  • Module level only. lazy import inside a function, a class body or a try/except block is a SyntaxError.
  • No star or future imports. lazy from x import * and lazy from __future__ import ... are not allowed.
  • Errors move to first use. A typo like lazy from json import dumsp does not fail at import time; it fails when you call dumsp(). The traceback shows both lines so it is still easy to debug.
  • Side-effect imports. If a module registers plugins or patches things when imported, keep it eager — otherwise those side effects happen later than you expect.

Supporting Python 3.14 and 3.15 at the same time

The lazy keyword is a syntax error on older Pythons. For libraries that must support several versions, PEP 810 adds __lazy_modules__. Older interpreters simply ignore it, so the imports stay eager there:

# Lazy on 3.15+, normal eager imports on 3.14 and below
__lazy_modules__ = ["pandas", "boto3"]

import pandas as pd
import boto3

Global switches and measuring the win

# Measure import time before and after (works on older versions too)
python -X importtime -c "import myapp" 2> import.log

# Experiment: make ALL imports lazy for this run
python -X lazy_imports=all -m myapp
PYTHON_LAZY_IMPORTS=all python -m myapp

# Inside Python
python -c "import sys; print(sys.get_lazy_imports())"   # 'normal' by default
Tip: Try -X lazy_imports=all on your test suite first. If everything passes, you have a free startup boost; if something breaks, you have found an import with hidden side effects.

2. frozendict: A Built-in Immutable Dictionary (PEP 814)

Python has had frozenset for years, but no frozen dict. Developers used MappingProxyType, tuples of pairs, or third-party packages. Python 3.15 adds frozendict as a built-in. It cannot be changed after creation and — because it is immutable — it is hashable, so you can use it as a dict key, put it in a set, or pass it to functools.lru_cache.

>>> cfg = frozendict(host="localhost", port=5432)
>>> cfg
frozendict({'host': 'localhost', 'port': 5432})
>>> cfg["port"] = 6543
TypeError: 'frozendict' object does not support item assignment
>>> prod = cfg | {"host": "db.internal"}   # merge returns a NEW frozendict
>>> hash(frozendict(a=1, b=2)) == hash(frozendict(b=2, a=1))
True

Real-world use: caching an expensive lookup

pricing.py
from functools import lru_cache

# A dict argument would raise "TypeError: unhashable type: 'dict'"
@lru_cache(maxsize=1024)
def quote(filters: frozendict) -> float:
    print("computing...")
    base = 100.0
    if filters.get("region") == "IN":
        base *= 0.8
    return base * filters.get("qty", 1)


print(quote(frozendict(region="IN", qty=3)))   # computing... 240.0
print(quote(frozendict(qty=3, region="IN")))   # cache hit: 240.0

When you accept “any mapping” in a function, check with isinstance(x, collections.abc.Mapping) or isinstance(x, (dict, frozendict)). Note that frozendict is not a subclass of dict, so isinstance(x, dict) returns False. The standard library modules json, pickle, copy and pprint already understand it.

3. Unpacking in Comprehensions (PEP 798)

Flattening a list of lists used to need a double loop that many beginners find confusing: [x for row in rows for x in row]. Python 3.15 lets you use * and ** directly inside comprehensions:

>>> rows = [[1, 2], [3, 4], [5]]
>>> [*row for row in rows]
[1, 2, 3, 4, 5]

>>> tag_sets = [{"py", "api"}, {"api", "db"}]
>>> {*s for s in tag_sets}
{'py', 'api', 'db'}

>>> layers = [{"debug": False, "port": 80}, {"debug": True}]
>>> {**layer for layer in layers}       # later dicts win
{'debug': True, 'port': 80}

>>> gen = (*row for row in rows)          # works in generators too
>>> sum(gen)
15

A nice backend example: merging config layers (defaults → environment → CLI flags) is now a single readable expression, settings = {**layer for layer in (defaults, env_cfg, cli_cfg)}.

4. UTF-8 Is Now the Default Encoding (PEP 686)

Before 3.15, open("file.txt") used your locale encoding. On Linux and macOS that was usually UTF-8, but on many Windows machines in India and elsewhere it was cp1252. Code that worked on your Mac crashed on a teammate’s Windows laptop with UnicodeDecodeError as soon as a file contained “₹”, “नमस्ते” or an emoji. Python 3.15 turns on UTF-8 Mode by default, so text I/O uses UTF-8 on every platform.

# Python 3.15: UTF-8 on every OS
with open("invoice.txt", "w") as f:
    f.write("Total: ₹1,499")

# Need the old behaviour for a legacy file? Ask for it explicitly:
with open("old_export.csv", encoding="locale") as f:
    data = f.read()
# Turn UTF-8 Mode off for a whole process (temporary escape hatch)
python -X utf8=0 legacy_script.py
PYTHONUTF8=0 python legacy_script.py
Best practice: Keep writing encoding="utf-8" explicitly in libraries that still support older Pythons. It is clear, and it behaves the same on every version.

5. Tachyon: The New Sampling Profiler (PEP 799)

Python 3.15 reorganises profiling into a new profiling package. profiling.tracing is the deterministic profiler you know from cProfile (which stays as an alias), and profiling.sampling is Tachyon — a high-frequency statistical profiler. Instead of instrumenting every function call, it samples the call stack many times per second, so the overhead is very low and you can even attach it to a process that is already running in production.

# Profile a script and open an interactive HTML flame graph
python -m profiling.sampling run --flamegraph app.py

# Profile a module (e.g. a FastAPI app started via uvicorn)
python -m profiling.sampling run --live -m uvicorn main:app

# Attach to an already-running process by PID, all threads
python -m profiling.sampling attach -a 12345

# Only count CPU time (ignore time spent waiting on I/O)
python -m profiling.sampling run --mode cpu --pstats app.py

# Where is my asyncio code stuck? Dump stacks with task info
python -m profiling.sampling dump --async-aware 12345

–flamegraph

Interactive HTML flame graph — the quickest way to spot hot paths.

–heatmap

Line-level HTML heatmap of your source files.

–live

A top-like terminal view that updates in real time.

–gecko / –collapsed

Export to Firefox Profiler or any flame-graph tool.

Modes matter: --mode wall (default) measures real elapsed time and is ideal for slow API endpoints that wait on databases; --mode cpu shows pure computation; --mode gil shows who is holding the GIL. The old pure-Python profile module is deprecated and scheduled for removal in Python 3.17.

6. sentinel() Built-in and Typing Upgrades

sentinel (PEP 661)

Ever needed to tell the difference between “argument not passed” and “argument passed as None“? People used object(), which has an ugly repr and does not work nicely with type checkers. Python 3.15 adds a sentinel built-in:

repo.py
MISSING = sentinel("MISSING")

def update_user(user_id: int, *, email: str | None | MISSING = MISSING) -> dict:
    changes = {}
    if email is not MISSING:
        changes["email"] = email      # None means "clear the email"
    return changes

print(update_user(1))               # {}
print(update_user(1, email=None))   # {'email': None}
print(MISSING)                      # MISSING  (a readable repr)

Sentinels compare equal only to themselves (always use is), survive copy/deepcopy, can be pickled when defined at module level, and support | in type hints.

TypedDict with extra_items and closed (PEP 728)

Very handy for typing JSON payloads in APIs. You can now say what type extra keys must have, or forbid them entirely:

from typing import TypedDict

class Movie(TypedDict, extra_items=bool):
    name: str          # any other key must be a bool

class StrictUser(TypedDict, closed=True):
    id: int
    email: str         # no extra keys allowed at all

m: Movie = {"name": "Dangal", "is_hit": True}   # OK for type checkers

Python 3.15 also adds TypeForm (PEP 747) for functions that accept a type expression as a value — useful for validation libraries like Pydantic-style parsers.

7. Smarter Error Messages and a Faster JIT

Python keeps getting friendlier for people coming from JavaScript or Java. The interpreter now recognises common method names from other languages:

>>> [1, 2, 3].push(4)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
>>> "hello".toUpperCase()
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?
>>> {}.put("a", 1)
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.
>>> (1, 2).append(3)
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?

It can even suggest nested attributes, e.g. “Did you mean ‘.inner.area’ instead of ‘.area’?”. On performance, the experimental JIT compiler got a big upgrade: the release notes report around an 8–9% geometric-mean speedup on x86-64 Linux and 12–13% on Apple Silicon macOS compared to the standard interpreter. Frame pointers are also enabled by default (PEP 831), which makes system profilers like perf much more useful.

If you are building AI tooling in Python or TypeScript, these startup and profiling gains matter for servers that start often — see our guide on how to build an MCP server for a related example of a short-lived developer tool process.

Common Python 3.15 Upgrade Errors & Fixes

Error / symptomWhy it happensFix
SyntaxError on lazy importUsed inside a function, class or try block — or running on Python ≤ 3.14.Move it to module level; for older versions use __lazy_modules__ instead.
ImportError appears “late”, deep in a requestLazy imports raise at first use, not at startup.Add a smoke test that touches each lazy name, or keep critical imports eager.
Plugin / signal handler silently not registeredA module with import-time side effects was made lazy.Import such modules eagerly (no lazy, not in __lazy_modules__).
UnicodeDecodeError reading an old Windows fileUTF-8 is now the default; the file was saved in cp1252.open(path, encoding="locale") or the real encoding, e.g. "cp1252".
TypeError: 'frozendict' object does not support item assignmentTrying to mutate an immutable mapping.Create a new one: new = fd | {"k": v}, or convert with dict(fd).
isinstance(x, dict) is False for frozendictfrozendict is not a dict subclass.Check collections.abc.Mapping instead.
DeprecationWarning for profileThe pure-Python profiler is deprecated (removal in 3.17).Switch to profiling.tracing / cProfile or Tachyon.
pip install builds from source and failsA C-extension package has no 3.15 wheels yet.Upgrade the package, wait for wheels, or keep production on 3.14 for now.

Python 3.15 Upgrade Best Practices

  1. Test in CI first. Add "3.15" to your GitHub Actions matrix (or a python:3.15-slim Docker job) and run the full test suite with -W error::DeprecationWarning.
  2. Check your dependencies. Big libraries such as NumPy, Pydantic and database drivers usually publish wheels within days or weeks of the release. Run pip install -r requirements.txt on 3.15 early so you know which ones block you.
  3. Adopt lazy imports where startup matters. CLIs, serverless functions (AWS Lambda, Cloud Run cold starts) and dev tools benefit most. Long-running web servers benefit less, since imports happen only once.
  4. Be explicit about encodings at boundaries. When reading files produced by other systems (Excel exports, bank CSVs), specify the encoding instead of relying on any default.
  5. Use frozendict for config and cache keys. Immutable settings objects prevent a whole class of “who changed this at runtime?” bugs.
  6. Profile before optimising. Run Tachyon with --flamegraph on a slow endpoint before rewriting anything.
  7. Roll out gradually. Many teams wait for 3.15.1 or 3.15.2 for critical production services — a reasonable, low-risk approach.
.github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.13", "3.14", "3.15"]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true
      - run: pip install -r requirements.txt pytest
      - run: python -m pytest -W error::DeprecationWarning
Note: allow-prereleases: true lets setup-python pick the release candidate until the final 3.15.0 is published, then it uses the stable version automatically.

For the complete list of changes, read the official What’s New in Python 3.15 page.

Frequently Asked Questions

When is Python 3.15 released?

The final Python 3.15.0 release is scheduled for October 9, 2026, according to PEP 790. Release Candidate 3 came out on October 2, 2026. Python 3.15 will get bugfix releases for about two years and security fixes until around October 2031.

What are the most important Python 3.15 new features?

Explicit lazy imports (lazy import x), the built-in frozendict, unpacking in comprehensions ([*x for x in xs]), UTF-8 as the default encoding, the Tachyon sampling profiler in the new profiling package, the sentinel built-in, TypedDict extra_items/closed, better error messages and a faster JIT.

Do lazy imports make my web app faster?

They mainly reduce startup time and memory for code paths that never use a module. CLIs, serverless functions and test runs gain the most. A FastAPI or Django server that runs for hours imports everything once, so request speed is unchanged — but faster restarts and cold starts still help.

Is Python 3.15 still using the GIL?

Yes, the default build still has the GIL. The free-threaded (no-GIL) build has been officially supported since Python 3.14 and is available as a separate build; the macOS installer for 3.15 now includes it by default as an option.

Should I upgrade my production app to Python 3.15 immediately?

Start testing now, but upgrade production once all your key dependencies publish 3.15 wheels and your test suite is green. Many teams wait for the first one or two bugfix releases (3.15.1/3.15.2).

Will UTF-8 default break my existing code?

Only code that silently relied on a non-UTF-8 locale encoding, mostly on Windows. Fix it by passing encoding="locale" or the exact encoding, or temporarily run with -X utf8=0.

Conclusion

Python 3.15 is a practical, “quality of life” release. Lazy imports solve a real startup problem without hacks, frozendict and sentinel fill long-standing gaps in the built-ins, UTF-8 by default removes a classic cross-platform bug, and Tachyon gives every developer a production-grade profiler for free. Install the release candidate in a virtual environment, add 3.15 to your CI matrix, and try -X lazy_imports=all on your project today — you may get a faster app with zero code changes.

New posts every day on DevDojo

Practical tutorials on Python, FastAPI, Node.js, React, Next.js and AI — written for beginner and intermediate developers.

Bookmark devdojo.co.in, and tell us in the comments which Python 3.15 feature you’ll use first!

Share