Skip to content

Python 3.15 Upgrade Guide: Install with uv, Use Lazy Imports, and Check Your Data Stack

Updated on

Install Python 3.15 in one uv command, then check whether numpy, pandas, PyTorch, DuckDB, and Polars actually install on it. Tested on macOS: a wheel compatibility table, lazy imports that cut a script's startup from 2.7 s to 0.01 s, the three places lazy import is a SyntaxError, and the free-threaded build.

The short answer: Python 3.15.0 was released on October 9, 2026. If you use uv (opens in a new tab), upgrade uv first, then install:

uv self update
uv python install 3.15

Then make a test project and try your real dependencies before you move anything important:

uv init --python 3.15 py315-test
cd py315-test
uv add numpy pandas scikit-learn matplotlib

The core scientific stack installs from ready-made wheels today. PyTorch and TensorFlow do not. DuckDB compiles from source. If your project depends on any of those, stay on 3.14 for now (uv python pin 3.14).

The feature worth upgrading for is lazy imports. One keyword, lazy import pandas as pd, took a test script's --help path from 2.7 seconds to 0.01 seconds.

What was tested: everything marked "tested" below was run on October 11, 2026, on an Apple M4 Max (macOS) with uv 0.13.0 and CPython 3.15.0 / 3.15.0t from uv python install. Windows and Linux behavior, the python.org installer, pyenv, and conda come from the official docs and were not tested.

What changed in 3.15 that you will notice

The changelog is long. If you write data scripts or CLIs, these five changes are the ones you will run into:

ChangeWhat it means for youAction
Lazy imports (PEP 810)lazy import x defers loading until first useUse it in CLIs, scripts, and slow-starting apps
UTF-8 is the default encoding (PEP 686)open("file.txt") now reads and writes UTF-8 on every OSWindows users with cp1252 / cp932 / GBK files: pass encoding= explicitly
uv 0.13 defaults to 3.15New uv projects with no pin get 3.15 automaticallyPin 3.14 where your dependencies are not ready
Free-threaded build gets a stable ABI (PEP 803)python3.15t runs threads in parallel, but most packages do not ship stable-ABI wheels for it yetExpect source builds on 3.15t
frozendict, sentinel built-ins, profiling packageNew built-in types and the Tachyon sampling profilerOptional; nothing breaks if you ignore them

The full list is in the official What's New in Python 3.15 (opens in a new tab). The rest of this guide covers the tasks: install, check compatibility, use lazy imports, handle the encoding change, and decide on free-threading.

Step 1: Install Python 3.15

With uv (tested)

Update uv before you install. Skipping this is the easiest mistake to make this week. An older uv only knows the alpha builds. On the uv 0.9.25 we already had installed, uv python list 3.15 showed only cpython-3.15.0a4. Running uv python install 3.15 there would install an alpha, not the release.

uv self update          # needs 0.13.0 or newer
uv --version
uv python install 3.15

uv 0.13.0 installed both the regular and free-threaded builds in 24.8 seconds:

uv python install 3.15 3.15t
Installed 2 versions in 24.83s
 + cpython-3.15.0+freethreaded-macos-aarch64-none (python3.15t)
 + cpython-3.15.0-macos-aarch64-none (python3.15)

If you installed uv with Homebrew, pip, or pipx, uv self update does not work. Upgrade it the same way you installed it (brew upgrade uv, pipx upgrade uv).

uv 0.13 also changes the default. The uv 0.13.0 release notes (opens in a new tab) say that uv python install with no version now downloads 3.15. A new uv venv or uvx python downloads 3.15 if no compatible Python is already installed. Existing installs are still used. To keep a project on 3.14, record it:

uv python pin 3.14

That writes .python-version, which uv reads every time it runs in that folder.

