Python 3.14 new features headline a release that finally makes two long-promised ideas official: string templating without eval-time surprises, and a Python interpreter that can run threads without the Global Interpreter Lock getting in the way. Python 3.14 was released on October 7, 2025, and the official What's New in Python 3.14 page along with the full Python 3.14 changelog lay out everything that shipped. If you have been putting off reading the Python 3.14 release notes, this is the practical rundown: what changed, why it matters, and what will break when you upgrade.
This is not a minor point release. Between t-strings, deferred annotation evaluation, and free-threaded builds becoming a supported (not experimental) configuration, Python 3.14 changes how a lot of "advanced" Python code gets written. Below is a complete tour of the top Python 3.14 new features, followed by migration notes on the Python 3.14 breaking changes that matter, gotchas, and a verdict on whether you should upgrade now.
Template strings are the single most talked-about of all the Python 3.14 new features, delivered through PEP 750. If you already use f-strings, t-strings will look familiar — same interpolation syntax, different prefix, and a very different return type.
An f-string evaluates immediately to a str. A t-string evaluates to a Template object from the new string.templatelib module, which keeps the static text and the interpolated expressions separate instead of smashing them together into a finished string. That separation is what makes t-strings useful for things f-strings were never safe for: building SQL fragments, generating HTML, or writing a logging wrapper that can inspect the raw values before they are formatted.
Here is the difference in practice:
from string.templatelib import Template, Interpolation
name = "O'Brien"
age = 41
# The old way: an f-string commits to a final string immediately
greeting = f"Hello {name}, you are {age} years old."
print(greeting)
# The new way: a t-string keeps structure so you can process it safely
template: Template = t"Hello {name}, you are {age} years old."
parts = []
for item in template:
if isinstance(item, Interpolation):
# item.value is the raw Python object, not a stringified one
parts.append(str(item.value).replace("'", "'"))
else:
parts.append(item)
safe_greeting = "".join(parts)
print(safe_greeting)
Because the template keeps a handle on the original value of name before it becomes text, a function consuming the template can decide how to escape or transform it. That is exactly what a web framework needs to stop HTML injection, and what a database layer needs to bind parameters instead of concatenating raw strings. Expect libraries for safe HTML rendering and SQL building to adopt t-strings quickly.
PEP 649 is the quieter but arguably more consequential of the Python 3.14 new features. Type annotations on functions, classes, and modules are no longer evaluated at definition time by default — they are evaluated lazily, on first access, via a mechanism the PEP calls deferred evaluation.
Previously, if you wanted to avoid the cost (and the circular-import headaches) of evaluating annotations eagerly, you reached for from future import annotations, which turned every annotation into a plain string. That trick worked, but it meant tools calling typing.get_type_hints() had to re-parse strings, and forward references needed manual quoting. Python 3.14 makes the lazy behavior the default without turning annotations into strings — they stay as real objects, just evaluated only when something asks for them.
from __future__ import annotations # still works, but no longer required for this trick
class Inventory:
# 'Item' does not need to exist yet when this class body runs in 3.14
items: list["Item"]
def add(self, item: "Item") -> None:
self.items.append(item)
class Item:
def __init__(self, name: str, quantity: int) -> None:
self.name = name
self.quantity = quantity
inventory = Inventory()
inventory.items = []
inventory.add(Item("USB cable", 3))
print([f"{i.name}: {i.quantity}" for i in inventory.items])
# Annotations are only resolved when you actually ask for them
import inspect
print(inspect.get_annotations(Inventory.add))
The practical win: forward references inside a module resolve naturally, import time drops because annotation-heavy code (think large dataclass or Pydantic-style models) no longer pays evaluation cost for annotations nobody inspects, and circular type references between two classes in the same file stop needing string quoting.
Python 3.13 shipped an experimental build of free-threaded Python without the Global Interpreter Lock. Python 3.14 promotes that work from experimental to officially supported through PEP 779. This does not mean the GIL is gone from the default python.org build — it means the free-threaded Python build (python3.14t) is no longer a "use at your own risk" side project; the core team commits to keeping it working and reasonably performant.
The headline number worth remembering: single-threaded code on the free-threaded Python build now runs with roughly a 5-10% overhead compared to the standard build, a big improvement from earlier free-threaded prototypes, largely because the specializing adaptive interpreter now runs in free-threaded mode too.
import threading
import time
def cpu_bound_work(n: int) -> int:
total = 0
for i in range(n):
total += i * i
return total
def run_threaded(thread_count: int, work_size: int) -> float:
threads = []
start = time.perf_counter()
for _ in range(thread_count):
t = threading.Thread(target=cpu_bound_work, args=(work_size,))
threads.append(t)
t.start()
for t in threads:
t.join()
return time.perf_counter() - start
if __name__ == "__main__":
elapsed = run_threaded(thread_count=4, work_size=5_000_000)
print(f"Ran 4 CPU-bound threads in {elapsed:.2f} seconds")
print("On a GIL build this barely parallelizes.")
print("On python3.14t (free-threaded), it can use multiple cores.")
Run this script on a standard build and the four threads mostly take turns because of the GIL. Run it on the free-threaded build and you should see real wall-clock improvement on a multi-core machine, since CPU-bound Python threads can now execute concurrently instead of just interleaving during I/O waits.
PEP 734 adds a new concurrent.interpreters module, exposing the subinterpreter machinery that has existed in CPython's C API for years but was never reachable from pure Python. Each interpreter gets its own GIL (or none, on a free-threaded build) and its own module state, so it sidesteps GIL contention differently than threads and avoids the process-startup cost of multiprocessing. This is a newer, less battle-tested corner of this Python 3.14 release, worth watching as it matures.
from concurrent.interpreters import create, create_queue
def worker(queue) -> None:
while True:
message = queue.get()
if message is None:
break
print(f"Interpreter received: {message}")
results = create_queue()
interp = create()
interp.call(worker, results)
for value in ["first job", "second job", None]:
results.put(value)
interp.close()
print("Subinterpreter finished processing the queue.")
Data cannot flow freely between interpreters the way it can between threads sharing memory — only a limited set of shareable types (None, bool, int, float, str, bytes, and a few others) can cross the boundary, typically through a queue created with create_queue(). That constraint is deliberate: it is what lets each interpreter run in true isolation without needing its own lock dance. For CPU-bound work that does not fit neatly into free-threading yet, concurrent.futures.InterpreterPoolExecutor (built on top of this module) is a drop-in-feeling alternative to ProcessPoolExecutor with lower overhead.
Projects have depended on third-party packages like zstandard or pyzstd for years just to get fast, high-ratio compression. PEP 784 folds Meta's Zstandard algorithm directly into the standard library as compression.zstd, and reorganizes the older bz2, lzma, gzip, and zlib modules under a shared compression namespace (your existing imports keep working).
from compression.zstd import ZstdFile, open as zstd_open
sample_text = ("Python 3.14 makes Zstandard compression available "
"without any third-party dependency.\n") * 2000
raw_bytes = sample_text.encode("utf-8")
with ZstdFile("sample.zst", mode="wb", level=12) as f:
f.write(raw_bytes)
with zstd_open("sample.zst", mode="rb") as f:
restored = f.read()
assert restored == raw_bytes
print(f"Original size: {len(raw_bytes)} bytes")
import os
print(f"Compressed size: {os.path.getsize('sample.zst')} bytes")
os.remove("sample.zst")
Zstandard sits in a genuinely useful spot: compression ratios close to lzma with speed much closer to zlib. For anything shipping compressed payloads over a network or writing large log or cache files to disk, this removes a dependency that almost every data-heavy Python project used to need — and it may be the most immediately useful of all the Python 3.14 new features for backend teams.
A small syntax change with an outsized quality-of-life effect: PEP 758 allows except and except* clauses to list multiple exception types without wrapping them in parentheses, as long as you are not also binding the exception to a name with as. It is one of the smallest Python 3.14 new features on this list, but it is the one you will type the most often.
def parse_config_value(raw: str) -> int | float:
try:
if "." in raw:
return float(raw)
return int(raw)
except ValueError, TypeError:
print(f"Could not parse {raw!r} as a number, defaulting to 0")
return 0
print(parse_config_value("42"))
print(parse_config_value("3.14"))
print(parse_config_value("not-a-number"))
Compare that except ValueError, TypeError: to the Python 3.13 requirement of except (ValueError, TypeError):. Both still work in Python 3.14 — the parenthesized form is not deprecated, it is just no longer mandatory. The moment you add as err to capture the exception instance, the parentheses come back: except (ValueError, TypeError) as err:. If you want the exact grammar diff, it is documented in the full Python 3.14 changelog.
Python 3.13 replaced the decades-old REPL with a PyPy-derived PyREPL that added multiline editing and history search. Python 3.14 builds on that with real-time syntax highlighting as you type — keywords, strings, numbers, and comments each get distinct colors — plus tab-completion for module names inside import statements, and colorized tracebacks driven by a new internal _colorize module that you can theme yourself by calling into _colorize.set_theme().
If you are running Python 3.14 in a terminal that supports ANSI colors, just opening python at a shell prompt and typing a few lines shows the difference immediately: no plugins, no IPython required. Colors respect the PYTHON_COLORS and NO_COLOR environment variables, so CI logs and piped output are not filled with escape codes by default. The REPL work rounds out the interactive side of the Python 3.14 new features in this release.
Most Python 3.14 code will run unmodified, but a handful of removals and behavior changes are worth auditing before you flip your production interpreter:
The official porting guide in the What's New in Python 3.14 document is worth a full read-through if your codebase touches ast, asyncio subprocess management, or multiprocessing directly. Cross-reference it against the full Python 3.14 changelog before you migrate to Python 3.14 across an entire fleet of services.
A few rough edges show up in practice once teams start running these Python 3.14 new features for real:
For most application and library code, yes — upgrade now, but treat the migration notes above as a checklist rather than an afterthought. Template strings and deferred annotations are additive: your existing f-strings and eagerly-evaluated annotations keep working exactly as before, so none of the Python 3.14 new features force a rewrite. The removed deprecated APIs (ast.Num and friends, the asyncio child watchers, pkgutil loaders) are the most visible Python 3.14 breaking changes, and a quick grep across your codebase will tell you in minutes whether you are affected. The bottom line: most teams should migrate to Python 3.14 sooner rather than later, and this Python 3.14 release earns that recommendation.
The one place to slow down before you migrate to Python 3.14 fleet-wide is free-threading. If your workload is CPU-bound and multi-threaded, and your dependencies already publish free-threaded Python wheels, Python 3.14's officially supported no-GIL build is worth a serious evaluation — potentially a big win. If you depend on C extensions that have not caught up yet, stick with the standard build for now and revisit in a release or two; PEP 779 explicitly plans for GIL-optional support to mature over several more releases before the GIL is ever disabled by default. Everyone else gets a genuinely pleasant upgrade: better REPL ergonomics, a useful new compression module, and syntax cleanups that make everyday code a little easier to read.
This closing example combines a few of the release's headline features — template strings for safe interpolation, deferred annotations for forward references, and the parenthesis-free except syntax — into one self-contained script:
from string.templatelib import Template, Interpolation
class Order:
# Forward reference to LineItem resolves lazily thanks to PEP 649
items: list["LineItem"]
def __init__(self) -> None:
self.items = []
def add(self, item: "LineItem") -> None:
self.items.append(item)
def total(self) -> float:
return sum(item.price * item.quantity for item in self.items)
class LineItem:
def __init__(self, name: str, price: float, quantity: int) -> None:
self.name = name
self.price = price
self.quantity = quantity
def render_receipt(order: Order) -> str:
parts = []
header: Template = t"Receipt for {len(order.items)} item(s):\n"
for item in header:
parts.append(str(item.value) if isinstance(item, Interpolation) else item)
for line in order.items:
row: Template = t" {line.name} x{line.quantity} = ${line.price * line.quantity:.2f}\n"
for item in row:
if isinstance(item, Interpolation):
parts.append(f"{item.value:{item.format_spec}}" if item.format_spec else str(item.value))
else:
parts.append(item)
return "".join(parts)
def safe_total(order: Order) -> float:
try:
return order.total()
except ValueError, TypeError:
print("Could not compute total due to bad line item data")
return 0.0
order = Order()
order.add(LineItem("Mechanical keyboard", 89.99, 1))
order.add(LineItem("USB-C cable", 8.50, 3))
print(render_receipt(order))
print(f"Total: ${safe_total(order):.2f}")
That is a snapshot of the Python 3.14 new features that matter most in one script: familiar syntax, a few sharp new tools, and nothing that forces you to rewrite what already works.