Python 3.14 Landed: Free Threading, Template Strings and Lazy Annotations
Python 3.14.0 shipped on October 7, and the list of accepted PEPs in it is longer and stranger than the usual October release. Four of them are worth your attention as a working developer, and three of those change things you can feel in code you’ve already written. The fourth is the one everyone is talking about and the one most likely to be misread.
Everything below that shows a result was run on CPython 3.14 on an Apple Silicon Mac rather than copied out of a doc.
Free threading is supported now, which is not the same as done
The big one is PEP 779, which moves the free-threaded build of CPython from “experimental” to officially supported. This is the build where the global interpreter lock is compiled out, so threads in a single process can genuinely run Python bytecode on multiple cores at once.
Read that carefully, because there are two things it doesn’t say. It doesn’t say the GIL is gone from Python. The free-threaded build is still a separate build, not the default one, and if you install Python 3.14 the normal way you get the normal interpreter with the lock still in place. PEP 779 explicitly calls this phase II and leaves phase III, making it the default, without concrete criteria, on the grounds that it depends on community adoption and on someone demonstrating clear benefit. That is a long way from a decided thing.
It also doesn’t say your code will get faster. Free threading removes a ceiling on multi-threaded CPU-bound work. It does nothing for a single-threaded script except cost you a little, because the build carries extra overhead on ordinary Python execution. The 3.14 release notes put that at roughly 5 to 10 percent depending on platform and C compiler, which is a tax you pay up front on every program in exchange for a benefit only threaded workloads collect.
If you want to try it, the free-threading how-to is the page to read. The macOS and Windows installers offer the free-threaded binaries as an option, and from source it’s the --disable-gil configure flag. To find out at runtime which build you’re on, sysconfig.get_config_var("Py_GIL_DISABLED") returns 1 on a free-threaded build, and sys._is_gil_enabled() tells you whether the lock is currently active. Those are two different questions, which matters, because a free-threaded interpreter will turn the GIL back on automatically if you import a C extension that hasn’t declared support for free threading, and it prints a warning when it does. You can also force the issue with the PYTHON_GIL environment variable or -X gil.
The honest advice for most teams is to install it somewhere and run your test suite against it, then look at what breaks. Your own Python code is probably fine, in the sense that it will run. Whether it is correct is a different question, because a lot of code that has never actually been concurrent has been quietly protected by the GIL from its own race conditions. The dependency story is the bigger one: compiled extensions need a separate free-threaded wheel, and coverage across the scientific and web stacks is still uneven. This is a thing to pilot, not a thing to migrate to.
Template strings are a security feature wearing f-string clothes
PEP 750 adds a t prefix that looks exactly like f and behaves nothing like it. An f-string produces a str. A t-string produces a string.templatelib.Template, and the entire point is that it hasn’t been turned into text yet.
>>> name = "world"
>>> t = t"hello {name}!"
>>> t.strings
('hello ', '!')
>>> t.interpolations
(Interpolation('world', 'name', None, ''),)
>>> t.values
('world',)
The static parts and the substituted values arrive separately, and each Interpolation carries its evaluated value, the original source text of the expression, the conversion (r, s, a or None) and the format spec. That structure is the feature. It means a library that accepts a Template can escape the interpolated values and leave the literal parts alone, which is the shape of the problem in SQL injection and in cross-site scripting. The PEP is upfront that it exists because f-strings make it very easy to write an injection bug that reads like clean code.
What it does not do is escape anything on its own. Nothing in the standard library currently consumes templates for HTML or SQL. This is plumbing for library authors, and it will be a year or two before your ORM or your template engine takes a Template directly. Until then a t-string is mostly a thing you build and hand to something that understands it.
Two details will catch you. There is no __str__, so str(t) gives you a debug repr of the Template rather than the interpolated text, and concatenating a Template with a str raises a TypeError rather than doing something ambiguous. Both of those are deliberate. A t-string that could silently become a string would be an f-string with extra steps, and the whole value is in the fact that it can’t.
Annotations stopped being evaluated at definition time
PEP 649, with PEP 749 covering the details, is the one most likely to touch code you already ship. Annotations on functions, classes and modules are no longer evaluated when the object is defined. The compiler stashes them in a generated __annotate__ function, and they’re computed the first time __annotations__ is read.
The practical payoff is that forward references and circular imports mostly stop hurting. You can annotate a parameter with a class that isn’t defined yet, and the annotation only has to resolve when something actually asks for it. Importing typing machinery purely to satisfy annotations also stops costing you at import time if nothing introspects them.
The new annotationlib module is how you ask, and it gives you three useful answers:
>>> from annotationlib import get_annotations, Format
>>> def f(x: Undefined) -> Later: ...
>>> get_annotations(f, format=Format.STRING)
{'x': 'Undefined', 'return': 'Later'}
>>> get_annotations(f, format=Format.FORWARDREF)
{'x': ForwardRef('Undefined', owner=<function f ...>), 'return': ForwardRef('Later', ...)}
Format.VALUE still raises NameError for genuinely undefined names, which is the behaviour you had before. FORWARDREF hands back a proxy instead, and STRING gives you the source text without evaluating anything.
Where this bites is code that reads __annotations__ directly and assumes it’s a plain dict that was populated at class creation. It’s a descriptor now, and anything that walked cls.__dict__ looking for the key, or mutated the dict in place expecting that to stick, deserves a second look. from __future__ import annotations still works and still stringizes everything, taking precedence over the new behaviour, so if you’re on that import you’re not getting PEP 649 semantics yet. Dropping it is the thing to try, not a thing to rush.
Multiple interpreters, and the small stuff
PEP 734 puts subinterpreters in the standard library as concurrent.interpreters, with a matching InterpreterPoolExecutor in concurrent.futures. Each interpreter is isolated with its own GIL, so you get real parallelism with multiprocessing-style isolation but without a second process.
from concurrent import interpreters
i = interpreters.create()
i.exec('print("hello from", __name__)')
i.close()
The catch is the isolation. Objects don’t cross the boundary for free; you pass data through a queue from create_queue(), and most of what goes through it is copied rather than shared. Interpreter.call() also started life with real restrictions on what kind of callable it accepts. Treat this as a first public API rather than a drop-in replacement for your thread pool.
Among the smaller changes, PEP 758 lets you drop the parentheses in except TimeoutError, ConnectionRefusedError:, though you still need them if you add an as clause, which the error message will tell you clearly. And the error messages generally got better in the way that quietly saves you time: a typo like whille True: now comes back with “Did you mean ‘while’?” instead of a bare syntax error. There’s also a compression.zstd module now, along with zstd support in tarfile, zipfile and shutil.
Calm upgrade advice
Upgrade a side project this month and your real codebase when your dependencies are ready, which is the same advice as every year. Run your test suite under 3.14 and pay particular attention to anything that introspects annotations, because that’s the change that arrives whether you asked for it or not. Install the free-threaded build in a scratch environment and see how far your dependency tree gets, so that when it does become the default you already know your gaps. Leave t-strings alone unless you maintain a library that formats untrusted input into a structured language, in which case start reading PEP 750 properly, because you’re the audience it was written for.