Other ways (from official docs, not tested here)

  • python.org installer (Windows, macOS): download 3.15.0 from python.org/downloads (opens in a new tab). According to the release notes, the official macOS installer now installs free-threading support by default, and the Windows 64-bit binaries use the new tail-calling interpreter.
  • pyenv: pyenv update, then pyenv install 3.15.0. Wait until your pyenv version lists 3.15.0.
  • conda: create an environment with python=3.15 once conda-forge has published it. Check before you plan a migration around it.

If you want the general picture of switching Python versions on each OS, our older guide to upgrading Python and the Python version manager comparison still apply. This page covers what is specific to 3.15.

Step 2: Check whether your packages install

This is the step that decides whether you can upgrade today. We created a clean 3.15 project and installed a typical data-science stack, then checked PyPI for the packages people ask about most.

Compatibility table (tested October 11, 2026)

Package (version)Python 3.15Python 3.15t (free-threaded)Notes
numpy 2.5.4WheelWheel
pandas 3.0.6WheelWheel
scipy 1.18.1WheelWheel
scikit-learn 1.9.1WheelWheel
matplotlib 3.11.2WheelWheel
pyarrow 26.0.0WheelWheel
polars 2.0.0Wheel (abi3)Builds from sourceIts runtime ships only cp310-abi3 wheels, which free-threaded Python cannot use. The Rust build took 6 min 51 s
ipykernel 9.17.1 (pyzmq, psutil, debugpy)WheelNot testedJupyter kernels work on 3.15
duckdb 1.5.6Builds from sourceNot testedWheels stop at cp314. The C++ build took about 2.5 min
pygwalker 0.5.0.1WorksNot testedPulls duckdb, so it inherits the source build
torch 2.14.1Cannot installCannot installWheels exist only for cp310 to cp314 and cp314t
tensorflow 2.21.0Cannot installCannot installWheels exist only for cp310 to cp313

Installing numpy, pandas, scipy, scikit-learn, matplotlib, pyarrow, and polars took 71 seconds on 3.15, download included, with no compilation. The same set on 3.15t took 6 minutes 51 seconds, almost all of it spent compiling Polars.

When a package has no wheel, uv tells you exactly why. This is what PyTorch looks like on 3.15:

hint: You require CPython 3.15 (`cp315`), but we only found wheels for `torch`
(v2.14.1) with the following Python ABI tags: `cp310`, `cp311`, `cp312`, `cp313`, `cp314`, `cp314t`

Check your own project in one minute

You do not need to install anything to find out. Run a dry run against a 3.15 environment:

uv venv --python 3.15 .venv315
uv pip install --dry-run --python .venv315/bin/python -r requirements.txt

If it resolves, you are fine. If you see the cp315 hint above, that package is your blocker.

A source build is not a failure, but it is slow, and it needs a compiler (Rust for Polars, C++ for DuckDB). In CI, a source build can turn a one-minute job into a ten-minute job. Pin 3.14 in CI until wheels land.

Step 3: Use lazy imports to make scripts start faster

I think lazy imports are the best reason to move to 3.15. The problem they fix is familiar.

A command-line script usually imports everything at the top. Even my_tool.py --help pays the full cost of loading pandas, matplotlib, and scikit-learn, although it never uses them.

The before and after (tested)

The eager version:

import sys
import pandas as pd
import matplotlib.pyplot as plt
import sklearn.linear_model
 
if len(sys.argv) > 1 and sys.argv[1] == "--help":
    print("usage: tool [--help] file.csv")
    sys.exit(0)

The lazy version only adds the lazy keyword:

import sys
lazy import pandas as pd
lazy import matplotlib.pyplot as plt
lazy from sklearn import linear_model
 
if len(sys.argv) > 1 and sys.argv[1] == "--help":
    print("usage: tool [--help] file.csv")
    sys.exit(0)
 
df = pd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 6]})
model = linear_model.LinearRegression().fit(df[["x"]], df["y"])
print(model.coef_)

Results on our machine, five runs each of python script.py --help:

VersionTimeLines in -X importtime
Eager imports2.69 to 2.74 s1,747
lazy import0.01 s27

