Programming

Python 3.15 Is Out, Goodbye 3.10? Lazy Imports and a New 3.11 Baseline

Python 3.15 arrived on October 9, 2026, and 3.10 has reached end of life. Here are the key changes, including lazy import, frozendict and the Tachyon profiler, plus a checklist for raising your minimum version to 3.11.

5 key points

  1. 01Python 3.15.0 was officially released on October 9, 2026, and 3.15 will be supported until around October 2031.
  2. 02Per the Developer's Guide, Python 3.10 reached end of life on 2026-10-01, and 3.11 gets security fixes until October 2027.
  3. 03The lazy keyword from PEP 810 (lazy import json) lets you postpone loading a module until it's first used.
  4. 04If you omit encoding in I/O like open(), UTF-8 is now the default regardless of the system environment (PEP 686).
  5. 05The docs say Tachyon (profiling.sampling) attaches to a running process by PID, samples without code changes, and supports sampling rates up to 1,000,000 Hz.

Python 3.15 was officially released on October 9, 2026. Open source developer Simon Willison titled his post on the news "goodbye 3.10, and now I can use 3.11." When a new version ships, most of us read the feature list. But if you maintain a library or a team project, "which versions can we stop supporting?" often matters more in practice. This post covers both: what actually changed in 3.15, and where to set your minimum supported version now that 3.10 is gone.

When did 3.15 ship, and how long is it supported?

Under PEP 602, Python ships a new feature release once a year. The 3.15 schedule is laid out in PEP 790. Development began on May 7, 2025, and after 8 alphas, 4 betas and 3 release candidates, 3.15.0 final came out on October 9, 2026. The last candidate (rc3) was on October 2, so the final took a week.

The PEP also lays out what comes next. 3.15.1 is scheduled for December 1, 2026, and bugfix releases arrive about every two months. The last regular bugfix release is 3.15.12 (scheduled for October 3, 2028), the last version that ships with binary installers. After that, source-only security fixes appear irregularly until around October 2031.

Support status by version

Based on the table in the Python Developer's Guide, here's where things stand.

VersionStatusFirst releaseEnd of support
3.15bugfix2026-10-092031-10
3.14bugfix2025-10-072030-10
3.13security2024-10-072029-10
3.12security2023-10-022028-10
3.11security2022-10-242027-10
3.10end-of-life2021-10-042026-10-01

One point is worth flagging. Simon's post says 3.10 is officially end of life now that 3.15 is out, but the Developer's Guide lists the 3.10 end date as 2026-10-01, slightly before the 3.15 release (October 9). Either way, the conclusion is the same. As of now, 3.10 is not supported.

3.11 deserves a look too. It ends in October 2027. If you drop 3.10 and make 3.11 your minimum, you'll face the same decision again in about a year, so picking 3.12 or 3.13 as your baseline is a realistic option if you're setting a new one.

Based on the Python Developer's Guide version table

3.15's big changes at a glance

What's new in Python 3.15 summarizes the release's main themes like this. Lazy import, the frozendict and sentinel built-in types, UTF-8 as the default encoding, unpacking inside comprehensions, and a stable ABI for free-threaded builds are the biggest changes. On the library side, a new profiling package and a sampling profiler stand out. Let's take them one at a time.

Explicit lazy import (PEP 810)

This is the feature most likely to get attention. Large Python applications often start slowly, and with a deep dependency tree, imports alone can take several seconds. Until now, people worked around it by importing inside functions or loading things on demand with importlib. That works, but import statements end up scattered through the code and are hard to keep consistent.

3.15 introduces a soft keyword, lazy. Here's the example from the docs.

lazy import json
lazy from pathlib import Path
 
print("Starting up...")  # json and pathlib are not fully imported yet
 
data = json.loads('{"key": "value"}')  # json is actually imported here
p = Path(".")                          # pathlib is actually imported here

