insight Simon Willison 2026-10-12T18:15 正文 ok 970 words

你好,Python 3.15;再见,3.10;现在我终于可以使用 3.11

英文标题:Hello Python 3.15, goodbye 3.10, and now I can use 3.11

Python 3.15 正式发布,带来了显式惰性导入、推导式解包和标准库采样性能分析器 Tachyon 等重要能力。新版本整体性能与 Python 3.14 大致相当,但实验性 JIT 在 Fibonacci 测试中带来了约 1.2 倍加速。随着 Python 3.10 结束官方支持,需兼容所有受支持版本的开源项目可以开始采用 Python 3.11 的 tomllib、asyncio TaskGroup、SQLite API 等功能。文章还介绍了如何通过 uv 切换到 Python 3.15,以及这些变化对 CLI 工具启动速度、性能诊断和项目维护的实际意义。
打开原文相关 2.4 · 机会 2
media127.9 KB

中文

你好,Python 3.15;再见,3.10;现在我终于可以使用 3.11

2026 年 10 月 12 日

Python 3.15 来了!这是必读的 Python 3.15 新特性指南,今年由 3.14 和 3.15 发布经理 Hugo van Kemenade 策划。作为一名构建需要在每个受支持 Python 版本中运行的开源软件的开发者,我终于可以告别 Python 3.10,并开始采用 Python 3.11 的功能。

首先,来看看 Python 3.15 的一些亮点。

显式惰性导入

我最期待的是来自 PEP 810 的显式惰性导入。它允许你像这样导入模块:

lazy import json
lazy from pathlib import Path

随后,模块会在后续代码首次使用这些符号时才被导入。

我的 LLM CLI 工具支持插件。由于只要安装使用了 sentence_transformers 等重型依赖的插件,就可能让每一次 llm 命令调用增加数秒时间,我不得不在插件作者文档中建议使用手动惰性导入。惰性导入直接解决了这类启动成本问题。

推导式中的解包

新的 [*sublist for sublist in lists] 解包语法很不错。下面是一个取自文档的示例:

lists = [[1, 2], [3, 4], [5]]
[*L for L in lists]
# [1, 2, 3, 4, 5]
# equivalent to [x for L in lists for x in L]

sets = [{1, 2}, {2, 3}, {3, 4}]
{*s for s in sets}
# {1, 2, 3, 4}
# equivalent to {x for s in sets for x in s}

dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
{**d for d in dicts}
# {'a': 3, 'b': 2}
# equivalent to {k: v for d in dicts for k,v in d.items()}

Adam Hartz 和 Erik Demaine 提出的 PEP 798:推导式中的解包,将这一能力作为 PEP 448 中解包语法的扩展。PEP 448 早在 2015 年便已随 Python 3.5 发布。

采样性能分析器

Python 现有的追踪型性能分析器已从 cProfile 更名为 profiling.tracing,同时新增了一个令人期待的采样性能分析器 Tachyon,即 profiling.sampling。

采样性能分析器会定期捕获运行中进程的堆栈跟踪,再利用这些信息计算各函数的耗时占比。它们的开销非常低,无需事先为系统添加插桩,便能深入洞察运行中的系统。

我此前使用过 py-spy 等工具来完成这件事。现在,这项能力已内置于标准库。

如果你拥有一个现有 Python 3.15 进程的 PID,可以运行以下命令开始收集样本(我在 macOS 上必须使用 sudo):

sudo uv run --python 3.15 python -m profiling.sampling attach 1234

与主程序交互一会儿,然后按 Ctrl+C 停止采集并查看报告,报告大致如下:

Python 采样性能分析器终端输出截图:黑色背景上显示彩色文字。标题行显示,在 2.65 秒内捕获了 2,651 个样本,采样率为 999.98 samples/sec,错误率为 0.19。Profile Stats 表格包含 nsamples、sample%、tottime、cumul%、cumtime 和 filename:lineno(function) 等列;表格显示 99.9% 的样本 (2.644 秒) 直接花费在 selectors.py 的 KqueueSelector.select 中,这意味着进程几乎完全处于空闲状态,正在等待 asyncio 事件循环。表格其余部分列出其下方的调用栈:asyncio 的 base_events.py 中 run_once、run_forever 和 run_until_complete,runners.py 中 Runner.run 和 run,cli.py 中 serve 和 sqlite_extensions wrapped,以及 click 的 core.py 中多个 invoke、main 和 call 栈帧;它们直接耗时均为零,但累计出现率为 100%。图例解释了各列含义,随后“Summary of Interesting Functions”部分列出热点 (KqueueSelector.select 的直接样本占比为 99.9%)、堆栈中最常出现的函数 (run_forever、run_until_complete 和 Runner.run 均为 100%),以及最高调用放大倍数 (BaseEventLoop._run_once 为 2645.0x)。

