{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/simonwillison-net/2026-10-12-hello-python-3-15-goodbye-3-10-and-now-i-can-use-3-11/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "Python 3.15 [is here](https://www.python.org/downloads/release/python-3150/)! Here’s the essential [What’s new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html) 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.\n\nFirst, some highlights from Python 3.15.\n\n#### Explicit lazy imports\n\nI’m most excited about [Explicit lazy imports](https://docs.python.org/3.15/whatsnew/3.15.html#pep-810-explicit-lazy-imports) from [PEP 810](https://peps.python.org/pep-0810/). These let you import modules like so:\n\n```\nlazy import json\nlazy from pathlib import Path\n```\n\nThe module will then be imported the first time those symbols are used in the code that follows.\n\nMy [LLM](https://llm.datasette.io/) CLI tool implements plugins. I had to [recommend manual lazy imports](https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#tip-lazily-load-expensive-dependencies) 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.\n\n#### Unpacking in comprehensions\n\nThe new `[*sublist for sublist in lists]` unpacking syntax is neat. Here’s an illustration [lifted from the documentation](https://docs.python.org/3.15/whatsnew/3.15.html#pep-798-unpacking-in-comprehensions):\n\n```\nlists = [[1, 2], [3, 4], [5]]\n[*L for L in lists]\n# [1, 2, 3, 4, 5]\n# equivalent to [x for L in lists for x in L]\n\nsets = [{1, 2}, {2, 3}, {3, 4}]\n{*s for s in sets}\n# {1, 2, 3, 4}\n# equivalent to {x for s in sets for x in s}\n\ndicts = [{'a': 1}, {'b': 2}, {'a': 3}]\n{**d for d in dicts}\n# {'a': 3, 'b': 2}\n# equivalent to {k: v for d in dicts for k,v in d.items()}\n```\n\n[PEP 798: Unpacking in Comprehensions](https://peps.python.org/pep-0798/) by Adam Hartz and Erik Demaine proposed this as an extension of the unpacking syntax [from PEP 448](https://peps.python.org/pep-0448/), which landed [in Python 3.5](https://docs.python.org/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations) back in 2015.\n\n#### A sampling profiler\n\nPython’s existing tracing profiler has been renamed from `cProfile` to [profiling.tracing](https://docs.python.org/3.15/library/profiling.tracing.html#module-profiling.tracing), and has been joined by an exciting new sampling profiler called Tachyon, aka [profiling.sampling](https://docs.python.org/3.15/whatsnew/3.15.html#tachyon-high-frequency-statistical-sampling-profiler).\n\nSampling 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.\n\nI’ve [previously used tools](https://simonwillison.net/tags/profiling/) like [py-spy](https://github.com/benfred/py-spy) for this. Now this capability is baked into the standard library.\n\nIf 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):\n\n```shell\nsudo uv run --python 3.15 python -m profiling.sampling attach 1234\n```\n\nInteract with the main program a bit, then hit Ctrl+C to stop gathering and see a report, which looks something like this:\n\n![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).](https://static.simonwillison.net/static/2026/profiler-output.webp)\n\nIt can also produce [flame graphs like this](https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/flamegraph.html) and [heat maps like this](https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/heatmap/index.html), and can even output a format that can be viewed in the Firefox profiler UI.\n\n#### 3.15 isn’t much faster than 3.14\n\nMiguel Grinberg investigated [How fast is Python 3.15?](https://blog.miguelgrinberg.com/post/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.\n\nMiguel’s [explorations of 3.14](https://blog.miguelgrinberg.com/post/python-3-14-is-here-how-fast-is-it) last year found significant boosts against Python 3.13 across many more categories.\n\n#### Upgrading to Python 3.15 with uv\n\nI 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:\n\n```shell\nuv self update\n```\n\nThen I installed the stable 3.15 release:\n\n```shell\nuv python install 3.15\n```\n\nAnd told `uv` that I wanted that to be the default for every `uv run` command, unless the project itself pinned a different version:\n\n```shell\nuv python pin --global 3.15\n```\n\nNow the following reports 3.15 as the version:\n\n```shell\nuv run python --version\n```\n\nI already have a bunch of tools installed using `uv tool install`. I reinstalled them to use Python 3.15 like this:\n\n```shell\nuv tool upgrade datasette --python 3.15\n```\n\nThen I can confirm the version like this:\n\n```shell\ndatasette --get /-/versions.json | jq\n```\n\n#### Highlights from Python 3.11\n\nThe release of 3.15 means that Python 3.10 (released in 2021) is officially [no longer supported](https://devguide.python.org/versions/).\n\nMy 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.\n\nHere’s the [What’s new in Python 3.11](https://docs.python.org/3/whatsnew/3.11.html) document. I finally get to rely on [tomllib](https://docs.python.org/3/library/tomllib.html#module-tomllib) being available!\n\nDatasette makes extensive use of `asyncio`. 3.11 adds [TaskGroup](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup) and [timeout()](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout), and [several more](https://docs.python.org/3/whatsnew/3.11.html#asyncio) new features.\n\n`datetime.UTC` is a new in 3.11 alias for `datetime.timezone.utc`.\n\nPython’s SQLite support got some significant upgrades in 3.11, including:\n\n- [setlimit()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.setlimit) and [getlimit()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.getlimit) expose a mechanism for setting [SQLite runtime limits](https://www.sqlite.org/c3ref/c_limit_attached.html) from Python.\n- [create\\_window\\_function()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_window_function) lets us create custom window functions in Python, as a class with `step()`, `value()`, `inverse()`, and `finalize()` methods.\n- [blobopen()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.blobopen) provides an efficient mechanism for partial reads and writes of large `BLOB` binary values stored in a SQLite row.\n- You can now call `set_authorizer(None)` to remove a SQLite authorizer. Datasette had a workaround for this!\n\nPlus a whole bunch of [improvements to Python’s typing system](https://docs.python.org/3/whatsnew/3.11.html#new-feat-related-type-hints-311).\n\nI plan to land Python 3.11 improvements to a bunch of my projects over the next few weeks. Here’s [my first Datasette PR](https://github.com/simonw/datasette/pull/2998) removing that `set_authorizer(None)` workaround.\n\nI’ve also updated my [click-app](https://github.com/simonw/click-app), [datasette-plugin](https://github.com/simonw/datasette-plugin), [python-lib](https://github.com/simonw/python-lib), and [llm-plugin](https://github.com/simonw/llm-plugin) cookiecutter templates to test against Python 3.11 through Python 3.15.\n",
  "authors": [],
  "captured_at": "2026-10-12T22:35:36.108955126+00:00",
  "category": "programming",
  "content_html": "<p>Python 3.15 <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.python.org/downloads/release/python-3150/\">is here</a>! Here’s the essential <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3.15/whatsnew/3.15.html\">What’s new in Python 3.15</a> 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.</p>\n<p>First, some highlights from Python 3.15.</p>\n<h4>Explicit lazy imports</h4>\n<p>I’m most excited about <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3.15/whatsnew/3.15.html#pep-810-explicit-lazy-imports\">Explicit lazy imports</a> from <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://peps.python.org/pep-0810/\">PEP 810</a>. These let you import modules like so:</p>\n<pre><code><span class=\"code-snippet\">lazy import json\nlazy from pathlib import Path\n</span></code></pre>\n<p>The module will then be imported the first time those symbols are used in the code that follows.</p>\n<p>My <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://llm.datasette.io/\">LLM</a> CLI tool implements plugins. I had to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#tip-lazily-load-expensive-dependencies\">recommend manual lazy imports</a> in the plugin author documentation, because simply installing plugins that used heavy dependencies such as <code>sentence_transformers</code> could add multiple seconds to every invocation of the <code>llm</code> command. Lazy imports directly address this startup cost.</p>\n<h4>Unpacking in comprehensions</h4>\n<p>The new <code>[*sublist for sublist in lists]</code> unpacking syntax is neat. Here’s an illustration <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3.15/whatsnew/3.15.html#pep-798-unpacking-in-comprehensions\">lifted from the documentation</a>:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"TOML\">lists = [[1, 2], [3, 4], [5]]\n[*L for L in lists]\n# [1, 2, 3, 4, 5]\n# equivalent to [x for L in lists for x in L]\n\nsets = [{1, 2}, {2, 3}, {3, 4}]\n{*s for s in sets}\n# {1, 2, 3, 4}\n# equivalent to {x for s in sets for x in s}\n\ndicts = [{&#39;a&#39;: 1}, {&#39;b&#39;: 2}, {&#39;a&#39;: 3}]\n{**d for d in dicts}\n# {&#39;a&#39;: 3, &#39;b&#39;: 2}\n# equivalent to {k: v for d in dicts for k,v in d.items()}\n</span></code></pre>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://peps.python.org/pep-0798/\">PEP 798: Unpacking in Comprehensions</a> by Adam Hartz and Erik Demaine proposed this as an extension of the unpacking syntax <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://peps.python.org/pep-0448/\">from PEP 448</a>, which landed <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations\">in Python 3.5</a> back in 2015.</p>\n<h4>A sampling profiler</h4>\n<p>Python’s existing tracing profiler has been renamed from <code>cProfile</code> to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3.15/library/profiling.tracing.html#module-profiling.tracing\">profiling.tracing</a>, and has been joined by an exciting new sampling profiler called Tachyon, aka <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3.15/whatsnew/3.15.html#tachyon-high-frequency-statistical-sampling-profiler\">profiling.sampling</a>.</p>\n<p>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.</p>\n<p>I’ve <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://simonwillison.net/tags/profiling/\">previously used tools</a> like <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/benfred/py-spy\">py-spy</a> for this. Now this capability is baked into the standard library.</p>\n<p>If you have the PID of an existing Python 3.15 process you can start gathering samples by running this (I had to use <code>sudo</code> on macOS):</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">sudo</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> uv run<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>python</span> 3.15 python<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>m</span> profiling.sampling attach 1234</span>\n</span></span></code></pre>\n<p>Interact with the main program a bit, then hit Ctrl+C to stop gathering and see a report, which looks something like this:</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://static.simonwillison.net/static/2026/profiler-output.webp\" alt=\"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).\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>It can also produce <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/flamegraph.html\">flame graphs like this</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/heatmap/index.html\">heat maps like this</a>, and can even output a format that can be viewed in the Firefox profiler UI.</p>\n<h4>3.15 isn’t much faster than 3.14</h4>\n<p>Miguel Grinberg investigated <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://blog.miguelgrinberg.com/post/how-fast-is-python-3-15\">How fast is Python 3.15?</a> 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.</p>\n<p>Miguel’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://blog.miguelgrinberg.com/post/python-3-14-is-here-how-fast-is-it\">explorations of 3.14</a> last year found significant boosts against Python 3.13 across many more categories.</p>\n<h4>Upgrading to Python 3.15 with uv</h4>\n<p>I used the following sequence of commands to get <code>uv</code> to use Python 3.15 on my Mac. First I had to upgrade <code>uv</code> itself to ensure it knew about the new release:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> self update</span>\n</span></span></code></pre>\n<p>Then I installed the stable 3.15 release:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> python install 3.15</span>\n</span></span></code></pre>\n<p>And told <code>uv</code> that I wanted that to be the default for every <code>uv run</code> command, unless the project itself pinned a different version:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> python pin<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>global</span> 3.15</span>\n</span></span></code></pre>\n<p>Now the following reports 3.15 as the version:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> run python<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>version</span></span>\n</span></span></code></pre>\n<p>I already have a bunch of tools installed using <code>uv tool install</code>. I reinstalled them to use Python 3.15 like this:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> tool upgrade datasette<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>python</span> 3.15</span>\n</span></span></code></pre>\n<p>Then I can confirm the version like this:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">datasette</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>get</span> /-/versions.json</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">jq</span></span>\n</span></span></code></pre>\n<h4>Highlights from Python 3.11</h4>\n<p>The release of 3.15 means that Python 3.10 (released in 2021) is officially <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://devguide.python.org/versions/\">no longer supported</a>.</p>\n<p>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.</p>\n<p>Here’s the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/whatsnew/3.11.html\">What’s new in Python 3.11</a> document. I finally get to rely on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/tomllib.html#module-tomllib\">tomllib</a> being available!</p>\n<p>Datasette makes extensive use of <code>asyncio</code>. 3.11 adds <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup\">TaskGroup</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout\">timeout()</a>, and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/whatsnew/3.11.html#asyncio\">several more</a> new features.</p>\n<p><code>datetime.UTC</code> is a new in 3.11 alias for <code>datetime.timezone.utc</code>.</p>\n<p>Python’s SQLite support got some significant upgrades in 3.11, including:</p>\n<ul>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.setlimit\">setlimit()</a> and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.getlimit\">getlimit()</a> expose a mechanism for setting <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.sqlite.org/c3ref/c_limit_attached.html\">SQLite runtime limits</a> from Python.</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_window_function\">create_window_function()</a> lets us create custom window functions in Python, as a class with <code>step()</code>, <code>value()</code>, <code>inverse()</code>, and <code>finalize()</code> methods.</li>\n<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.blobopen\">blobopen()</a> provides an efficient mechanism for partial reads and writes of large <code>BLOB</code> binary values stored in a SQLite row.</li>\n<li>You can now call <code>set_authorizer(None)</code> to remove a SQLite authorizer. Datasette had a workaround for this!</li>\n</ul>\n<p>Plus a whole bunch of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.python.org/3/whatsnew/3.11.html#new-feat-related-type-hints-311\">improvements to Python’s typing system</a>.</p>\n<p>I plan to land Python 3.11 improvements to a bunch of my projects over the next few weeks. Here’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simonw/datasette/pull/2998\">my first Datasette PR</a> removing that <code>set_authorizer(None)</code> workaround.</p>\n<p>I’ve also updated my <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simonw/click-app\">click-app</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simonw/datasette-plugin\">datasette-plugin</a>, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simonw/python-lib\">python-lib</a>, and <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/simonw/llm-plugin\">llm-plugin</a> cookiecutter templates to test against Python 3.11 through Python 3.15.</p>\n",
  "content_markdown": "Python 3.15 [is here](https://www.python.org/downloads/release/python-3150/)! Here’s the essential [What’s new in Python 3.15](https://docs.python.org/3.15/whatsnew/3.15.html) 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.\n\nFirst, some highlights from Python 3.15.\n\n#### Explicit lazy imports\n\nI’m most excited about [Explicit lazy imports](https://docs.python.org/3.15/whatsnew/3.15.html#pep-810-explicit-lazy-imports) from [PEP 810](https://peps.python.org/pep-0810/). These let you import modules like so:\n\n```\nlazy import json\nlazy from pathlib import Path\n```\n\nThe module will then be imported the first time those symbols are used in the code that follows.\n\nMy [LLM](https://llm.datasette.io/) CLI tool implements plugins. I had to [recommend manual lazy imports](https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html#tip-lazily-load-expensive-dependencies) 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.\n\n#### Unpacking in comprehensions\n\nThe new `[*sublist for sublist in lists]` unpacking syntax is neat. Here’s an illustration [lifted from the documentation](https://docs.python.org/3.15/whatsnew/3.15.html#pep-798-unpacking-in-comprehensions):\n\n```\nlists = [[1, 2], [3, 4], [5]]\n[*L for L in lists]\n# [1, 2, 3, 4, 5]\n# equivalent to [x for L in lists for x in L]\n\nsets = [{1, 2}, {2, 3}, {3, 4}]\n{*s for s in sets}\n# {1, 2, 3, 4}\n# equivalent to {x for s in sets for x in s}\n\ndicts = [{'a': 1}, {'b': 2}, {'a': 3}]\n{**d for d in dicts}\n# {'a': 3, 'b': 2}\n# equivalent to {k: v for d in dicts for k,v in d.items()}\n```\n\n[PEP 798: Unpacking in Comprehensions](https://peps.python.org/pep-0798/) by Adam Hartz and Erik Demaine proposed this as an extension of the unpacking syntax [from PEP 448](https://peps.python.org/pep-0448/), which landed [in Python 3.5](https://docs.python.org/3/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations) back in 2015.\n\n#### A sampling profiler\n\nPython’s existing tracing profiler has been renamed from `cProfile` to [profiling.tracing](https://docs.python.org/3.15/library/profiling.tracing.html#module-profiling.tracing), and has been joined by an exciting new sampling profiler called Tachyon, aka [profiling.sampling](https://docs.python.org/3.15/whatsnew/3.15.html#tachyon-high-frequency-statistical-sampling-profiler).\n\nSampling 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.\n\nI’ve [previously used tools](https://simonwillison.net/tags/profiling/) like [py-spy](https://github.com/benfred/py-spy) for this. Now this capability is baked into the standard library.\n\nIf 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):\n\n```shell\nsudo uv run --python 3.15 python -m profiling.sampling attach 1234\n```\n\nInteract with the main program a bit, then hit Ctrl+C to stop gathering and see a report, which looks something like this:\n\n![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).](https://static.simonwillison.net/static/2026/profiler-output.webp)\n\nIt can also produce [flame graphs like this](https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/flamegraph.html) and [heat maps like this](https://simonw.github.io/research/python-3-15-web-edit-profiling/artifacts/heatmap/index.html), and can even output a format that can be viewed in the Firefox profiler UI.\n\n#### 3.15 isn’t much faster than 3.14\n\nMiguel Grinberg investigated [How fast is Python 3.15?](https://blog.miguelgrinberg.com/post/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.\n\nMiguel’s [explorations of 3.14](https://blog.miguelgrinberg.com/post/python-3-14-is-here-how-fast-is-it) last year found significant boosts against Python 3.13 across many more categories.\n\n#### Upgrading to Python 3.15 with uv\n\nI 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:\n\n```shell\nuv self update\n```\n\nThen I installed the stable 3.15 release:\n\n```shell\nuv python install 3.15\n```\n\nAnd told `uv` that I wanted that to be the default for every `uv run` command, unless the project itself pinned a different version:\n\n```shell\nuv python pin --global 3.15\n```\n\nNow the following reports 3.15 as the version:\n\n```shell\nuv run python --version\n```\n\nI already have a bunch of tools installed using `uv tool install`. I reinstalled them to use Python 3.15 like this:\n\n```shell\nuv tool upgrade datasette --python 3.15\n```\n\nThen I can confirm the version like this:\n\n```shell\ndatasette --get /-/versions.json | jq\n```\n\n#### Highlights from Python 3.11\n\nThe release of 3.15 means that Python 3.10 (released in 2021) is officially [no longer supported](https://devguide.python.org/versions/).\n\nMy 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.\n\nHere’s the [What’s new in Python 3.11](https://docs.python.org/3/whatsnew/3.11.html) document. I finally get to rely on [tomllib](https://docs.python.org/3/library/tomllib.html#module-tomllib) being available!\n\nDatasette makes extensive use of `asyncio`. 3.11 adds [TaskGroup](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup) and [timeout()](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout), and [several more](https://docs.python.org/3/whatsnew/3.11.html#asyncio) new features.\n\n`datetime.UTC` is a new in 3.11 alias for `datetime.timezone.utc`.\n\nPython’s SQLite support got some significant upgrades in 3.11, including:\n\n- [setlimit()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.setlimit) and [getlimit()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.getlimit) expose a mechanism for setting [SQLite runtime limits](https://www.sqlite.org/c3ref/c_limit_attached.html) from Python.\n- [create\\_window\\_function()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_window_function) lets us create custom window functions in Python, as a class with `step()`, `value()`, `inverse()`, and `finalize()` methods.\n- [blobopen()](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.blobopen) provides an efficient mechanism for partial reads and writes of large `BLOB` binary values stored in a SQLite row.\n- You can now call `set_authorizer(None)` to remove a SQLite authorizer. Datasette had a workaround for this!\n\nPlus a whole bunch of [improvements to Python’s typing system](https://docs.python.org/3/whatsnew/3.11.html#new-feat-related-type-hints-311).\n\nI plan to land Python 3.11 improvements to a bunch of my projects over the next few weeks. Here’s [my first Datasette PR](https://github.com/simonw/datasette/pull/2998) removing that `set_authorizer(None)` workaround.\n\nI’ve also updated my [click-app](https://github.com/simonw/click-app), [datasette-plugin](https://github.com/simonw/datasette-plugin), [python-lib](https://github.com/simonw/python-lib), and [llm-plugin](https://github.com/simonw/llm-plugin) cookiecutter templates to test against Python 3.11 through Python 3.15.\n",
  "dateCreated": "2026-10-12T22:35:36.108955126+00:00",
  "date_created": "2026-10-12T18:15:38+00:00",
  "date_modified": "2026-10-12T18:15:38+00:00",
  "date_published": "2026-10-12T18:15:38+00:00",
  "description": "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…",
  "external_url": "https://simonwillison.net/2026/Oct/12/python-3-15/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/simonwillison-net/2026/10/2026-10-12-hello-python-3-15-goodbye-3-10-and-now-i-can-use-3-11.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/simonwillison-net/2026/10/2026-10-12-hello-python-3-15-goodbye-3-10-and-now-i-can-use-3-11.md"
  },
  "id": "urn:aggr:item:9f89d4b34fd0f44df7fee5bec5770584526c2487",
  "isBasedOn": {
    "@id": "https://simonwillison.net/2026/Oct/12/python-3-15/"
  },
  "mainEntity": {
    "@id": "https://simonwillison.net/2026/Oct/12/python-3-15/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/simonwillison-net/2026-10-12-hello-python-3-15-goodbye-3-10-and-now-i-can-use-3-11/#webpage"
    },
    "dateModified": "2026-10-12T18:15:38+00:00",
    "datePublished": "2026-10-12T18:15:38+00:00",
    "headline": "Hello Python 3.15, goodbye 3.10, and now I can use 3.11",
    "image": {
      "@type": "ImageObject",
      "caption": "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, cumu",
      "height": 90,
      "url": "https://static.simonwillison.net/static/2026/profiler-output.webp",
      "width": 160
    },
    "timeRequired": "PT5M",
    "url": "https://simonwillison.net/2026/Oct/12/python-3-15/",
    "wordCount": 912
  },
  "name": "Archived snapshot: Hello Python 3.15, goodbye 3.10, and now I can use 3.11",
  "preview": {
    "alt": "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, cumu",
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://static.simonwillison.net/static/2026/profiler-output.webp",
    "width": 160
  },
  "reading_minutes": 5,
  "replicated_at": null,
  "source": "simonwillison-net",
  "source_name": "Simon Willison's Weblog",
  "summary": "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: 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: uv self update Then I installed the stable 3.15 release: uv python install 3.15 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: uv run python --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. Tags: open-source, profiling, python, sqlite, uv",
  "tags": [
    "open-source",
    "profiling",
    "python",
    "sqlite",
    "uv"
  ],
  "temporalCoverage": "2026-10-12T22:35:36.108955126+00:00",
  "title": "Hello Python 3.15, goodbye 3.10, and now I can use 3.11",
  "url": "https://aggr.aymericbeaumet.com/items/simonwillison-net/2026-10-12-hello-python-3-15-goodbye-3-10-and-now-i-can-use-3-11/",
  "word_count": 912
}