DEV Community

Cover image for 5 Python 3.14 Features That Change How You Write Code in 2026 (And 2 I'm Still Waiting For)
Diven Rastdus
Diven Rastdus

Posted on • Originally published at astraedus.dev

5 Python 3.14 Features That Change How You Write Code in 2026 (And 2 I'm Still Waiting For)

Python 3.14 lets you delete a line from the top of almost every file you own: from __future__ import annotations. That is one of five changes in the October 2025 release that actually reach the code you write daily. The rest: threads that use every core, strings that cannot be SQL-injected, a debugger you attach to a live process, and one dependency you can finally uninstall. I run our whole automation stack on Python, so I upgraded and kept notes on what changed in practice. Here is the short list that matters, with runnable examples.

Map of the 5 Python 3.14 features covered in this article

1. from __future__ import annotations is finally dead

Python 3.14 stops evaluating annotations when a function or class is defined, so forward references just work and you no longer need the future import. This is PEP 649 and PEP 749, and it is the change most likely to touch your existing code.

Before, referencing a type before it existed raised NameError, and the fix was to quote the type or add the future import:

# Python 3.13 and earlier: NameError at class-definition time
class Node:
    def add_child(self, child: Node) -> None:  # NameError: Node isn't defined yet
        ...
    # the old workaround was to quote it: child: "Node"
Enter fullscreen mode Exit fullscreen mode

In 3.14 that class body runs as-is. Annotations are stored and only computed when something asks for them. When you do want to read them, the new annotationlib module gives you three formats instead of one:

from annotationlib import get_annotations, Format

get_annotations(Node.add_child, format=Format.STRING)
# {'child': 'Node', 'return': 'None'}
Enter fullscreen mode Exit fullscreen mode

If your codebase starts every module with from __future__ import annotations, you can start deleting those lines. Tools like Pydantic and dataclasses benefit for free.

2. Template strings kill the f-string injection footgun

Template strings, written with a t prefix, return the parts of the string before they are joined, so you can escape or validate every interpolated value. This is PEP 750, and it is the fix for the oldest f-string footgun: pasting user input straight into SQL, HTML, or a shell command.

An f-string joins everything immediately, which is exactly what you do not want here:

name = "'; DROP TABLE users; --"
query = f"select * from users where name = '{name}'"  # already unsafe
Enter fullscreen mode Exit fullscreen mode

A t-string hands you a Template object instead of a finished string:

name = "'; DROP TABLE users; --"
template = t"select * from users where name = {name}"
type(template)  # <class 'string.templatelib.Template'>
Enter fullscreen mode Exit fullscreen mode

Now you decide how each part is rendered. Iterating a Template yields the static text as plain strings and every interpolation as an object you can escape:

# Static text is trusted; interpolated values get escaped
def render_safe(template):
    result = []
    for part in template:
        if isinstance(part, str):
            result.append(part)               # literal SQL, safe
        else:
            result.append(quote(part.value))  # your escaping function
    return "".join(result)
Enter fullscreen mode Exit fullscreen mode

The static parts pass through untouched, the values run through quote, and the result is a safe-by-construction string that reads exactly like an f-string.

3. No-GIL is official: threads that use every core

Free-threaded (no-GIL) Python is now an officially supported build in 3.14, though it stays opt-in rather than the default interpreter. CPU-bound threads run in true parallel on it for the first time. This is PEP 779. In earlier versions the global interpreter lock forced Python threads to take turns, so threading only helped for I/O-bound work.

You opt in with the free-threaded build (python3.14t), and you can check the state at runtime:

import sys
sys._is_gil_enabled()  # False on the free-threaded build
Enter fullscreen mode Exit fullscreen mode

Now this actually saturates your cores instead of one:

from concurrent.futures import ThreadPoolExecutor

def crunch(n):
    return sum(i * i for i in range(n))

with ThreadPoolExecutor(max_workers=8) as pool:
    results = list(pool.map(crunch, [10_000_000] * 8))
Enter fullscreen mode Exit fullscreen mode

The trade-off is honest, and the CPython team publishes it: single-threaded code runs about 5 to 10 percent slower on the free-threaded build. For a service that fans work across threads, that is a great deal. Check that your C-extension dependencies ship free-threaded wheels before you switch.

4. Attach a debugger to a live process, no restart

Python 3.14 lets you drop into a debugger inside an already-running process with python -m pdb -p <PID>. This is PEP 768, and it is the feature I did not know I needed until the first time a background job hung in production.

No restart, no adding breakpoint() and redeploying, no scattering print statements and waiting for the bug to happen again:

# Find the stuck process, then attach a live pdb session to it
python -m pdb -p 4242
Enter fullscreen mode Exit fullscreen mode

Under the hood this uses a new sys.remote_exec() interface with real safety controls. You can lock it down in hardened environments:

# Disable remote attaching entirely
export PYTHON_DISABLE_REMOTE_DEBUG=1
Enter fullscreen mode Exit fullscreen mode

Being able to inspect a live process instead of trying to reproduce its state later is a genuine change to how you debug long-running Python.

5. Zstandard is in the stdlib, so pip install zstandard can go

Python 3.14 adds compression.zstd, a built-in module for Zstandard compression, which means one fewer third-party package for a very common job. This is PEP 784. Zstandard gives you gzip-level ratios at much higher speed, and until now you had to pip install zstandard.

The API matches the other compression modules, so it is instantly familiar:

from compression import zstd

data = b"log line that repeats a lot\n" * 1000
packed = zstd.compress(data)
assert zstd.decompress(packed) == data
print(len(data), "->", len(packed))
Enter fullscreen mode Exit fullscreen mode

The standard tarfile, zipfile, and shutil modules learned to read and write Zstandard archives too, so .tar.zst files work with the tools you already use.

What I'm still waiting for

Two things landed as "here, but not yet the default," and both are on my wishlist for the next release.

The JIT on by default. Python 3.14 ships an experimental just-in-time compiler in the official Windows and macOS binaries, but it stays off unless you ask for it:

# Opt in to the experimental JIT
PYTHON_JIT=1 python my_script.py
Enter fullscreen mode Exit fullscreen mode

It is promising, but "experimental and off by default" means most people never see it. I want a JIT that is stable and automatic.

Free-threading as the plain python. Right now no-GIL is a separate build, and a lot of the ecosystem still needs per-package free-threaded wheels before it is safe to switch. I want the day python just means free-threaded, with the whole wheel ecosystem shipping compatible builds so nobody has to think about it.

The takeaway

Python 3.14 is quietly one of the most practical releases in years. If you upgrade, start by deleting your from __future__ import annotations lines, reach for t"..." anywhere you build SQL or HTML, and benchmark your CPU-bound paths on the free-threaded build before you commit to it. All five changes are worth the version bump on their own.


I write these from real work at astraedus.dev, where I build apps and tools. Building something, or stuck on something like this? Reach me at astraedus.dev or theagentthatcould@gmail.com.

Get the next one in your inbox → subscribe at astraedus.dev.

Top comments (0)