Python 3.15 Beta 1 dropped on May 11, locking in the feature set for the October 2026 release. The usual suspects — lazy imports, JIT improvements, the Tachyon profiler — got all the coverage. However, feature freeze also confirmed four changes that will have more daily impact on production code than those headline additions: a clean way to cancel asyncio task groups, thread-safe iterator utilities that replace Queue boilerplate, a long-overdue fix to how context managers handle async code, and a frozendict built-in that belongs in every config system you have written. These are the Python 3.15 features worth paying attention to.
Related: Python 3.15 Beta 1: Lazy Imports, JIT Gains, and Tachyon Are Locked In
TaskGroup.cancel() Closes a Long-Standing asyncio Gap
If you have written “first result wins” code in asyncio — fire off several parallel requests and discard the slow ones — you know the workaround tax. Before Python 3.15, cancelling a running TaskGroup mid-execution required custom scaffolding. The structured concurrency block had no official cancellation mechanism, which meant developers either accepted the ExceptionGroup cascade or reached for AnyIO and Trio, both of which have had cancel_scope.cancel() since 2018.
Python 3.15 adds TaskGroup.cancel() and closes that gap directly. Call it inside the task group context and all pending tasks are cancelled cleanly — no exception cascade, no manual bookkeeping.
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_from_replica_1())
tg.create_task(fetch_from_replica_2())
tg.create_task(fetch_from_replica_3())
if await any_result_ready():
tg.cancel() # Discard slower replicas cleanly
This matters most for parallel read replicas, multi-source aggregation, and any pattern where you race tasks and want the winner to cancel the rest. The use case existed before; now the stdlib supports it without workarounds. The CPython issue tracking this addition was open for over a year before landing in 3.15.
Thread-Safe Iterators: Finally Replace Your Queue Workarounds
Multi-threaded Python with shared iterators has always been a footgun. The classic problem: multiple worker threads pulling from the same iterator cause skipped values, duplicate items, or outright crashes because iterators are not thread-safe. The workaround — restructuring everything around queue.Queue — works, but it forces you to abandon iterator semantics entirely and rewrite producer-consumer logic around put() and get().
Python 3.15 adds three threading utilities that solve this directly. threading.serialize_iterator(it) wraps any existing iterator with a lock, making it safe to share across threads without restructuring. threading.synchronized_iterator does the same as a decorator for generator functions. threading.concurrent_tee(it, n) splits a single iterator into n independent thread-safe copies — the fan-out pattern that previously required careful Queue plumbing.
shared = threading.serialize_iterator(iter(work_items))
threads = [Thread(target=worker, args=(shared,)) for _ in range(8)]
# Or decorate a generator directly
@threading.synchronized_iterator
def generate_batches(dataset):
for batch in dataset:
yield preprocess(batch)
The relevance grows as free-threaded CPython (no GIL) gains adoption in the 3.15 cycle. Code that was implicitly safe under the GIL will become explicitly unsafe without it. These utilities are the right primitives to build thread-safe data pipelines against the no-GIL future. The Python 3.15 What’s New documentation covers the full API surface.
ContextDecorator Async Fix: The Silent Production Bug
Python’s ContextDecorator has a quiet bug that has been biting developers since 3.11. When you use a @contextmanager-decorated function as a decorator on an async function, the context exits at the first yield instead of covering the full function lifespan. Cleanup code in the finally block silently fails to run. Logging, tracing, database transaction management, audit logging — anything relying on this pattern in async middleware has been broken in subtle ways.
In Python 3.15, ContextDecorator correctly detects and handles async functions, async generators, and regular generators. The context now covers the entire decorated function’s lifespan. For FastAPI middleware, Django async views, and Starlette request handlers, this means existing decorator-based instrumentation code will start working correctly without changes.
@contextlib.contextmanager
def trace_request(operation):
logger.start(operation)
try:
yield
finally:
logger.end(operation) # Now reliably runs for async functions in 3.15
@trace_request("fetch_user")
async def fetch_user(user_id: int):
return await db.users.get(user_id)
The fix was documented in Jamie Chang’s breakdown of underrated 3.15 features, which sparked a 362-point Hacker News discussion this week. The core observation: context managers are now the best way to write decorators in Python — a statement that could not have been made safely for async code before 3.15.
frozendict Built-in and Immutable JSON Parsing
PEP 814 adds frozendict as a Python built-in — an immutable, hashable dictionary. Developers have been implementing this in every major codebase for years: attrs has it, pydantic has a frozen model mode, custom implementations litter every shared config module. It is now in stdlib, which removes the dependency and provides consistent semantics across projects.
Paired with json.loads‘s new array_hook parameter, you can parse JSON directly into immutable structures in one call. Pass array_hook=tuple to get tuples instead of lists throughout. Combine with frozendict for fully immutable config loading.
config = frozendict(host="localhost", port=5432)
config["port"] = 6000 # TypeError: frozendict is immutable
# Immutable JSON parsing in one call
settings = json.loads(config_file.read(), array_hook=tuple)
# Lists in JSON become tuples — no accidental mutation downstream
frozendict is shallow-immutable — inner collections remain mutable unless you also use array_hook=tuple during parsing. For fully immutable configs, you need both. PEP 814 covers hashing rules and comparison semantics in full.
Key Takeaways
- Python 3.15 Beta 1 is feature-frozen as of May 11, 2026 — October 1 is the final release date; start testing on beta now
TaskGroup.cancel()brings asyncio in line with Trio and AnyIO for structured concurrency — the gap is finally closed- Thread-safe iterator utilities (
serialize_iterator,synchronized_iterator,concurrent_tee) replace Queue-based workarounds without restructuring existing code - Any async code using
@contextmanageras a decorator should be tested against 3.15 — the ContextDecorator fix changes behavior, and the new behavior is correct frozendictis now in stdlib; combine withjson.loads(array_hook=tuple)for immutable config loading in one line













