Python π (3.14) beta 3 is out, which means the features are locked in (normally beta 1, but two additions were made exceptionally in beta 3!). The big feature this time around is template strings. There’s also lots more color (including syntax highlighting in the REPL!), remote debugging, deferred evaluation of annotations, and the usual error message and performance improvements. Subintepreters are now accessible without the C-API (finally!), and free-threaded Python is no longer experimental.
Template strings
Possibly the most noticeable addition to Python π is template strings. These are
basically identical to f-strings, except they don’t render into a string
automatically. So while f"hello {world}"
becomes a string, t"hello {world}"
is a new type, string.templatelib.Template
. You can write functions that take
this new type and process it. Here’s an simple example that implements f-strings
(but skips handling conversion and format specifiers for clarity):
from string.templatelib import Template, Interpolation
def to_string(template: Template) -> str:
return "".join(
item.value if isinstance(item, Interpolation) else item for item in template
)
world = "world"
assert f"hello {world}" == to_string(t"hello {world}")
Since this is a new type, you can check for in it APIs. It is not lazy, though
you can manually build in laziness by evaluating callables, for example. You
have access to the original value
, the expression
string that gave that
value, along with the conversion
and format_spec
options. There are some
proposed additions to standard library based on it, but those are deferred to
3.15.
Forcing a Template
can be useful. If your API takes only template strings, you
can make sure substitutions are sanitized. Unfortunately, that’s a backward
incompatible change on an existing API. I expect initial usage will simply allow
both, with template strings getting automatic sanitation.
REPL
Color all the things!
The new REPL added color in 3.13, and now we are getting it in many more places.
Syntax highlighting is now supported in the REPL. The unittest
, (new) json
,
and calendar
command lines now sport colors. And argparse supports color too,
with a new color
parameter, enabled on the stdlib modules too! Argparse also
gets a new suggest_on_error
parameter.
Remote debugging
You can now connect pdb from one process to another one, using the process ID
with -p PID
. This is enabled by a new sys.remote_exec()
function, which lets
you execute code on an running interpreter in a different process. Other
debuggers and profilers can take advantage of this too. Various ways to disable
this have also been added; you can even build Python with this disabled.
There’s also a new addition to the python -m asyncio
module; new commands
ps PID
and ptree PID
, which allow you to inspect the asyncio state of a
running Python process.
More autocompletion
Modules now autocomplete with <tab>
(though not attributes inside modules).
Error messages
Many improvements have been made to error messages. These are:
- “Did you mean” for Python keyword typos
- Argument unpacking length hints
elif
followingelse
dedicated message- Highlighting for statements in the wrong place
- Better incorrectly closed string message
- Incompatible string prefix explanation
- Better messages for incompatible
as
targets - Better JSON serialization errors with added notes
Faster CPython
There are some speedups from the Faster CPython team:
- A new opt-in tail call interpreter (when built with LLVM 19+), 3-5% faster, up to 30% faster
- Official CPython builds now have JIT enabled as a runtime opt-in
Start up time for a handful of modules has been improved, like subprocess
,
tomllib
, and asyncio
.
Using pdb from the CLI or with breakpoint()
now uses the much faster
sys.monitoring
backend. The popular coverage
library can now do branch
measurements with sys.monitoring
; applying this to the packaging repo cut the
test time from 55 seconds to 35 seconds.
Interpreters
There are two new pure Python ways to use subinterpreters.
The easiest way is with InterpreterPoolExecutor
, which behaves a lot like the
other executors. Here’s an example:
import concurrent.futures
import random
import statistics
def pi(trials: int) -> float:
Ncirc = 0
for _ in range(trials):
x = random.uniform(-1, 1)
y = random.uniform(-1, 1)
if x * x + y * y <= 1:
Ncirc += 1
return 4.0 * (Ncirc / trials)
def pi_in_threads(threads: int, trials: int) -> float:
if threads == 0:
return pi(trials)
with InterpreterPoolExecutor(max_workers=threads) as executor:
return statistics.mean(executor.map(pi, [trials // threads] * threads))
This is faster than ProcessPoolExecutor
, while ThreadPoolExecutor
with
free-threaded Python requires random.Random()
in each thread.
There’s also a lower-level concurrent.interpreters
module that gives you more
control over subinterpreters. Here’s an example:
import concurrent.interpreters
import concurrent.futures
import contextlib
import inspect
tasks = concurrent.interpreters.create_queue()
with (
contextlib.closing(concurrent.interpreters.create()) as interp1,
contextlib.closing(concurrent.interpreters.create()) as interp2,
):
with concurrent.futures.ThreadPoolExecutor() as pool:
for interp in [interp1, interp2]:
interp.prepare_main(tasks=tasks)
pool.submit(
interp.exec,
inspect.cleandoc(
"""
import time
time.sleep(5)
tasks.put(1)
"""
),
)
tasks.put(2)
print(tasks.get_nowait(), tasks.get_nowait(), tasks.get_nowait())
# Prints 1, 1, 2
Static Typing
The big update here is PEP 649/PEP 749, deferred evaluation of type
annotations. This brings most of the features we used
from __future__ import annotations
with less runtime impact, including (most
of the) speed, new constructs, and forward references.
This comes with a new library if you need to use the annotations at runtime,
annotationlib
. This lib has methods to get annotations as strings, values, or
forward references. This change should be mostly transparent, and tools like
typing.get_origin
and typing.get_args
continue to work correctly.
The only other change is the unification of types.UnionType
and types.Union
,
which means that the old-style Union[A, B]
and the new-style A | B
unions
are identical. Union
is now valid in isinstance
.
Smaller features:
memoryview
is now generic type
Typing development is still active, there are several in-progress PEPs related to typing, but they were deferred to Python 3.15.
Compression
A new namespace, compression
, was added, and all the existing compression
libraries are now available inside it. So import tarfile
can now be written
from compression import tarfile
. Similarly with lzma
, bz2
, gzip
, and
zlib
. Don’t worry, the old names are still around.
There’s now a new compression library, as well: zstd
, with a similar API to
lzma
and bz2
.
Language
You no longer have to use parentheses around exceptions in the except
statement. This was a holdover from Python 2, where leaving them off did
something completely different.
try:
f()
except Err1, Err2:
pass
Other features
Several new methods made it to pathlib.Path
, for copying and moving files and
directory trees. There’s also a new .info
attribute with path information,
filled when using iterdir()
.
Other features include:
map()
now supportsstrict=True
, likezip()
super()
is now pickleable and copyable- A few small updates to regex, such as
\z
added and\B
matches an empty string python -c
now dedents code passed to itast.compare()
compares two ASTsast
nodes now have more descriptive reprs (finally!)- You can now terminate/kill workers in ProcessPoolExecutor
- You can specify a max buffer size for
Executor.map
fnmatch.filterfalse()
for excluding matchesfunctools.Placeholder
to hold a place for positional argumentsinspect.ispackage()
addedio.Reader
andio.Writer
protocols addedpython -m json
added (with color!)os.reload_environ()
reloads the environment- More assert methods for unittest.
- New arguments to better handle
file:
URls inurllib
.
Removals and deprecations
The ability to call control flow altering statements inside a finally block is
now a SyntaxWarning
. This was considered confusing for some reason (though I
believe this was clear with a good a mental model of how exceptions and finally
work). Regardless, it’s now a warning. Pluggy (used by pytest) currently uses
this, so you’ll see warning in your tests until it’s updated.
Some other deprecations:
- The
asyncio
policy system PurePath.as_uri
(usePath.as_uri()
instead)os.popen
andos.spawn*
are soft deprecated (no removal timeline)
Some things were removed, but most of them should have been producing warnings
for a while. Remember to run your tests with warnings as errors turned on! These
removals are things like ast
classes that were replaced by ast.Constant
in
3.8, some asyncio stuff, importlib.abc stuff, and some pkgutil stuff.
The remove of __package__
(replaced by __spec__.parent
) was delayed to 3.15.
Other Developer changes
Other features include:
- iOS testing and output improvements
- Three argument
pow()
now considers__rpow__
- Complex arithmetic update to match C99
-X importtime=2
added, tracks cached modules too- Slots are not wrapped if not overridden (small perf improvement)
- Default multiprocessing method on Linux is now
forkserver
instead offork
- Some ctypes structure updates
- Instances are reused in pdb, keeping instance data across breakpoints
- Pickle default protocol is now 5.
- Windows now include ABIFLAGS in
sysconfig.get_config_Vars()
.
And CAPI had a lot of additions to clean it up and make it easier to use, like
PyUnicodeWriter_*
, iteration and conversion additions, integer API, checking
for immortality, and more. There’s also a new PyConfig_*
and PyInitConfig_*
C API for getting and setting runtime configuration.
Updating to 3.14
So far, updates have been really easy.
If you are using argparse, you should use something like this to get the new argparse features on 3.14+:
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser()
(It’s old now, but I like allow_abbrev=False
too, also shown above).
Final Words
If you are using GitHub Actions, the new and best way to add 3.14 is to use this:
- uses: actions/setup-python@v5
with:
python-version: "3.14"
allow-prereleases: true
This works in a matrix, etc. too. If you want to try out free-threaded Python,
that’s either "3.14t"
or enable the free-threaded option.
So far, it’s been pretty easy to adopt. There’s a deprecation warning coming from pluggy that shows up in pytest in some cases. The current nox works out of the box. Tox now works too. pybind11 and mypyc found a bug that was fixed in beta 3. You’ll need pybind11 3.0 (currently in RC) to support 3.14. You can get numpy for 3.14 and 3.14t from the scientific-python nightly wheels.