Python 3.15 Feature Freeze: The 7 PEPs That Will Change How You Write Python

Python 3.15 has entered beta and the feature set is locked. From built-in frozendict and lazy imports to a 15% Windows speed boost, here's everything you need to know about what's coming in October 2026 — and how to prepare your codebase now.

Python 3.15 hit its first beta — and with it, the feature freeze — on May 7, 2026. The final release is scheduled for October 1, but the language changes are now locked. If you maintain a Python project, this is the moment to understand what’s coming, because several of these additions will quietly change how idiomatic Python looks over the next two years.

This isn’t a minor point release. Python 3.15 delivers seven accepted PEPs that touch the language itself, a revamped profiling story, substantial performance gains (especially on Windows), and security hardening that will break some legacy configurations. And behind the headlines, there are smaller changes — a new Counter operator, colored diffs from difflib, base64 encoding that runs 2–3× faster — that make everyday Python coding smoother.

Here’s the breakdown.

What “Feature Freeze” Actually Means

When Python reaches beta 1, the language specification is frozen. No new PEPs can target 3.15. No syntax changes can land. The remaining beta releases (beta 2 on June 2, beta 3 on June 23, and beta 4 on July 18) are strictly for bug fixes and stabilization. After that come two release candidates (August 4 and September 1) and the final 3.15.0 release on October 1, 2026.

The practical takeaway: everything described below is shipping. If you try the 3.15 beta today, you’re running the features that will be in the final release.

The Seven Language PEPs That Made the Cut

PEP 814: frozendict Becomes a Built-in

For years, Python developers have asked for an immutable dictionary. Third-party packages like frozendict filled the gap, but they were never quite first-class. Python 3.15 changes that.

from frozendict import frozendict  # No longer needed — it's built-in

# frozendict is now available without imports
config = frozendict({"host": "localhost", "port": 5432})

# It's hashable — you can use it as a dictionary key
cache = {}
cache[frozendict({"method": "GET", "path": "/api/users"})] = "response data"

# It's accepted by standard library functions
import json
json.dumps(frozendict({"key": "value"}))  # Works natively

import pickle
pickle.dumps(frozendict({"key": "value"}))  # Also works

The practical impact is significant. Configuration objects, cache keys, and any data structure that shouldn’t be mutated now have a built-in, standard representation. If your code currently checks isinstance(obj, dict), you’ll want to switch to collections.abc.Mapping to remain compatible with frozendicts that flow through your system.

PEP 810: Explicit Lazy Imports

Startup time has been a persistent pain point for Python CLI tools and large applications. PEP 810 introduces explicit lazy imports — modules that are only loaded when first accessed, not at import time.

# Before: eager import — all modules loaded at startup
import numpy as np
import pandas as pd
from myapp.heavy import analytics_engine

# After: lazy import — modules loaded only on first use
__lazy_modules__ = ["numpy", "pandas", "myapp.heavy"]

# numpy is not loaded yet
def compute(data):
    import numpy as np  # Only loaded when compute() is first called
    return np.mean(data)

For CLI tools that import heavy scientific libraries just to print a --help message, this is transformative. A tool that previously took 800ms to display help text can drop to under 100ms. The lazy import mechanism is opt-in and backward-compatible — existing eager imports continue working exactly as before.

PEP 661: The sentinel Built-in

Every Python developer has written — or encountered — the _MISSING = object() pattern. It’s a sentinel value: a unique object used to distinguish “no value provided” from None. PEP 661 gives it a proper home.

# Before: the ad-hoc sentinel
_MISSING = object()

def get(key, default=_MISSING):
    if default is _MISSING:
        raise KeyError(key)
    return cache.get(key, default)

# After: the built-in sentinel
from sentinel import sentinel  # Actually, it's a built-in now

_MISSING = sentinel("MISSING")

def get(key, default=_MISSING):
    if default is _MISSING:
        raise KeyError(key)
    return cache.get(key, default)