A lazy import creates a lightweight proxy object, and the real loading happens the moment you first use the name. If loading fails (say, the module doesn't exist), the exception is raised at first use, not at the import statement. The docs explain that the traceback shows both where the name was used and the original import statement, so tracking down the cause isn't hard.

The lazy soft keyword from PEP 810

The limits are clear too. Lazy imports are only allowed at module top level. Using one inside a function or class body, or inside try/except/finally, is a SyntaxError. Star imports and __future__ imports can't be made lazy either.

You can also turn it on globally without changing code. The -X lazy_imports option or the PYTHON_LAZY_IMPORTS environment variable accepts all or normal (the default). With sys.set_lazy_imports_filter() you can control it selectively, such as "only our app's modules are lazy." For code that must also support older versions, put a list of names in the module, like __lazy_modules__ = ["json", "pathlib"], and normal imports of those modules are treated as lazy.

Simon's reason for being happiest about this feature is practical too. The llm CLI he builds has a plugin architecture, and just installing a plugin with heavy dependencies like sentence_transformers added seconds to every command. So he had been recommending that plugin authors use manual lazy imports. Now there's a language-level fix.

Unpacking in comprehensions (PEP 798)

You can use * and ** in list, set and dict comprehensions and in generator expressions. It's syntax that replaces nested comprehensions or itertools.chain.from_iterable().

lists = [[1, 2], [3, 4], [5]]
[*L for L in lists]   # [1, 2, 3, 4, 5]
# same as [x for L in lists for x in L]
 
dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
{**d for d in dicts}  # {'a': 3, 'b': 2}

It extends the unpacking syntax that PEP 448 introduced in 3.5 to comprehensions. Async generator expressions are supported the same way.

frozendict and the sentinel built-in

frozendict is a new built-in dictionary type that can't be modified after creation (PEP 814). One thing to watch: it isn't a subclass of dict but inherits from object directly. It's hashable if all its keys and values are hashable, and it keeps insertion order but ignores order when comparing. So code that checks isinstance(arg, dict) needs to change to isinstance(arg, (dict, frozendict)) or collections.abc.Mapping to accept it. Several standard library modules, including json, pickle and copy, were updated to accept frozendict.

sentinel (PEP 661) is a built-in type for creating a unique marker object that means "no value." It keeps its identity when copied and can be used with the | operator in type expressions. It's a welcome change if you've been hand-rolling patterns like _MISSING = object().

UTF-8 as the default encoding (PEP 686)

This change is quiet but wide-reaching. I/O with no encoding specified, like open('flying-circus.txt'), now uses UTF-8 regardless of the system environment. If you've ever had non-English files get garbled on Windows because of the locale encoding, you'll feel the difference.

On the other hand, code that relied on the locale encoding may behave differently. For compatibility across versions, the docs recommend always passing encoding explicitly, and point to the opt-in encoding warning for finding affected code. If you need the old behavior, you can turn UTF-8 mode off with the PYTHONUTF8=0 environment variable or the -X utf8=0 option. To use the current locale, pass encoding='locale', which has been supported since 3.10.

The new profiling package and Tachyon

The deterministic call tracing that cProfile did has moved to profiling.tracing, and cProfile stays as a backward-compatible alias. Joining it is Tachyon (profiling.sampling), a new statistical sampling profiler (PEP 799). The pure Python profile module is deprecated and scheduled for removal in 3.17.

A sampling profiler periodically captures the stacks of a running process and works out which functions the time piles up in. It doesn't instrument every call, so overhead is low, and no code changes or restarts are needed. According to the docs, it can sample at up to 1,000,000 Hz. Usage looks like this.

# Attach to a running process by PID and sample (stop with Ctrl+C to print the report)
python -m profiling.sampling attach 1234

Simon noted that he needed sudo on macOS. There are four ways to specify the target: attach (a running process), run (a script from the start), run -m (a module) and dump (a per-thread stack snapshot, useful for investigating a hung process). You can choose the measure from wall-clock, CPU, GIL hold time and exception handling time, and output formats include pstats, collapsed stacks, flamegraphs, heatmaps and the Gecko format for Firefox Profiler. The standard library now covers work that used to depend on external tools like py-spy.

Related to this, CPython is now built with frame pointers enabled by default (PEP 831). That makes native stack unwinding faster and more reliable, which helps system profilers and eBPF-based observability tools. The docs do warn, however, that third-party build backends need to preserve Python's sysconfig flags, and that a single native component built without frame pointers can break unwinding for the whole stack.

How much faster is it?

Contrary to what you might hope, general speedups aren't big. In Miguel Grinberg's measurements (on 3.15.0rc3) that Simon cited, performance was mostly the same as 3.14, with the experimental JIT as the exception. On a Fibonacci test it was about 1.2x faster than the standard 3.15 interpreter, and Miguel reportedly saw meaningful JIT gains for the first time. That contrasts with 3.14, which showed big gains over 3.13 in several benchmarks. In other words, 3.15 is closer to a version you upgrade to for features and tooling than for speed.

The release also includes a stable ABI for free-threaded builds (abi3t), wider use of color in command-line output, and typing PEPs 728, 747 and 800, among others. Check the official docs for the full list.

What you get by letting 3.10 go

Raising your minimum supported version lets you strip out compatibility code. Here's what Simon listed as gains from 3.11.

  • tomllib is in the standard library, so you don't need an external package to read TOML.
  • asyncio gained TaskGroup and timeout().
  • datetime.UTC was added as an alias for datetime.timezone.utc.
  • sqlite3 gained setlimit()/getlimit(), create_window_function() and blobopen(), and you can remove an authorizer with set_authorizer(None).
  • The type system also improved in several ways.

He opened a PR in Datasette that deletes the set_authorizer(None) workaround, and updated his cookiecutter templates to test 3.11 through 3.15. If you maintain a library, the most straightforward first step is to adjust your CI matrix the same way, to "3.11 through 3.15."

Trying 3.15 with uv

The fastest way to try it is uv. Here's the sequence Simon used on his Mac. uv has to know about the new release, so update uv itself first.

uv self update
uv python install 3.15
uv python pin --global 3.15   # Default for projects that don't pin their own
uv run python --version

For tools you already installed with uv tool install, you can reinstall them against a specific Python, like uv tool upgrade datasette --python 3.15. A global pin is convenient, but if your projects use different versions, pinning explicitly inside each project causes fewer accidents.

What should you do now?

There's no need to rush. But setting an order of work lightens the load.

  • Find every place in your running services and CI that uses 3.10 and make a list.
  • Schedule an upgrade to 3.11 or later for each 3.10 use. Note that 3.11 lasts only until October 2027.
  • Add 3.15 to your CI matrix and check that your dependencies install on 3.15.
  • Find code that omits encoding in open() or Path.read_text() and make it explicit. It may be affected by the UTF-8 default change.
  • If you have isinstance(x, dict) checks, decide whether they need to accept frozendict.
  • If you have a CLI or tool with slow startup, try lazy import or -X lazy_imports.
  • If a package must also support older versions, consider __lazy_modules__ instead of the lazy keyword.
  • If you have a performance problem in production, try sampling it with profiling.sampling on 3.15.
  • If you ship C extensions, check how your build tools handle abi3t and the frame pointer flag.

Wrapping up

3.15 is a release that changes the development experience more than speed. Lazy import speaks to startup time, Tachyon to finding bottlenecks in production, and the UTF-8 default to cross-platform file-handling accidents. And 3.10's exit gives you a reason to clean up the compatibility code you've held onto for a long time. Even if you don't use the new features in production right away, just adding 3.15 to your CI cuts a good chunk off the next year's migration cost.

FAQ

I'm still on Python 3.10. Am I at risk right now?

The Developer's Guide lists 3.10 as end-of-life (2026-10-01). That means you can no longer expect security fixes, so you should plan to move to 3.11 or later. Services that are already deployed won't stop working immediately.

Do I have to move straight to 3.15, or can I stay on 3.14 or 3.13?

3.13 and 3.12 are in the security-fix phase, and 3.14 is in the bugfix phase, so they're all still supported. The safe order is to check that your required dependencies support 3.15, then add 3.15 to your CI.

Is 3.15 much faster than 3.14?

In Miguel Grinberg's measurements (on 3.15.0rc3), quoted by Simon Willison, performance was mostly similar to 3.14. Only the experimental JIT showed about a 1.2x gain, on a Fibonacci test. Results vary by workload, so measuring your own is the most accurate.

Will code with lazy import still run on 3.14 and earlier?

The lazy keyword itself is 3.15 syntax, so earlier versions raise a SyntaxError. For code that also supports older versions, the docs describe defining a __lazy_modules__ list in the module.

Sources

  1. What's new in Python 3.15 (docs.python.org)docs.python.org
  2. PEP 790, Python 3.15 Release Schedulepeps.python.org
  3. Status of Python versions (Python Developer's Guide)devguide.python.org
  4. Hello Python 3.15, goodbye 3.10, and now I can use 3.11 (Simon Willison)simonwillison.net