Python · Python 3.12 to 3.14

Python 3.14: Template Strings, Deferred Annotations and More

t-strings and why they aren't strings, lazily evaluated annotations and annotationlib, except without parentheses, the warning for return in finally, zstd and time-ordered uuid7 ids.

  • 5 min read
  • 10 practice questions
On this page
  1. Description
  2. Template strings (PEP 750)
  3. Deferred annotations (PEP 649/749)
  4. Smaller changes in 3.14
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

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:

  1. Template strings: what a Template holds and why it isn't a str.
  2. Deferred annotations: forward references without quotes, and how to read annotations safely.
  3. Smaller changes: except A, B:, warnings for return in finally, zstd and uuid7.

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" raises TypeError, and str(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 Template for 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 Customer doesn't exist yet. Reading Order.__annotations__ evaluates them then - and, if Customer is still undefined, raises NameError: 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 become ForwardRef('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 annotations is no longer needed for forward references.

Smaller changes in 3.14

  • except A, B: without parentheses (PEP 758): except ValueError, TypeError: catches either, as except (ValueError, TypeError): always did. With as the parentheses are still required. (Before 3.14 the unparenthesized form was a SyntaxError.)
  • Control flow in finally (PEP 765): return, break or continue inside a finally block now gives a SyntaxWarning - 'return' in a 'finally' block - because it silently discards any exception being propagated.
  • compression.zstd: Zstandard compression in the standard library (with compression.gzip, .bz2, .lzma aliases in the new package).
  • uuid.uuid7(): time-ordered UUIDs (version 7) that sort by creation time - good database keys, unlike random uuid4.
  • From earlier skills: the free-threaded build is officially supported (PEP 779), concurrent.interpreters and InterpreterPoolExecutor (PEP 734), forkserver is Linux's default multiprocessing start method, and asyncio gained python -m asyncio ps PID to inspect a running program's tasks.

Which tool for which job?

You want toUse
Build SQL, HTML or shell text from values safelya t"..." template and a library renderer
Just format text for peoplean f-string
Refer to a class defined later in an annotationwrite it plainly (3.14) - no quotes needed
Read annotations in a framework or toolannotationlib.get_annotations(obj, format=...)
Catch two exception typesexcept A, B: (3.14), or except (A, B) as e:
Time-ordered IDs for database keysuuid.uuid7()
Fast compression in the standard librarycompression.zstd

Common mistakes

MistakeWhat happensDo this instead
Passing a t-string where a str is expectedTypeError, or a Template(...) repr in the outputgive it to a renderer that accepts templates
Reading __annotations__ directly on 3.14NameError for a forward referenceannotationlib with Format.FORWARDREF
except A, B as e:SyntaxErrorexcept (A, B) as e:
return in a finally blockthe exception in flight is silently lostreturn after the try statement
3.14 syntax in code that must run on 3.13SyntaxError on import therewait 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 for return in finally.
  • zstd, uuid7, supported free-threading and subinterpreters round out the newest release.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
SQL, HTML or shell text built from valuest"..." + a renderer
Text for humansan f-string
Forward references in annotationswrite them plainly (3.14)
Read annotations in a toolannotationlib.get_annotations(obj, format=...)
Catch two types, no asexcept A, B:
Catch two types with asexcept (A, B) as e:
Time-ordered database IDsuuid.uuid7()
Compression in the stdlibcompression.zstd
Debug a live processpython -m pdb -p PID

Snippets worth knowing by heart

from string.templatelib import Interpolation

def to_sql(template):
    parts, params = [], []
    for item in template:
        if isinstance(item, Interpolation):
            parts.append("%s")
            params.append(item.value)
        else:
            parts.append(item)
    return "".join(parts), params

sql, params = to_sql(t"SELECT * FROM users WHERE name = {name}")

from annotationlib import Format, get_annotations
hints = get_annotations(Order, format=Format.FORWARDREF)

order_id = uuid.uuid7()

The traps

  • A t-string is not a str - + with a str raises; str() gives its repr.
  • Reading __annotations__ directly - NameError for undefined forward references.
  • except A, B as e: - still a SyntaxError.
  • return/break/continue in finally - a SyntaxWarning; it hides exceptions.
  • 3.14 syntax - a hard floor for anyone on 3.13.

Staff-level points, one line each

  • Interpolation = value, expression, conversion, format_spec - recorded, never applied; the renderer decides.
  • t-strings evaluate eagerly; only rendering is deferred.
  • Templates concatenate with templates (+ or implicitly), never with str.
  • Annotations compile to an __annotate__ function; __annotations__ calls it with VALUE and caches success, not failure.
  • Format.FORWARDREF gives ForwardRef placeholders; Format.STRING gives source text.
  • from __future__ import annotations still yields strings - unnecessary now, don't add it.
  • except A, B without as only, so it can't be read as Python 2's except A, e.
  • uuid7: millisecond timestamp first, monotonic within a process - 2,000 IDs came out sorted.
  • PEP 768: sys.remote_exec and pdb -p PID attach to a live process at a safe point.
  • Free-threading is supported (PEP 779), still a separate build.

Say it in an interview

  • "I'd make the query API accept only templates, so an f-string can't reach it."
  • "On 3.14 our framework reads annotations through annotationlib with forward references."
  • "UUIDv7 keys insert at the end of the index - fewer page splits than UUIDv4."
  • "If production hangs, we can attach pdb -p without restarting the process."

Practice

This skill comes with 10 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python 3.14: Template Strings, Deferred Annotations and More free

Practice Python 3.14: Template Strings, Deferred Annotations and More free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime