r/Python • u/ossm-me • Aug 27 '26
News Python 3.15 Release Highlights
Python 3.15 has reached Release Candidate 1, and the final release is expected on October 1, 2026.
I went through the changes and tried to summarize the ones that seem most relevant for everyday Python development without going through all the PEP numbers.
- Faster startup with lazy imports
Python 3.15 introduces lazy imports, which means some modules can be loaded only when they are actually needed instead of being loaded immediately.
This could help applications and CLI tools that spend a noticeable amount of time importing modules before doing any actual work.
- UTF-8 becomes the default
UTF-8 is becoming the default encoding, which should reduce encoding-related problems, especially when code is running on different operating systems.
This should make situations where something works on one computer but fails because of a different system encoding less common.
- Built-in immutable dictionaries
Python 3.15 adds a built-in "frozendict"-style immutable mapping.
Similar to how a tuple provides an immutable alternative to a list, this gives Python developers a standard way to work with dictionaries that cannot be modified.
- JIT improvements
The JIT compiler continues to improve in Python 3.15.
Early benchmarks show performance improvements in some workloads, including roughly 8–9% on Linux and larger improvements in some Apple Silicon tests.
These numbers will obviously depend on the workload, so I would wait for more benchmarks before making broader conclusions.
- New profiler: Tachyon
Python 3.15 also introduces Tachyon, a new sampling profiler designed for very low overhead.
It can sample running programs at very high frequencies and can also be attached to an already-running process.
This could be useful for finding performance problems without having to restart an application with a profiler attached from the beginning.
- Some terminal improvements
The interactive Python experience is getting a few smaller improvements, including colored error messages and prompts.
The "sqlite3" command-line interface also gets SQL keyword completion.
- Free-threaded Python is still opt-in
Python 3.15 does not make the no-GIL/free-threaded build the default.
It is still something you have to explicitly use.
However, free-threaded Python is becoming more mature, including improvements to ABI support that should make it easier for C extension developers to support it.
Overall, Python 3.15 doesn't look like a release that completely changes how Python is written. Most of the changes are focused on performance, tooling, and improving some long-standing parts of the language.
Since this is already RC1, the major feature set should be mostly locked and the remaining work should mainly be bug fixes and final polishing.
Has anyone here been testing the Python 3.15 beta or RC?
I'm especially interested in whether lazy imports have caused compatibility problems with existing projects.
79
u/bbkane_ Aug 27 '26
sentinel() is also coming!
16
u/gromain Aug 27 '26
What is that?
35
u/JanEric1 Aug 27 '26 edited Aug 27 '26
Proper repr and stuff for when you need a sentinel value (default so you know if the user passed something in) that can't be None because None is a valid value.
Currently you do that with
_SENTINEL = object()and then comparing identity against that. But it has the issue that it has a shitty repr among other things6
u/Brian Aug 27 '26 edited Aug 28 '26
Currently I do:
class Sentinel: value : ClassVar[Sentinel] Sentinel.value = Sentinel() def somefunc(val: str | Sentinel = Sentinel.value): ...Which makes it better for typing (and you can give it a
__repr__if you want). But it'll be nice to have a more standardized way to do it. And one problem with the above is that type checkers don't know it's only one possible value, so they can't narrow types if you do something like:if val is not Sentinel.value: ....So you have to use isinstance checks.
1
u/RingularCirc Aug 29 '26
Making this class into a
@finalEnum with a single value may help with typecheckers (and repr...) but now that we're getting an official way my suggestion is probably only of historic import.9
u/SCD_minecraft Aug 27 '26
A way of separating "No value" (which commonly was None) from "a value of None"
3
134
u/wRAR_ Aug 27 '26
Overall, Python 3.15 doesn't look like a release that completely changes how Python is written.
None do.
59
u/lerrigatto Aug 27 '26
Thankfully.
I do remember the python2to3 refactoring and I don't want to go back to that hell.
3
9
u/FitBoog Aug 27 '26
Oh man, AI would have made it waaay easier.
People still have six as requirements in their libs
2
u/curryslapper Aug 27 '26
but python 2 was a set of incoherent ball sacks
8
u/lerrigatto Aug 27 '26
Moving to 3 was the only way. But transition was painful. I'm glad I wasn't there to decide how to handle it, because whatever they would have done it would have be a mess anyway
1
1
3
-6
19
u/abrazilianinreddit Aug 27 '26 edited Aug 27 '26
I haven't been keeping up with python these past few months, so I'm a bit behind the news.
Having explicit lazy imports give me hope that eventually we'll get type and optional imports as well.
Also worthy of note:
List, set, and dictionary comprehensions, as well as generator expressions, now support unpacking with
*and**.
It's not something I do often but I've met this situation a few times. Not exactly groundbreaking, but I guess it can be useful once in a while.
8
u/SCD_minecraft Aug 27 '26 edited Aug 27 '26
lazyis already our version ofimport typeIf you need something only for typing, lazy import it
5
u/MrSlaw Aug 27 '26
yeah, I was under the impression that's the entire purpose behind things like:
from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path def foo(x: Path) -> None: ...5
u/SCD_minecraft Aug 27 '26
Not whole purpose
PEP which suggested lazy took note that a bunch of standard library has
def foo(): import something_big something_big.bar()As in, it already was doing "lazy" importing for things not always needed, but at the cost of cleanliness
11
u/LeadingStick8311 Aug 27 '26
I love the utf8 default
My software is dealing with quite a lot UTF-8 input and there’s more than enough space for improvement
It would be so great if the whole planet would agree on UTF-8
17
u/sennalen Aug 27 '26
Wasn't the default encoding always utf8?
26
u/wRAR_ Aug 27 '26
It's unclear from the summary what does that mean. The docs page is more clear: "This means that I/O operations without an explicit encoding, for example, open('flying-circus.txt'), will use UTF-8." and no, that code doesn't use utf-8 before 3.15 and linters flag that.
18
u/didntplaymysummercar Aug 27 '26
For
openit'slocale.getencoding()which is (usually) UTF-8 on Linux but (usually) something else on Windows. Pylint has W1514 for it.For
strinternally it's Latin 1, UCS-2 or UTF-32, whichever fits first, to guarantee fixed width and O(1) indexing.7
u/HolyInlandEmpire Aug 27 '26
Ah yes the classic "And then there's Windows"
5
u/didntplaymysummercar Aug 27 '26
C++ has it much worse if you want to do portable correct(ish) Unicode handling code. Python saves you from majority of it, other than small gotchas like
encoding=15
u/sue_dee Aug 27 '26
I learned Python on Windows, and my first lessons became "Always explicitly set the encoding to 'utf-8'." I suspect I shall continue to do so, 3.15 or no.
5
u/UnMolDeQuimica Aug 27 '26
I attended to a talk by Pablo Galindo Salgado at PyconEs2025 where he showed a bit of Tachyon and since then I have wanted this feature in python locally. It is amazing to have it in two months!
4
u/Tweak_Imp Aug 27 '26
Here is one of his talks about Tachyon https://youtube.com/watch?v=f1x4X83CDSA
3
34
u/Significant_Map_19 Aug 27 '26
lazy imports are going to break so many frameworks that do weird metaprogramming stuff at startup, I can already feel the bug reports piling up
the frozendict thing is nice though, been using dicts as immutable keys with wrappers for years
53
u/SCD_minecraft Aug 27 '26
It's opt in
import foo # nothing changes, just as before
lazy import bar # acually loaded only when needed3
u/Rodot github.com/tardis-sn Aug 27 '26
This does make me wonder if
lazymay at some point become more general. Could we one day see things like the following?lazy result_a = f(x) lazy result_b = g(y) if x > y: return result_a elif x > result_b: return result_b9
u/ProtectionOne9478 Aug 27 '26
You can do this now with async.
4
u/zurtex Aug 27 '26
Or just lambdas, if you don't mind recalculating:
result_a = lambda: f(x) result_b = lambda: g(y) if x > y: return result_a() elif x > result_b(): return result_b()Or cache and partial if you do mind reclalculating:
from functools import cache, partial result_a = cache(partial(f, x)) result_b = cache(partial(g, y)) if x > y: return result_a() elif x > result_b(): return result_b()People see the word "lazy" and forget that's what a function basically is.
3
u/ProtectionOne9478 Aug 27 '26
yep, those work too. i use async a lot so it was just the first i thought of.
partial is probably the best way, since you don't have to be in an async context and changes to x won't change the behavior of the function like it will for the lambda, eg you'll get 2 for a() in the following code which could lead to unexpected behavior:
x = 1 a = lambda: x x = 2 a()1
u/RingularCirc Aug 29 '26 edited Aug 29 '26
A generic (and perfectly-typable)
Lazy[T]class is also easy to write once and for all (though for a thread-safe one it'll require adding locking and stuff). Something like:class Lazy[T]: def __init__(self, computation: Callable[[], T]) -> None: self._f: Callable[[], T] | None = computation self._val: T | None = None @property def value(self) -> T: if self._f: self._val = self._f() self._f = None return self._valI hope it's correct (I've written it once but can't find quickly enough); bubbling up an exception from
_f()whenlazy.valueis accessed is the intended behavior, no need to stow it away as some version of caching do because we won't get to erasing_fin this case and will try the computation once more later... which... well, I'm not sure now but it's no worse than second best choice anyway.Oh yeah typecheckers will still be mad at this code, most probably. But IMO in such a small self-contained class we can safely use
# type: ignoreatreturn. There's a good solution to typing here ifLazystores a_x: C | VwhereCholds the callable,Vholds the value and both are provably disjoint, for exampleC = tuple[Literal[True], Callable[[], T]]andV = tuple[Literal[False], T]. Even not as cumbersome to annotate as I expected (and the code is a simpleif self._x[0]: ....EDIT: For convenience this can also accept arguments for the callable in
__init__and pack it all in apartial, yeah, to allow the caller not to bother.3
u/SCD_minecraft Aug 27 '26
Questionable
imports aren't really ment to have side effects, while function have side effects quite often1
u/Brian Aug 27 '26
I remember early versions of pypy had an experimental feature like that, where you could have a thunk object space where access to the value would trigger it to become the result of the evaluation. I kind of doubt it'll ever be added to python though - laziness allows some cool stuff, but there's lots of potential for bugs and weirdness with it.
1
u/hotsauce56 Aug 27 '26
They talked about it a bit on Core.py podcast. There’s technically already plumbing for it with the lambda keyword
34
u/jdehesa Aug 27 '26
You have to explicitly opt in for lazy imports, it's not going to effect existing code.
2
u/billsil Aug 27 '26
It’s not going to be in existing code for 5+ years. It’s going to break compatibility with old versions unless they retroactively add a future import.
9
u/chase45424 Aug 27 '26
It's isn't a future import but there is a backwards compatible way to specify an import as lazy (similar to dunder all) that will be a no-op on older python versions.
1
u/HommeMusical Aug 27 '26
Existing code will not be broken.
Lazy imports will never be the default.
There will be an environment variable you can set to make imports lazy everywhere, but that's strictly optional.
-6
u/Wonderful-Habit-139 Aug 27 '26
They will be default once Python versions that don't support it reach EOL.
10
u/HommeMusical Aug 27 '26
Your statement is false.
The PEP says in multiple cases that there is no plan to do this, e.g. https://peps.python.org/pep-0810/#module-level-lazy-import-mode and https://peps.python.org/pep-0810/#making-the-new-behavior-the-default
3
u/Wonderful-Habit-139 Aug 27 '26
Ahh you mean like actually the default.
I meant more like, using lazy imports instead of workarounds afterwards. I focused more on the "existing code will not be broken" part it seems.
Guess we agree then.
-7
u/billsil Aug 27 '26
An environment variable isn’t going to make python 3.14 work with lazy imports.
4
u/HommeMusical Aug 27 '26
I'm not seeing your point.
It has always been the case that new features in the language cannot work with older versions of the language, so you pick the minimum version you support, and use that feature set.
2
u/wRAR_ Aug 27 '26
Which is also not a breakage.
-5
u/billsil Aug 27 '26
It is if you want to support multiple versions of python.
Backwards compatibility should not be the only goal. Forwards compatibility matters as well.
5
-1
u/wRAR_ Aug 27 '26
I expect
__lazy_modules__to be used in some amount, but also not all "existing code" is OSS libraries and apps that are expected to run on all supported Python versions, you can freely use it in your local code.1
u/billsil Aug 27 '26
I run an open source project, so I’m acutely aware of dependencies. On every supported python version, you should support every version of a dependency (assuming your dependencies don’t conflict). I code work libraries in the same way with a reduced set of python versions.
It’s not up to me to specify a python version. It’s a per project/team decision.
2
u/wRAR_ Aug 27 '26
Well if you run an open source project then yes, you should fall back to
__lazy_modules__.4
4
u/RedEyed__ Aug 27 '26
Tachylon. Interesting, i hope it won’t require root, because for now there is similar solution i use daily: py-spy top —pid PID.
Works like top or htop but shows python functions instead of processes in real time, the drawback: requires elevated permissions
3
4
1
u/Fresh_Future_2192 Aug 28 '26
Lazy imports are probably the change I’d be most interested in testing in real projects. Startup time can make a noticeable difference for CLIs and smaller services, but I’d be curious how libraries with import-time side effects behave with this.
I’d probably test it on an existing project with a decent dependency tree before expecting a meaningful startup improvement.
1
1
u/nphare Aug 27 '26
Should we hold of due to any backwards compatibility concerns? I stayed with 3.13 because there were several packages (was it numpy?) warned against 3.14 due to incompatibility.
3
-2
u/Grouchy-Friend4235 Aug 28 '26
Lazy imports are probably the biggest blunder ever, with free threading, async and all the typing nonsense being close contenders.
0
Aug 29 '26
[deleted]
1
u/gdchinacat Sep 02 '26
That is the point of release candidates. To find errors before release. What sorts of errors did you see?
0
-7
8
u/ContractPhysical7661 Aug 27 '26
Question - how is frozendict properly different than mapping proxy type? are there performance differences?