
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"
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'}
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
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'>
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)
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
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))
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
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
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))
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
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 [email protected].
Get the next one in your inbox → subscribe at astraedus.dev.