The difference might seem cosmetic, but named sentinels are picklable, produce useful repr() output, and are type-checkable. They also eliminate the subtle bug where two modules accidentally use object() as a sentinel and compare equal because someone used == instead of is.

PEP 728: TypedDict Gets Strict and Permissive Modes

TypedDict has been Python’s tool for type-annotating dictionaries with known key structures. But until 3.15, there was no way to declare that a TypedDict should reject unknown keys — or that it should accept arbitrary extra keys.

from typing import TypedDict

# Strict mode: reject keys not declared in the TypedDict
class UserConfig(TypedDict, closed=True):
    theme: str
    notifications: bool

config: UserConfig = {"theme": "dark", "notifications": True}
# config["font_size"] = 14  # Type checker would flag this

# Permissive mode: accept extra keys of a specific type
class LogEntry(TypedDict, extra_items=str):
    timestamp: str
    level: str

entry: LogEntry = {
    "timestamp": "2026-07-11T12:00:00",
    "level": "INFO",
    "user_id": "abc123",  # Allowed — extra_items permits additional str values
}

For codebases that lean heavily on TypedDict for API contracts, these additions close a long-standing gap. The closed=True variant is especially useful for configuration objects and API payloads where unknown keys indicate a bug.

PEP 798: Unpacking in Comprehensions

A small but satisfying quality-of-life improvement: you can now use * unpacking inside list, set, and dict comprehensions.

# Before: clunky workarounds
rows = [[1, 2, 3], [4, 5, 6]]
flattened = []
for row in rows:
    flattened.extend(row)

# After: unpacking in a comprehension
flattened = [*row for row in rows]
# Result: [1, 2, 3, 4, 5, 6]

This also works with set and dict comprehensions, and it composes naturally with the rest of Python’s unpacking syntax. It’s the kind of feature that doesn’t make headlines but quietly removes friction from dozens of patterns.

PEP 800: Disjoint Bases

Multiple inheritance is powerful but dangerous. PEP 800 introduces @typing.disjoint_base, a decorator that tells type checkers two classes must never be combined in the same MRO.

from typing import disjoint_base

@disjoint_base
class SyncClient:
    def request(self): ...

@disjoint_base
class AsyncClient:
    async def request(self): ...

# A type checker will now flag this as an error:
class BrokenClient(SyncClient, AsyncClient):  # ❌ Type error
    pass

This is primarily useful for stub files and library authors who want to prevent users from accidentally combining incompatible base classes. Think of it as a guardrail, not a feature you’ll use in everyday application code.

PEP 747: TypeForm

PEP 747 makes type expressions themselves first-class values. This enables more precise type-checking of code that manipulates types — think serialization frameworks, ORMs, and data validation libraries.

from typing import TypeForm

def validate(value: object, expected_type: TypeForm[int]) -> int:
    # expected_type is not just type[int], it's the type expression itself
    if not isinstance(value, expected_type):
        raise TypeError(f"Expected {expected_type}, got {type(value)}")
    return value

For most application developers, TypeForm is infrastructure — you’ll benefit from it indirectly when your libraries produce better type errors. For library authors, it’s a significant improvement to the type system’s expressiveness.

Performance: The Free Lunch Continues

Python 3.15 delivers measurable speed improvements without any code changes on your part. The JIT compiler — introduced experimentally in 3.13 and refined through 3.14 — continues to improve, delivering roughly 8–9% geometric mean speed gains on x86-64 Linux.

The bigger story is Windows. Python 3.15 brings a ~15% performance improvement on Windows, significantly narrowing the historical gap between Windows and Linux Python performance. If you deploy Python services on Windows Server or run data pipelines on Windows workstations, this upgrade pays for itself immediately.

The free-threaded interpreter — Python’s answer to the GIL — is now considered stable for production use in 3.15. A new stable ABI (abi3t) simplifies distribution of compiled extensions that support free-threading. If you have CPU-bound, multi-threaded workloads that were previously forced into multiprocessing, 3.15’s free-threaded mode is worth a serious evaluation.

On the memory side, pymalloc (Python’s small-object allocator) reduces the memory footprint of small objects by 8–12%. The garbage collector has been reverted to the generational algorithm from Python 3.13, fixing a memory-bloat regression introduced in 3.14.

The New Profiling Story

Python 3.15 introduces a unified profiling package that consolidates the classic deterministic profiler and a brand-new statistical sampling profiler.

# Deterministic profiling (formerly cProfile)
import profiling.tracing
profiling.tracing.run('my_function()', sort='cumtime')

# Statistical sampling profiler ("tachyon") — much lower overhead
import profiling.sampling
with profiling.sampling.Sampler(interval=0.01) as sampler:
    my_long_running_function()
sampler.print_stats()

The sampling profiler (nicknamed “tachyon”) incurs only a fraction of the overhead of cProfile, making it viable for production monitoring. If you’ve ever tried to profile a production service with cProfile and watched your latency double, the sampling profiler is the solution you’ve been waiting for. Note that profile and cProfile modules are now deprecated and will be removed in Python 3.17.

Security Hardening You Shouldn’t Ignore

Python 3.15 tightens several security defaults that may break applications relying on legacy configurations:

  • TLS 1.2 is now the minimum version. TLS 1.0 and 1.1 connections will fail by default.
  • SHA-1 signatures are disabled for certificate verification.
  • Certificate verification is stricter across all ssl module operations.
  • Hash randomization uses a stronger algorithm to mitigate hash-collision DoS attacks.

If your application connects to internal services using self-signed certificates or legacy TLS versions, test against the 3.15 beta now. These changes are configurable — you can override the defaults in your SSL context — but the defaults are moving in a direction you should follow.

Smaller Changes Worth Knowing

Not every improvement in 3.15 comes with a PEP number. Here are the smaller additions that will quietly improve your daily workflow:

collections.Counter gets symmetric difference operators:

from collections import Counter

a = Counter("abracadabra")
b = Counter("alakazam")

# New in 3.15: symmetric difference
diff = a ^ b  # Counter({'b': 2, 'c': 1, 'd': 1, 'k': 1, 'l': 1, 'z': 1})
a ^= b        # In-place symmetric difference

difflib.unified_diff produces colored output:

import difflib

diff = difflib.unified_diff(
    old_lines, new_lines,
    fromfile="old.py", tofile="new.py",
    color=True  # New in 3.15
)

base64 encoding gets a significant speedup — 2–3× faster for standard base64, and orders of magnitude faster for Ascii85, Base85, and Z85 variants. If you process large volumes of base64-encoded data (think API payloads, file uploads, or database blobs), this is a free performance upgrade.

tomllib gains write support, completing the stdlib’s TOML 1.1 story. You can now read and write TOML without a third-party dependency.

Migration Checklist for Your Codebase

Here’s what to do between now and October 1 to prepare for Python 3.15.

  1. Replace object() sentinels with sentinel("NAME"). Audit your codebase for the _MISSING = object() pattern and swap it out.

  2. Switch isinstance(obj, dict) to collections.abc.Mapping. With frozendicts now flowing through standard library APIs, any code that checks for dict specifically will miss frozendict instances.

  3. Add __lazy_modules__ to CLI tools and large applications. Identify the heavy imports that slow down startup and make them lazy.

  4. Test against free-threaded mode. If you have multithreaded CPU-bound code, run it under python -X free_threaded and measure the difference.

  5. Verify TLS configurations. Test your application’s outbound connections against the new TLS 1.2 minimum and ensure any legacy internal services are updated or explicitly configured.

  6. Convert .pth files to .start format (PEP 829). The old .pth mechanism is deprecated.

  7. Update profiler imports. Replace import cProfile with import profiling.tracing. The old modules will be removed in 3.17, so you have time — but start now.

Most existing Python code will run unchanged on 3.15. The language is intentionally backward-compatible. But adopting the new features — especially lazy imports, frozendict, and the sampling profiler — will make your code faster, safer, and more expressive. And with the feature freeze behind us, now is the right time to start testing against the beta so you’re ready for the October 1 release.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.