I turned on the Python 3.15 JIT and got 3%. I changed the import lines in a CLI to lazy import and its startup dropped from 76.9 ms to 21.4 ms.
Python 3.15.0 came out on October 9, 2026, and the release page advertises a JIT that is "significantly upgraded," with a 7-8% geometric mean gain on x86-64 Linux. I installed 3.13, 3.14 and 3.15 side by side, ran the same scripts on each, and wrote down what got faster and what broke.
Short version: pure-Python code runs about 15% faster than on 3.13 with the JIT off. Lazy imports cut CLI startup by 72%. The global lazy switch broke pandas. My pandas workload got slightly slower, torch still has no 3.15 wheels, and PyYAML installed "successfully" and then parsed 4x slower.
The setup
Everything ran on a 4-vCPU VM (AMD EPYC 7F32) with uv 0.13.0:
uv python install 3.13 3.14 3.15
# -> cpython-3.13.16, cpython-3.14.8, cpython-3.15.0
These are the python-build-standalone builds that uv downloads. The 3.14 and 3.15 builds include the JIT but leave it off; sys._jit.is_available() returns True and sys._jit.is_enabled() returns False. I turned it on with PYTHON_JIT=1. Each CPU number below is the best of 5 runs inside one process, averaged over two full passes. The two passes agreed within 4%.
Lazy imports: 76.9 ms down to 21.4 ms
PEP 810 adds lazy as a soft keyword. A module marked lazy isn't loaded until you first use the name:
lazy import json
lazy from pathlib import Path
My test case is the most common shape of a slow CLI. It imports asyncio, http.client, urllib.request, sqlite3, email.message, tarfile, zipfile, decimal, xml.etree.ElementTree and a few other modules at the top, then exits early on --version. Times are the median of 40 process launches:
| Run | 3.13 | 3.14 | 3.15 |
|---|---|---|---|
python -c pass |
9.9 ms | 10.0 ms | 10.1 ms |
| CLI, plain imports | 75.1 ms | 80.7 ms | 76.9 ms |
CLI, lazy import
|
SyntaxError | SyntaxError | 21.4 ms |
CLI, plain imports + -X lazy_imports=all
|
n/a | n/a | 20.5 ms |
That's a 72% cut from changing import lines and nothing else. python -X importtime showed why: asyncio alone took around 30 ms of the eager startup, and the lazy version's import log was less than a third as long.
The catch is the SyntaxError cells. As soon as lazy import appears in a file, that file won't parse on 3.14 or older. If you still support older versions, use the escape hatch from the PEP, __lazy_modules__ = ["json", "pathlib"] at module level. 3.15 treats those plain imports as lazy, and older versions ignore the line.
-X lazy_imports=all broke pandas at first use
The global switch looked like free speed. Then I ran a pandas script with it:
$ python -X lazy_imports=all -c "import pandas as pd; print(pd.DataFrame({'a':[1]}))"
ImportError: lazy import of 'pandas._typing.NDFrameT' raised an exception during resolution
...
TypeError: 'lazy_import' object is not subscriptable
import pandas by itself succeeds, because nothing has loaded yet. The failure shows up later, at first use, deep inside pandas' typing module. requests and numpy were fine in my quick test. This is the import-time side-effects problem people raised in the HN thread when someone asked why lazy isn't the default.
A filter fixes it, but excluding pandas alone wasn't enough, and with pandas and numpy excluded pyarrow failed next with <lazy_import 'pyarrow._compute.ScalarAggregateOptions'> is not a callable object. This version worked:
import sys
sys.set_lazy_imports_filter(
lambda importing, imported, fromlist:
not imported.startswith(("pandas", "numpy", "pyarrow")))
sys.set_lazy_imports("all")
My advice is to use the lazy keyword in your own entry points and keep all away from production until your dependencies have tested against it.
CPU-bound code: 15% faster than 3.13, and the JIT adds 3%
These are small pure-Python kernels, times in ms (lower is better):
| Benchmark | 3.13 | 3.14 | 3.14 JIT | 3.15 | 3.15 JIT |
|---|---|---|---|---|---|
| nbody | 897 | 854 | 1203 | 878 | 698 |
| fib(30) | 105 | 92 | 91 | 90 | 92 |
| spectral_norm | 485 | 350 | 360 | 342 | 321 |
| string/dict | 695 | 706 | 751 | 690 | 662 |
| object alloc | 755 | 504 | 527 | 528 | 596 |
| sieve | 379 | 424 | 441 | 338 | 337 |
Geometric means from that table:
- 3.15 vs 3.13, no JIT: 15% less time
- 3.15 vs 3.14, no JIT: about 4% less time
- 3.15 with the JIT vs without: about 3% less time
- 3.14 with the JIT vs without: about 9% more time
The JIT on 3.15 is clearly better than the one in 3.14, which slowed down nbody by 41%. On 3.15 it makes nbody 20% faster, but object allocation gets 13% slower. The What's New page claims a 7-8% pyperformance geomean on x86-64 Linux (linking to this benchmark run) and says per-benchmark results range "from roughly 15% slowdown to over 100% speedup." My six kernels came in well under that geomean, though my spread (object alloc 13% slower, nbody 20% faster) fits inside their range. If you plan to set PYTHON_JIT=1 in production, benchmark your own hot path first.
Most of the gain over 3.13 had already arrived in 3.14 (spectral_norm 485 → 350, object alloc 755 → 504). On sieve, 3.14 was slower than 3.13 and 3.15 won it back.
A pandas + requests script got slower
Next I ran something closer to real work. requests fetched JSON from a local HTTP server 50 times, the results went into DataFrames, and pandas ran groupby, transform and string ops over 3M rows. Package versions were the same on all three: pandas 3.0.6, numpy 2.5.4, requests 2.34.2.
| 3.13 | 3.14 | 3.15 | |
|---|---|---|---|
| pandas section (3 runs) | 3.75–3.78 s | 3.93–3.99 s | 3.97–4.10 s |
| HTTP + JSON | ~0.26 s | ~0.27 s | ~0.26 s |
import pandas, numpy, requests (warm) |
~300 ms | ~295 ms | ~310 ms |
On this workload 3.15 was 5-9% slower than 3.13. That could be noise or differences between the wheel builds, and I wouldn't file a bug over it. If your time goes to compiled code, a faster interpreter loop doesn't help.
torch, tensorflow and four others won't install
This table matters more than the benchmarks for deciding whether to upgrade. I ran uv pip install --no-build (wheels only) against 3.14 and 3.15:
| Package | 3.14 | 3.15 |
|---|---|---|
| pandas, numpy, pyarrow, pydantic, lxml, orjson | yes | yes (native cp315 wheels) |
polars (abi3 wheel), requests (pure Python) |
yes | yes |
| scipy, scikit-learn, matplotlib, pillow, numba, grpcio, uvloop, cryptography, psycopg2-binary | yes | yes |
| torch 2.14.1 | yes | no, wheels stop at cp314
|
| onnxruntime 1.31.0 | yes | no cp315 wheel |
| tensorflow | only 2.22.0rc0 resolved | no, 2.21.0 wheels stop at cp313
|
| spacy 3.8.16 | yes | no usable wheels |
| gevent 26.9.0 | yes | no, it has cp315 wheels but its zope.interface dependency doesn't |
| pyyaml 6.0.3 | yes | no wheel, sdist only |
PyYAML is the nasty one, because nothing fails. Without --no-build, uv quietly builds it from source on 3.15, and on my box the build came out without libyaml. yaml.__with_libyaml__ was True on 3.14 and False on 3.15, so CSafeLoader doesn't exist. My loader fell back to yaml.SafeLoader, and parsing a 20,000-item document went from 1.07 s to 4.64 s, with every install step having printed success.
The profiler, UTF-8 and the removals
The new sampling profiler (Tachyon) works out of the box and needed no sudo when it launched the target itself:
python3.15 -m profiling.sampling run cpu.py spectral_norm
It printed a pstats-style table and a hot-spot summary. attach <pid>, --live and --flamegraph are also there. profile is deprecated (removal planned for 3.17).
UTF-8 by default (PEP 686) didn't change anything on this Linux box. Even with LC_ALL=POSIX, 3.13 already ran in UTF-8 mode, because the C/POSIX locale has triggered it for years. Windows machines are where encodings will change. If you need the old behavior, use PYTHONUTF8=0 or -X utf8=0. What's New also lists removals that will catch someone: sre_compile/sre_parse, http.server's CGIHTTPRequestHandler, keyword-argument NamedTuple("Point", x=int), and __cached__ no longer being set.
Should you upgrade?
- CLI tools and pure-Python services: yes. You get roughly 15% over 3.13 for free, and lazy imports can bring cold starts from tens of milliseconds to near the bare interpreter.
- Data stacks on pandas, numpy and pydantic: it installs cleanly. Just don't expect a speedup.
- Anything that touches torch, onnxruntime, tensorflow, spacy or gevent: wait. No wheels means no upgrade, and for torch the version of Python you can run is decided by its release schedule.
Before you switch, run this against your lockfile:
uv venv -p 3.15 /tmp/py315 && VIRTUAL_ENV=/tmp/py315 uv pip install --no-build -r requirements.txt
If it fails, you have your list of blockers. If it succeeds, check every C extension you depend on for the PyYAML problem.
What's keeping you off 3.15: a missing wheel, or that the JIT doesn't buy enough yet? And has anyone run -X lazy_imports=all on a real codebase without hitting something like the pandas failure?
Top comments (0)