|
| 1 | +--- |
| 2 | +date: 2026-10-10 |
| 3 | +authors: |
| 4 | + - xarlos |
| 5 | +description: Python 3.15 is out. Lazy imports, frozendict, unpacking in comprehensions, UTF-8 by default and a new profiler. |
| 6 | +categories: |
| 7 | + - Python releases |
| 8 | +--- |
| 9 | + |
| 10 | +# Python 3.15 is out |
| 11 | + |
| 12 | +Python 3.15.0 was released on 9 October 2026. It adds a `lazy` keyword for imports, a built-in |
| 13 | +`frozendict`, `*` unpacking inside comprehensions, and a new sampling profiler. It also makes UTF-8 |
| 14 | +the default text encoding on every platform, which is the change most likely to affect code you have |
| 15 | +already written. |
| 16 | + |
| 17 | +<!-- more --> |
| 18 | + |
| 19 | +Everything below comes from the official |
| 20 | +[What's new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html), which is the full |
| 21 | +list. This is the short one. |
| 22 | + |
| 23 | +## The parts you will notice |
| 24 | + |
| 25 | +### Lazy imports |
| 26 | + |
| 27 | +Put `lazy` in front of an import and the module is not loaded until you first use the name |
| 28 | +([PEP 810](https://peps.python.org/pep-0810/)). |
| 29 | + |
| 30 | +```python |
| 31 | +lazy import json |
| 32 | +lazy from pathlib import Path |
| 33 | + |
| 34 | +print("Starting up...") # json and pathlib are not fully imported yet |
| 35 | + |
| 36 | +data = json.loads('{"key": "value"}') # json gets fully imported here |
| 37 | +p = Path(".") # pathlib gets fully imported here |
| 38 | +``` |
| 39 | + |
| 40 | +This is for programs with slow startup, mostly command-line tools that import a lot and use a little |
| 41 | +of it on any one run. The catch: if the module is missing, the error turns up at first use, not on |
| 42 | +the import line. A typo in a lazy import can sit there until that code path runs. |
| 43 | + |
| 44 | +### `frozendict` |
| 45 | + |
| 46 | +A dictionary you can't change, and can therefore hash |
| 47 | +([PEP 814](https://peps.python.org/pep-0814/)). It is a built-in, so there is nothing to import. |
| 48 | + |
| 49 | +```pycon |
| 50 | +>>> a = frozendict(x=1, y=2) |
| 51 | +>>> a |
| 52 | +frozendict({'x': 1, 'y': 2}) |
| 53 | +>>> a['z'] = 3 |
| 54 | +Traceback (most recent call last): |
| 55 | + File "<python-input-2>", line 1, in <module> |
| 56 | + a['z'] = 3 |
| 57 | + ~^^^^^ |
| 58 | +TypeError: 'frozendict' object does not support item assignment |
| 59 | +>>> b = frozendict(y=2, x=1) |
| 60 | +>>> hash(a) == hash(b) |
| 61 | +True |
| 62 | +>>> a == b |
| 63 | +True |
| 64 | +``` |
| 65 | + |
| 66 | +Being hashable means it can be a dictionary key or a member of a set, which a `dict` can't. It keeps |
| 67 | +insertion order but ignores it when comparing, same as `dict`. It is not a subclass of `dict`, so |
| 68 | +`isinstance(a, dict)` is `False`. |
| 69 | + |
| 70 | +### Unpacking in comprehensions |
| 71 | + |
| 72 | +`*` and `**` now work inside comprehensions and generator expressions |
| 73 | +([PEP 798](https://peps.python.org/pep-0798/)). |
| 74 | + |
| 75 | +```pycon |
| 76 | +>>> lists = [[1, 2], [3, 4], [5]] |
| 77 | +>>> [*L for L in lists] # equivalent to [x for L in lists for x in L] |
| 78 | +[1, 2, 3, 4, 5] |
| 79 | + |
| 80 | +>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}] |
| 81 | +>>> {**d for d in dicts} # equivalent to {k: v for d in dicts for k,v in d.items()} |
| 82 | +{'a': 3, 'b': 2} |
| 83 | +``` |
| 84 | + |
| 85 | +Flattening a list of lists is one of the most-asked questions in the server. The old answer was a |
| 86 | +comprehension with two `for` clauses in an order nobody remembers. This one reads the way you would |
| 87 | +say it. |
| 88 | + |
| 89 | +### Error messages |
| 90 | + |
| 91 | +`AttributeError` now recognises method names from other languages and tells you the Python one: |
| 92 | + |
| 93 | +```pycon |
| 94 | +>>> [1, 2, 3].push(4) |
| 95 | +AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'? |
| 96 | + |
| 97 | +>>> 'hello'.toUpperCase() |
| 98 | +AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'? |
| 99 | + |
| 100 | +>>> {}.put("a", 1) |
| 101 | +AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v. |
| 102 | + |
| 103 | +>>> (1, 2, 3).append(4) |
| 104 | +AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object? |
| 105 | +``` |
| 106 | + |
| 107 | +If you came to Python from JavaScript or Java, this one is for you. |
| 108 | + |
| 109 | +## The part that can break things |
| 110 | + |
| 111 | +Python now uses UTF-8 whenever you don't pass `encoding` |
| 112 | +([PEP 686](https://peps.python.org/pep-0686/)). So `open("notes.txt")` reads UTF-8 on every |
| 113 | +platform. |
| 114 | + |
| 115 | +On Linux and macOS that was almost always the case already. On Windows the default used to be the |
| 116 | +system code page, so a script that reads a file written by an older Windows program may now raise |
| 117 | +`UnicodeDecodeError`, or quietly read the wrong characters. |
| 118 | + |
| 119 | +| Situation | What to do | |
| 120 | +|----------------------------------------|----------------------------------------------------| |
| 121 | +| New code | Pass `encoding="utf-8"` anyway. It works on every version | |
| 122 | +| Old file in a legacy encoding | Name it: `open(path, encoding="cp1252")` | |
| 123 | +| Need the old behaviour for a whole run | Set `PYTHONUTF8=0` or run with `-X utf8=0` | |
| 124 | + |
| 125 | +## For people who measure things |
| 126 | + |
| 127 | +There is a new `profiling` package ([PEP 799](https://peps.python.org/pep-0799/)). |
| 128 | +`profiling.tracing` is the deterministic profiler you know as `cProfile`, which stays as an alias. |
| 129 | +`profiling.sampling` is new. It is a sampling profiler called Tachyon that can attach to a process |
| 130 | +that is already running, by PID, and write flame graphs. The pure-Python `profile` module is |
| 131 | +deprecated and goes away in 3.17. |
| 132 | + |
| 133 | +The experimental JIT compiler got a significant upgrade as well. It is still experimental. |
| 134 | + |
| 135 | +## Smaller things |
| 136 | + |
| 137 | +- A `sentinel` built-in for making unique marker values, the job `object()` has been doing |
| 138 | + ([PEP 661](https://peps.python.org/pep-0661/)). |
| 139 | +- `re.prefixmatch()` is the new name for what `re.match()` does. `re.match()` still works and is |
| 140 | + soft deprecated, meaning no removal is planned. |
| 141 | +- The `__cached__` module attribute is gone. Use `__spec__.cached`. |
| 142 | +- Typing gained `TypedDict` with typed extra items ([PEP 728](https://peps.python.org/pep-0728/)) |
| 143 | + and `TypeForm` ([PEP 747](https://peps.python.org/pep-0747/)). |
| 144 | +- The official macOS installer now includes the free-threaded build by default. |
| 145 | + |
| 146 | +## Should you upgrade? |
| 147 | + |
| 148 | +| You are | Suggestion | |
| 149 | +|---------------------------------------|-----------------------------------------------------------------| |
| 150 | +| Learning, on your own machine | Yes. The error messages alone are worth it | |
| 151 | +| Working on a project with dependencies | Try it in a fresh virtual environment first. Packages with compiled parts can take a few weeks to publish 3.15 builds | |
| 152 | +| Running something in production | Wait for 3.15.1 unless you need a feature now | |
| 153 | + |
| 154 | +Python 3.14 is supported until October 2030, so nobody has to hurry. |
| 155 | + |
| 156 | +## Trying it |
| 157 | + |
| 158 | +Installers are on [python.org](https://www.python.org/downloads/). With |
| 159 | +[uv](https://docs.astral.sh/uv/), which keeps versions side by side without touching the one you |
| 160 | +already have: |
| 161 | + |
| 162 | +```console |
| 163 | +$ uv self update |
| 164 | +$ uv python install 3.15 |
| 165 | +$ uv run --python 3.15 python |
| 166 | +``` |
| 167 | + |
| 168 | +Update uv first. A copy from before the release only knows about the release candidates. |
| 169 | + |
| 170 | +Then type `[1, 2, 3].push(4)` and see what it says. |
0 commit comments