GeezerVison is showing iscinumpy.dev — Python 3.15
Python 3.15 is out! This is a really impactful release, with some really big
additions. A new lazy import system, a powerful sampling profiler, not one but
two new builtins, the usual color/types/errors updates, and lots of key changes
for developers.
Now, re will not be imported until you call the regex() function. If you
never do, it’s never imported. Accessing the special object materializes it,
except in type annotations. This makes things like command line tools faster,
and enables common patterns (like importing everything into a top level module)
to not have an import time drawback. Given that uv does not compile pyc files
by default when installing, this can have an even bigger first-run impact for
uses like uvx. It also support lazy from X import Y syntax.
There’s also a second set of uses; this can help fix circular imports,
typing-only imports, and optional dependencies. This works because you can’t
disable lazy imports; the beta option to force all imports to be eager was
removed before the final release. You can go the other way, though:
-X lazy_imports=all or PYTHON_LAZY_IMPORTS=all makes every import lazy, and
sys.set_lazy_imports_filter() lets you pick which modules this applies to.
A lazy import is only allowed at module scope. Using it in a function, a class
body, or a try/except block is a SyntaxError. Star imports and
__future__ imports can’t be lazy.
This does have a drawback: it moves the import errors to first use. For stdlib,
that’s not a problem, but if you are importing a dependency that might be
missing, you can sometimes use importlib.util.find_spec("module") to check to
see if something is installed without importing it. Due to the way the import
system works, this will import parent modules, and there’s a small (shared)
cost.
If you need help writing __lazy_modules__, my flake8-lazy plugin helps with
that.
Sampling Profiler
The new sampling profiler, tachyon, is awesome. I used it in
my performance of packaging post. The existing cProfile
has become profiling.tracing (back-compat name remains available, though). The
new one is profiling.sampling. Tracing profilers tell you everything that
runs, at the cost of changing the performance of your application. Sampling
profilers just check to see what the state is at a frequency (and Tachyon
supports up to 1MHz!), keeping the performance profile consistent. Having the
fastest sampling profiler for Python ever created built in is amazing!
This also supports modern features, like:
Attachment to running processes (like other 3.14 tools)
Dumping the state of a stuck process
Async and thread aware
Multiple modes, like CPU, wall time, GIL-holding time, exception handling time
5 output modes, including a self-contained HTML flamegraph using D3.js!
Live monitoring mode
Opcode profiling, including the specializations from the adaptive interpreter
This should be one of the first things you try.
python -m profiling.sampling run ... is all you need to get started. On macOS,
it does require sudo, like other sampling profilers.
Also note that CPython is now built with frame pointers enabled, so the profiler
(and debuggers and other tools) can handle native stack unwinding quickly, which
helps. Note that third-party distributions might need updating.
New builtins
There are two new builtins! The first is frozendict, which does what it
says. It’s a mirror of frozenset, but for dict! It is not mutable and can be
used where immutable values are accepted (like as a dict key). The one thing to
keep in mind: like frozenset and set, it’s not a subclass of dict, so
isinstance(D, dict) will be false if D is a frozendict. Always use
collections.abc.Mapping, pattern matching, or duck typing for such a check and
you’ll be fine.
The second builtin is sentinel, and it’s great. There are a few sentinel
values in Python already, like None and Ellipsis. However, if you need your
own, there’s not a really standard way to do it. Here’s what it looks like:
Has a nice repr (MISSING instead of <object object at 0x000002825DF09650>)
Works correctly when pickled or copied (model or class level sentinels)
Supports static typing
A backport (with slightly different semantics) is available in
typing_extensions.
New syntax
There’s a small piece of new syntax: unpacking in comprehensions. This is
something I think most people have tried at least once; and it works now!
Let’s say you have a nested list:
lists=[[1,2],[3,4],[5]]
And you want to flatten it. Knowing how * works everywhere else in Python,
this seems like it would work:
[*lstforlstinlists]
But you ended up having to rewrite it as
[item for lst in lists for item in lst] (if the order is hard to remember,
just format it like a normal nested for loop but without colons and inner
contents above it). Well, now the above expression just works! (generator, list,
set, and dict comprehensions all supported, also async variations).
Errors and colors
python --help is now in color. Unraisable exceptions are colored too. Tab
completion in the REPL is now colored by object kind. sqlite3, ast,
difflib, tokenize, http.server, and timeit now support color. pdb uses
the new REPL now.
Error messages are now better. Nested attributes are now included in the
suggestions; if you access container.area and there’s a
container.inner.area, that will be suggested, for example. And even more
magical; if you try to use a method name from Java, JavaScript, Ruby, or C#,
then Python will suggest the Python equivalent! list.push will recommend
list.append, or str.toUpperCase will recommend str.upper. If you try to
use a mutable method on a immutable object, the error message will suggest using
the mutable counterpart (currently just a few methods covered). del a.b now
also suggests on failure.
Typing changes
The new TypeForm describes the special typing expression’s types. If you write
code that processes types (which I do a lot, for declarative dataclass based
structure processing), this is really handy to typecheck that code.
TypedDict now has closed (no extra keys allowed) and extra_items (type for
arbitrary extra keys).
Other changes:
slice supports subscription.
@typing.disjoint_base, an advanced feature but should help type checkers
better reflect runtime.
TypeVarTuple takes the keyword arguments that TypeVar/ParamSpec already
does.
Performance
The JIT compiler is getting faster. The tail-calling interpreter is faster too,
so JIT is about 7-8% faster, and 11-12% faster on Apple Silicon (15% slower to
100% faster, depending on what you are doing). If it weren’t for the
tail-calling interpreter, maybe we’d already be at the threshold for enabling
the JIT by default. It should be available on official and uv distributions but
still is opt-in.
Windows now can be built with the tail calling interpreter (VS 2026+ required as
the MSVC team helped make this possible). This gives a 15-20% speedup on average
(14-40% depending on the code) on Windows over the old interpreter. Official
binaries are built this way. The official macOS installer now also installs the
free-threaded build by default.
Base64 encoding/decoding 2-3x faster, and base32, Ascii85, base85, and Z85 are 2
orders of magnitude faster. Class creation is a bit faster with shared
descriptors. Free-threaded builds now use mimalloc for raw memory allocations
too.
The lazy imports feature above is also a performance feature. Frame pointers
also can be one.
Other features
TOML support is now TOML 1.1 This is backward compatible, but keep in mind
that something that parses in 3.15 might not parse in 3.11-3.14. Changes include
trailing commas and newlines in inline tables (fantastic!), some new notation
for codepoints, and seconds are optional.
math.integer added for integer-argument math functions.
TaskGroup.cancel can cancel running task groups.
Complex numbers and half-floats are now supported in array
argparse’s suggest_on_error (from 3.14) is now True by default.
array_hook option for json.load (perfect with object_pairs_hook and
tuple/frozendict!)
More math functions, like math.isnormal.
t-string support for pprint (and better pprint defaults).
re.prefixmatch added as better spelling for re.match.
threading.serialize_iterator, synchronized_iterator, and concurrent_tee
make it safe to share iterators and generators between threads (important for
free-threading).
unicodedata.iter_graphemes iterates over grapheme clusters (what a user sees
as one character, like an emoji with a skin tone modifier).
contextmanager (and other ContextDecorators) used as a decorator on a
generator or coroutine function now stays open while it runs; before, it
exited as soon as the generator or coroutine object was created.
Unary plus is allowed in match literal patterns (case +1:).
The import system takes module locks parent-first, which fixes a long-standing
deadlock when two threads import submodules of the same package.
Removals and deprecations
There are quite a few removals and new deprecations. If you’ve been keeping
warnings as errors turned on in your test suite and running against 3.14, you
should be ready (though you might see some new ones). I don’t think most code
will run into the removals, you might see a new deprecation warning though.
Using NamedTuple with keyword arguments (undocumented) is gone. ByteString no
longer shows up in __all__ (deprecated in 3.12, removal coming in 3.17). The
sre_compile, sre_constants, and sre_parse modules are removed, as is
CGIHTTPRequestHandler (and --cgi from http.server). The import system no
longer calls load_module() on loaders. datetime.strptime now raises an error
for a %d without a year. AST node constructors now raise TypeError for
missing or unknown arguments. Several other deprecated arguments and methods
have been cleaned up.
Lots of standard library modules actually had a __version__, version, or
VERSION, those are deprecated; just check sys.version_info. The asyncio loop
policy system (deprecated in 3.14) will be removed in 3.16. The old profile
module is deprecated (removal in 3.17); use profiling.tracing. The -b/-bb
flags (Python 2 to 3 transition helpers) are deprecated.
As mentioned above, re.match is deprecated in favor of re.prefixmatch, but
it’s a soft deprecation, meaning there are no plans to remove it. Just update
when you drop 3.14.
Other Developer changes
There are some big developer changes this round. I’ve already covered the new
profiler. But there are more big ones!
UTF-8 is now the default. This has been in the works for a long time, with
PYTHONWARNDEFAULTENCODING warning if you didn’t request an encoding
explicitly, but now it’s the default. I think this will be a huge boon for new
Python developers, as I’ve often seen errors opening files in the native
encoding by mistake. Hopefully you’ve been enabling that warning and then
explicitly specifying the encoding. Note that the “locale” encoding as an
explicit option was added in 3.10; use that if you do need native encoding.
New package start up configuration. Python has *.pth files that run
whenever you startup Python (unless you pass -S). Those path files support two
unrelated things, though: they can contain path entries and they contain
import <...> lines that import (and therefore run) arbitrary code. This is how
editable installs are implemented, by the way! Now Python has a new file type,
*.start, that takes over the import feature of *.pth files. It also now
calls an entry point, keeping your importer files from having to rely on a
import-time side effect. In 3.15 and 3.16, if you have matching X.start and
X.pth files, the import lines will be ignored in the X.pth file. Eventually
*.pth files will only support paths.
Stable ABI for free-threaded builds. There’s now a stable ABI for
free-threaded Python! It has a t suffix (abi3t). This means you can build a
3.15 free-threaded wheel that will work on future versions of Python. Tools like
scikit-build-core, meson-python, and maturin support it. Also, starting in
3.15rc1, stable ABI extension file names that end in .so can include the
platform triplet (like foo.abi3-x86_64-linux-gnu.so instead of foo.abi3.so),
so extensions for several architectures can be installed side by side. If you
have tests that check for the exact file name, they might need updating.
Other changes:
Protecting the C API from interpreter finalization with new guards and views
and APIs.
PYTHONWARNINGS/-W now supports regular expressions if it starts and ends
with a forward slash.
bytearray.take_bytes added, is faster than bytes(bytearray)
Various methods related to compiling/parsing code now can take module name
Back to the old garbage collector (also backported to 3.14.5!)
Waiting for a subprocess to terminate is more efficient on some platforms.
sys.abi_info added.
Lots of new C-API, including PyBytesWriter, a mechanism to build a bytes
object, PySlot, and more.
Updating to 3.15
So far, updates have been really easy. This has been the best cycle for early
support; even numba (historically months behind) supported 3.15 before release
day!
Final Words
The easiest and fastest way to get any Python is using uv:
uv python install 3.15
# Or 3.15t
This is also one of the best optimized versions of Python, generally faster than
homebrew, etc.
Every letter was typed by me. AI
helped review a draft of the post: Qwen3.5:397 (which complained about 3.15 not
existing and called my post satire…) and Kimi-K2.6 (I loaded the release notes
into the context to avoid any complaints this time!), both using OpenCode, were
both helpful and found a few issues. Claude Opus 5.5 helped update the post for
the 3.15.0 release.
You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.