On this page
Description
Python 3.14 (October 2025) is the newest release in this course. Its two headline features change how strings and annotations are evaluated: template strings (t"..."), which keep values apart from the text around them so they can be escaped safely, and deferred annotations, which are evaluated only when something asks for them. It also brings a handful of smaller changes you'll meet in code review.
Here's the kind of bug t-strings exist for. A team builds SQL with f-strings and relies on code review to catch injection:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
With name = "Ada'); DROP TABLE users;--" the query is whatever the attacker wrote. An f-string produces a finished str - by the time execute sees it, the value and the SQL are indistinguishable. Python 3.14's template strings keep them apart:
query = t"SELECT * FROM users WHERE name = {name}" # a Template, not a str
A library can turn that into 'SELECT * FROM users WHERE name = %s' plus [name] - a parameterized query - from the same convenient syntax (a test's ten-line renderer did exactly that).
We'll go from simple to more technical:
- Template strings: what a
Templateholds and why it isn't astr. - Deferred annotations: forward references without quotes, and how to read annotations safely.
- Smaller changes:
except A, B:, warnings forreturninfinally, zstd anduuid7.
Template strings (PEP 750)
from string.templatelib import Template, Interpolation
t = t"SELECT * FROM users WHERE name = '{name}'"
t.strings # ("SELECT * FROM users WHERE name = '", "'")
t.interpolations # (Interpolation(value="Ada'); DROP...", expression='name', ...),)
def to_sql(template: Template) -> tuple[str, list]:
parts, params = [], []
for item in template: # yields strings and Interpolations in order
if isinstance(item, Interpolation):
parts.append("%s"); params.append(item.value)
else:
parts.append(item)
return "".join(parts), params
- A t-string evaluates its expressions immediately, like an f-string, but returns a
Template: the literal parts and the values (with their expression text and any!r/:spec) are kept separately. - It is not a
str:"x" + t"y"raisesTypeError, andstr(t)doesn't render it - so a template can't silently become an unescaped string. Something must process it: an SQL driver, an HTML escaper, a structured logger that records the values as fields. - The standard library provides the types; the renderers come from libraries. The point is that APIs can require a
Templatefor anything security-sensitive.
Deferred annotations (PEP 649/749)
Until 3.14, annotations were evaluated when the def or class ran, so a forward reference needed quotes (customer: "Customer") or from __future__ import annotations. In 3.14 they're evaluated lazily:
class Order:
customer: Customer # Customer is defined later in the module - fine
total: int
- The class is defined without error even though
Customerdoesn't exist yet. ReadingOrder.__annotations__evaluates them then - and, ifCustomeris still undefined, raisesNameError: name 'Customer' is not defined(as a test showed). annotationlib.get_annotations(obj, format=...)reads them safely:Format.VALUE(evaluate, may raise),Format.FORWARDREF(undefined names becomeForwardRef('Customer')),Format.STRING({'customer': 'Customer', 'total': 'int'}). Frameworks that read annotations (dataclasses, Pydantic, FastAPI) use these instead of__annotations__.- Startup gets cheaper (annotations are never evaluated unless someone asks), and
from __future__ import annotationsis no longer needed for forward references.
Smaller changes in 3.14
except A, B:without parentheses (PEP 758):except ValueError, TypeError:catches either, asexcept (ValueError, TypeError):always did. Withasthe parentheses are still required. (Before 3.14 the unparenthesized form was aSyntaxError.)- Control flow in
finally(PEP 765):return,breakorcontinueinside afinallyblock now gives aSyntaxWarning-'return' in a 'finally' block- because it silently discards any exception being propagated. compression.zstd: Zstandard compression in the standard library (withcompression.gzip,.bz2,.lzmaaliases in the new package).uuid.uuid7(): time-ordered UUIDs (version 7) that sort by creation time - good database keys, unlike randomuuid4.- From earlier skills: the free-threaded build is officially supported (PEP 779),
concurrent.interpretersandInterpreterPoolExecutor(PEP 734),forkserveris Linux's defaultmultiprocessingstart method, andasynciogainedpython -m asyncio ps PIDto inspect a running program's tasks.
Which tool for which job?
| You want to | Use |
|---|---|
| Build SQL, HTML or shell text from values safely | a t"..." template and a library renderer |
| Just format text for people | an f-string |
| Refer to a class defined later in an annotation | write it plainly (3.14) - no quotes needed |
| Read annotations in a framework or tool | annotationlib.get_annotations(obj, format=...) |
| Catch two exception types | except A, B: (3.14), or except (A, B) as e: |
| Time-ordered IDs for database keys | uuid.uuid7() |
| Fast compression in the standard library | compression.zstd |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Passing a t-string where a str is expected | TypeError, or a Template(...) repr in the output | give it to a renderer that accepts templates |
Reading __annotations__ directly on 3.14 | NameError for a forward reference | annotationlib with Format.FORWARDREF |
except A, B as e: | SyntaxError | except (A, B) as e: |
return in a finally block | the exception in flight is silently lost | return after the try statement |
| 3.14 syntax in code that must run on 3.13 | SyntaxError on import there | wait until 3.13 support ends |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. What's the difference between f"... {name}" and t"... {name}"?
Show answer
The f-string produces a finished str. The t-string produces a Template that keeps the literal parts and name's value apart, so a renderer can escape or parameterize it - and it can't be used as a str by accident.
2. On 3.14, a class annotates customer: Customer before Customer is defined. When, if ever, does that fail?
Show answer
Only when the annotations are evaluated - e.g. reading __annotations__ while Customer is still undefined raises NameError. Defining the class is fine; annotationlib.get_annotations(..., format=Format.FORWARDREF) reads them without failing.
3. Why does 3.14 warn about return inside finally?
Show answer
A return (or break/continue) in finally silently discards any exception that was propagating. PEP 765 makes it a SyntaxWarning.
Key takeaways
- t-strings: templates, not strings - the basis for safe SQL, HTML and logging APIs.
- Annotations are lazy in 3.14: forward references just work; read them with
annotationlib. except A, B:without parentheses; warnings forreturninfinally.- zstd, uuid7, supported free-threading and subinterpreters round out the newest release.