它还可以生成这样的火焰图和这样的热力图,甚至能够输出可在 Firefox 性能分析器 UI 中查看的格式。

3.15 并没有比 3.14 快多少

Miguel Grinberg 前几天针对 3.15.0rc3 发布版本研究了 Python 3.15 有多快?,发现其性能总体与 3.14 基本相同,实验性 JIT 是例外。JIT 相较于标准 3.15 解释器带来了 1.2 倍加速(在 Fibonacci 测试中),这是 Miguel 首次观察到有实际意义的 JIT 性能提升。

Miguel 去年对 3.14 的研究则发现,相比 Python 3.13,3.14 在更多类别中实现了显著加速。

使用 uv 升级至 Python 3.15

我在 Mac 上使用以下一系列命令,让 uv 使用 Python 3.15。首先,我必须升级 uv 本身,以确保它能够识别这个新版本:

随后,我安装了稳定版 3.15:

然后告诉 uv,除非项目本身固定了其他版本,否则希望它将该版本设为每一条 uv run 命令的默认版本:

uv python pin --global 3.15

现在,以下命令会报告使用的是 3.15:

我已经通过 uv tool install 安装了一批工具。我用下面的方式重新安装它们,让它们改用 Python 3.15:

uv tool upgrade datasette --python 3.15

随后可以这样确认版本:

datasette --get /-/versions.json | jq

Python 3.11 的亮点

3.15 的发布意味着 Python 3.10(发布于 2021 年)已正式不再受支持。

对于我的主要开源项目,我的政策是让它们在每一个受支持的 Python 版本中均能运行。现在我可以放弃 3.10,并开始使用 3.11 引入的功能。

这里是 Python 3.11 新特性文档。我终于可以依赖 tomllib 一定可用了!

Datasette 大量使用 asyncio。3.11 新增了 TaskGroup 和 timeout(),以及更多新功能。

datetime.UTC 是 3.11 中新增的 datetime.timezone.utc 别名。

Python 对 SQLite 的支持在 3.11 中获得了一些重要升级,包括:

  • setlimit() 和 getlimit() 让 Python 可以设置 SQLite 运行时限制。
  • create_window_function() 允许我们用 Python 创建自定义窗口函数,形式是一个包含 step()、value()、inverse() 和 finalize() 方法的类。
  • blobopen() 提供了一种高效机制,可对 SQLite 行中存储的大型 BLOB 二进制值进行部分读取和写入。
  • 现在可以调用 set_authorizer(None) 来移除 SQLite authorizer。Datasette 过去为此专门设置了一个变通方案!

此外,还有大量针对 Python 类型系统的改进。

我计划在未来几周内,将 Python 3.11 改进应用到我的一批项目中。这是我的第一个 Datasette PR,用于移除那个 set_authorizer(None) 变通方案。

我还更新了我的 click-app、datasette-plugin、python-lib 和 llm-plugin cookiecutter 模板,使其测试覆盖 Python 3.11 至 Python 3.15。

English / 原文

Hello Python 3.15, goodbye 3.10, and now I can use 3.11

12th October 2026

Python 3.15 is here! Here’s the essential What’s new in Python 3.15 guide, this year curated by 3.14 and 3.15 release manager Hugo van Kemenade. As someone who builds open source software that works in every supported Python version, I get to say goodbye to Python 3.10 and start embracing features from Python 3.11.

First, some highlights from Python 3.15.

Explicit lazy imports

I’m most excited about Explicit lazy imports from PEP 810. These let you import modules like so:

lazy import json
lazy from pathlib import Path

The module will then be imported the first time those symbols are used in the code that follows.

My LLM CLI tool implements plugins. I had to recommend manual lazy imports in the plugin author documentation, because simply installing plugins that used heavy dependencies such as sentence_transformers could add multiple seconds to every invocation of the llm command. Lazy imports directly address this startup cost.

Unpacking in comprehensions

The new [*sublist for sublist in lists] unpacking syntax is neat. Here’s an illustration lifted from the documentation:

lists = [[1, 2], [3, 4], [5]]
[*L for L in lists]
# [1, 2, 3, 4, 5]
# equivalent to [x for L in lists for x in L]

sets = [{1, 2}, {2, 3}, {3, 4}]
{*s for s in sets}
# {1, 2, 3, 4}
# equivalent to {x for s in sets for x in s}

dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
{**d for d in dicts}
# {'a': 3, 'b': 2}
# equivalent to {k: v for d in dicts for k,v in d.items()}

PEP 798: Unpacking in Comprehensions by Adam Hartz and Erik Demaine proposed this as an extension of the unpacking syntax from PEP 448, which landed in Python 3.5 back in 2015.

A sampling profiler

Python’s existing tracing profiler has been renamed from cProfile to profiling.tracing, and has been joined by an exciting new sampling profiler called Tachyon, aka profiling.sampling.

Sampling profilers work by periodically capturing stack traces from running processes, then using those to calculate which functions have the most time spent inside of them. They’re very low overhead and can provide a deep insight into running systems, without needing to instrument those systems first.

I’ve previously used tools like py-spy for this. Now this capability is baked into the standard library.

If you have the PID of an existing Python 3.15 process you can start gathering samples by running this (I had to use sudo on macOS):

sudo uv run --python 3.15 python -m profiling.sampling attach 1234

Interact with the main program a bit, then hit Ctrl+C to stop gathering and see a report, which looks something like this:

Screenshot of terminal output from a Python sampling profiler with colored text on a black background. Header lines report 2,651 samples captured in 2.65 seconds at a sample rate of 999.98 samples/sec with an error rate of 0.19. A Profile Stats table with columns for nsamples, sample%, tottime, cumul%, cumtime and filename:lineno(function) shows that 99.9% of samples (2.644 seconds) were spent directly in selectors.py KqueueSelector.select, meaning the process was almost entirely idle waiting on the asyncio event loop. The rest of the table lists the call stack beneath it with zero direct time but 100% cumulative presence: asyncio base_events.py run_once, run_forever and run_until_complete, runners.py Runner.run and run, cli.py serve and sqlite_extensions wrapped, and several click core.py invoke, main and call frames. A legend explains each column, followed by a Summary of Interesting Functions section listing hot spots (KqueueSelector.select at 99.9% direct samples), the most frequently present functions on the stack (run_forever, run_until_complete and Runner.run at 100%), and the highest call magnification (2645.0x for BaseEventLoop._run_once).

It can also produce flame graphs like this and heat maps like this, and can even output a format that can be viewed in the Firefox profiler UI.

3.15 isn’t much faster than 3.14

Miguel Grinberg investigated How fast is Python 3.15? the other day against the 3.15.0rc3 release and found performance to be mostly the same as 3.14, with the exception of the experimental JIT. The JIT gave a 1.2x boost over the standard 3.15 interpreter (on a Fibonacci test), the first time Miguel observed a meaningful JIT performance increase.

Miguel’s explorations of 3.14 last year found significant boosts against Python 3.13 across many more categories.

Upgrading to Python 3.15 with uv

I used the following sequence of commands to get uv to use Python 3.15 on my Mac. First I had to upgrade uv itself to ensure it knew about the new release:

Then I installed the stable 3.15 release:

And told uv that I wanted that to be the default for every uv run command, unless the project itself pinned a different version:

uv python pin --global 3.15

Now the following reports 3.15 as the version:

I already have a bunch of tools installed using uv tool install. I reinstalled them to use Python 3.15 like this:

uv tool upgrade datasette --python 3.15

Then I can confirm the version like this:

datasette --get /-/versions.json | jq

Highlights from Python 3.11

The release of 3.15 means that Python 3.10 (released in 2021) is officially no longer supported.

My policy for my main open source projects is to keep them working in every supported Python version. I can now drop 3.10 and start using features that were introduced in 3.11.

Here’s the What’s new in Python 3.11 document. I finally get to rely on tomllib being available!

Datasette makes extensive use of asyncio. 3.11 adds TaskGroup and timeout(), and several more new features.

datetime.UTC is a new in 3.11 alias for datetime.timezone.utc.

Python’s SQLite support got some significant upgrades in 3.11, including:

  • setlimit() and getlimit() expose a mechanism for setting SQLite runtime limits from Python.
  • create_window_function() lets us create custom window functions in Python, as a class with step(), value(), inverse(), and finalize() methods.
  • blobopen() provides an efficient mechanism for partial reads and writes of large BLOB binary values stored in a SQLite row.
  • You can now call set_authorizer(None) to remove a SQLite authorizer. Datasette had a workaround for this!

Plus a whole bunch of improvements to Python’s typing system.

I plan to land Python 3.11 improvements to a bunch of my projects over the next few weeks. Here’s my first Datasette PR removing that set_authorizer(None) workaround.

I’ve also updated my click-app, datasette-plugin, python-lib, and llm-plugin cookiecutter templates to test against Python 3.11 through Python 3.15.