The full code path still works: the lazy version fit the regression and printed 2.0. pandas loads at the line that first uses pd.

Your absolute numbers will differ. In this environment, import sklearn.linear_model alone took 2.2 s and import pandas took 0.86 s. The pattern holds for any branch of your program that does not touch a heavy library.

See what lazy imports are doing

A lazy name is a proxy until you use it. You can see it:

import sys
lazy import json
 
print(type(globals()["json"]))   # <class 'lazy_import'>
print("json" in sys.modules)     # False
json.dumps(1)
print(type(globals()["json"]), "json" in sys.modules)   # <class 'module'> True

To find which imports are slow in the first place, run python -X importtime your_script.py 2> imports.log and sort by the second column.

Three places where lazy is a SyntaxError (tested)

lazy only works at module level. We hit all three of these errors:

WhereError message
Inside a functionSyntaxError: lazy import not allowed inside functions
Inside try / exceptSyntaxError: lazy import not allowed inside try/except blocks
Star importSyntaxError: lazy from ... import * is not allowed

The try/except rule matters most. Code like try: import ujson as json except ImportError: import json cannot become lazy. Keep the fallback eager, or move it into a small module that you import lazily.

The official docs add that class bodies and from __future__ imports cannot be lazy either.

Errors move to first use

A missing module no longer fails at the top of the file:

lazy import not_a_real_module
print("defined ok")          # this prints
not_a_real_module.x          # ModuleNotFoundError is raised here

That is convenient for optional dependencies. It also means a typo in an import can hide until a rarely used code path runs. Keep at least one test that exercises each lazy import.

Making everything lazy without editing code

You can switch the whole program to lazy imports from the command line:

python -X lazy_imports=all my_tool.py --help
# or
PYTHON_LAZY_IMPORTS=all python my_tool.py --help

Our unmodified eager script dropped to 0.01 s this way. Be careful with modules that do work at import time, though. We made a tiny plugin module that prints and registers itself when imported. With lazy_imports=all, the registration never ran, because the program never touched the module's name:

normal:   plugin registered / main done
all:      main done

Libraries that register plugins, codecs, or matplotlib backends on import can break the same way. Use =all to measure the potential gain, then add lazy by hand to the imports you know are safe. For finer control, the docs describe sys.set_lazy_imports_filter().

Supporting 3.14 and 3.15 in the same file

The lazy keyword is a syntax error on 3.14 and older. If your code must run on both, use the module-level __lazy_modules__ list instead. Older versions ignore it, and 3.15 treats those imports as lazy:

__lazy_modules__ = ["pandas"]
 
import sys
import pandas as pd          # lazy on 3.15, eager on 3.14
 
print("pandas" in sys.modules)   # False on 3.15 until pd is used

We confirmed that pandas stayed out of sys.modules on 3.15 with this pattern.

Step 4: Handle the UTF-8 default

On 3.15, open() without an encoding argument uses UTF-8 on every platform. We confirmed sys.flags.utf8_mode == 1 by default and 0 with PYTHONUTF8=0.

On macOS and Linux this changes almost nothing. On Windows it changes behavior for any code that reads legacy files without naming an encoding: Excel CSV exports in cp1252, Japanese Shift-JIS (cp932) files, or Chinese GBK files. Those reads can now raise UnicodeDecodeError or produce garbled text.

The fix is to be explicit:

import pandas as pd
 
df = pd.read_csv("export.csv", encoding="cp932")      # Japanese Excel export
with open("legacy.txt", encoding="locale") as f:      # old behavior: system locale
    text = f.read()

To find affected code before upgrading, run your tests on your current Python with -X warn_default_encoding. To get the old behavior back temporarily, set PYTHONUTF8=0.

Step 5: Decide on the free-threaded build (3.15t)

Free-threaded Python removes the GIL, so pure-Python threads can use several CPU cores. We ran a CPU-bound loop four times, sequentially and then on four threads:

BuildSequential4 threadsSpeedup
Python 3.150.72 s0.71 s1.0×
Python 3.15t0.65 s0.19 s3.4×

The GIL stayed off after importing numpy, pandas, scikit-learn, polars, matplotlib, and pyarrow (sys._is_gil_enabled() returned False).

Use 3.15t if you run CPU-heavy pure-Python code in threads and your dependencies have cp315t wheels. Skip it if you depend on Polars, DuckDB, or deep-learning libraries. Skip it as well if your heavy work already runs inside NumPy or pandas, which release the GIL on their own.

Should you upgrade now?

Your situationRecommendation
Scripts, CLIs, automation with the numpy / pandas / sklearn stackUpgrade. Lazy imports are a real win
Jupyter notebooks for analysisUpgrade in a new environment; keep your old kernel for a few weeks
PyTorch or TensorFlow projectsStay on 3.14 (uv python pin 3.14)
DuckDB or Polars-on-3.15t in CIWait for wheels, or accept compile time and a compiler in CI
Windows code that reads legacy-encoded filesAdd explicit encoding= first, then upgrade

Using 3.15 in Jupyter

ipykernel installs and works on 3.15. To register a 3.15 kernel from a uv project:

uv add ipykernel
uv run python -m ipykernel install --user --name py315 --display-name "Python 3.15"

If you work in notebooks, RunCell (opens in a new tab) runs as an AI agent inside Jupyter, so it can see your live kernel state. That helps when you test whether an existing notebook still runs on the new kernel. PyGWalker (opens in a new tab) also works on 3.15 (tested: pyg.to_html() rendered a DataFrame), with the DuckDB compile noted above.

Troubleshooting

SymptomCauseFix
uv python install 3.15 installs 3.15.0a4uv is older than 0.13uv self update, or upgrade through Homebrew / pipx
No solution found … no wheels with a matching Python ABI tag (cp315)The package has no 3.15 wheel yet (torch, tensorflow)uv python pin 3.14 for that project
Install hangs for minutes on duckdb or polars-runtimeSource build because no matching wheel existsWait it out (needs a C++ or Rust compiler), or use 3.14 / regular 3.15 instead of 3.15t
SyntaxError: lazy import not allowed …lazy inside a function, try, or a classMove the import to module level, or keep it eager
A plugin or backend stops registering-X lazy_imports=all skipped an import with side effectsRemove =all; use lazy only on chosen imports
UnicodeDecodeError on Windows after upgradingUTF-8 is now the default encodingPass encoding="cp1252" / "cp932" / "gbk" or encoding="locale"
New uv project unexpectedly on 3.15uv 0.13 changed the defaultuv python pin 3.14
SyntaxWarning: 'return' in a 'finally' block from a dependencyThe library has old code flagged by newer PythonHarmless; it comes from the library, not your code

FAQ

When was Python 3.15 released? Python 3.15.0 was released on October 9, 2026.

How do I install Python 3.15 with uv? Update uv to 0.13.0 or newer, then run uv python install 3.15. Older uv versions only know the 3.15 alpha builds.

Does pandas work on Python 3.15? Yes. pandas 3.0.6 ships cp315 and cp315t wheels, as do numpy, scipy, scikit-learn, matplotlib, and pyarrow.

Does PyTorch support Python 3.15? Not yet. As of October 11, 2026, torch 2.14.1 has wheels only up to cp314 and cp314t, so it cannot be installed on 3.15.

What are lazy imports in Python 3.15? Writing lazy import module makes Python create a lightweight proxy and load the module only when the name is first used. This cuts startup time for code paths that never touch the module.

Can I use lazy imports and still support Python 3.14? Yes. Define __lazy_modules__ = ["module_name"] at the top of the module. Python 3.15 treats those imports as lazy, and older versions ignore the list.

How do I keep a project on Python 3.14? Run uv python pin 3.14 in the project folder. It writes a .python-version file that uv respects.

Related Guides