Python 3.15.0 was released on 9 October 2026. Its headline changes land exactly where LLM apps hurt first: explicit lazy imports (PEP 810) for process start-up, UTF-8 as the default encoding (PEP 686) for files and pipes, new built-in sentinel and frozendict types, and a sampling profiler (PEP 799).
I read the What’s New, the PEPs and the PyPI metadata so you can decide fast: what changes, what the ecosystem supports today, and a one-week plan. I have not benchmarked 3.15 myself, so every number below is attributed to its source.
1. Context: What Shipped on 9 October 2026
3.15.0 arrived on the date set in the release schedule (PEP 790) after three release candidates, and python.org marks it as the stable release. The docs say 3.15 supports Windows 10 and newer, and that the official Windows 64-bit binaries now use the tail-calling interpreter.
Release schedule (PEP 790)
Two headline items you can safely skim
PEP 803 adds abi3t, a stable ABI for free-threaded builds: it concerns authors of C extensions and wheel tooling, not application code, though compiled dependencies may start shipping abi3t wheels. The JIT is still experimental: the What’s New reports a 7–8% pyperformance geometric-mean gain over the standard interpreter on x86-64 Linux, with individual benchmarks between roughly a 15% slowdown and a 100%+ speedup against non-JIT builds. Those are CPython benchmark-suite figures, not agent workloads that mostly wait on the network.
Practical consequence: lazy imports, sentinel, frozendict and the profiler are things you adopt on your own schedule. UTF-8 is different — it applies the moment your existing code runs on 3.15, which is why it gets a full section below.
2. Four Implications for LLM and Agent Developers
These are the four changes that reach application code. Lazy imports are the biggest start-up lever; UTF-8 is the one that can change behaviour without you opting in.
Lazy imports for cold start
PEP 810 · Opt-in · Module scope only
lazy import openai binds a proxy and loads the module on first use, so --help or a short subcommand stops paying for every SDK imported at the top of the file. Gains depend on your import graph; measure with -X importtime.
UTF-8 by default
PEP 686 · Not opt-in · Mostly Windows
open(), TextIOWrapper and text-mode subprocess pipes without an explicit encoding now use UTF-8 regardless of locale. On Linux and macOS that is usually what you had; on Windows it replaces the ANSI code page.
sentinel and frozendict
PEP 661 · PEP 814 · Built-ins
sentinel('MISSING') is a unique marker with a readable repr that survives copy; it separates “the model omitted the argument” from “the model sent null”. frozendict is an immutable mapping, hashable when all keys and values are, for schemas and config.
Tachyon sampling profiler
PEP 799 · profiling.sampling
Attach to a running process by PID and get a flame graph without touching the code. Wall mode shows where an agent loop waits, cpu mode where it burns CPU, and --async-aware follows await chains.
Before and after: an agent CLI with heavy imports
A typical agent CLI imports an SDK, an array library and a tokenizer at the top of the file and then uses each in one subcommand. With PEP 810 you put the soft keyword lazy in front of the import: the name is bound to a lightweight proxy and the module loads on first use. The syntax below is the one in the language reference. There are no timings because I have not measured your workload — the next block shows how you will.
# agent_cli.py — Python 3.14 and earlier: everything loads before main() runs
import argparse
import json
import numpy as np # only the `embed` subcommand needs it
import tiktoken # only `count` needs it
from openai import OpenAI
# agent_cli.py — Python 3.15 (PEP 810): proxies first, real modules on first use
import argparse
import json
lazy import numpy as np
lazy import tiktoken
lazy from openai import OpenAI
def main():
args = build_parser().parse_args() # --help never loads the heavy three
if args.cmd == 'embed':
vectors = np.asarray(load_vectors(args.path)) # numpy loads hereThe import graph for --help
Nothing else in the file changes. Names are bound immediately; the cost moves to the first line that actually uses them.
Before (eager)
agent_cli.py --help ├─ argparse, json ├─ numpy loaded ├─ tiktoken loaded └─ openai loaded └─ whole dependency tree
After (lazy import)
agent_cli.py --help ├─ argparse, json ├─ numpy proxy only ├─ tiktoken proxy only └─ openai proxy only agent_cli.py embed └─ numpy loads on first use
Measure it, don’t trust it
python -X importtime prints self and cumulative time for every import to stderr, and -X importtime=2 (3.14+) also lists modules that were already loaded, marked cached. Run it before and after on the commands users run most. The PEP’s motivation says laziness can cut start-up by 50–70% for CLI tools but cites no source for that figure; the only attributed number in its case studies is 10–20% for PySide applications. Your result depends entirely on what you import and what runs at import time.
# per-import cost (self and cumulative, microseconds) goes to stderr
python -X importtime agent_cli.py --help 2> importtime.txt
# top 15 by cumulative time (second column)
sort -t'|' -k2 -n -r importtime.txt | head -15
# also list modules that were already loaded, marked `cached`
python -X importtime=2 agent_cli.py --help 2> importtime2.txt
# prove laziness instead of assuming it (expect False once the import is lazy)
python -c "import sys, agent_cli; print('openai' in sys.modules)"Going beyond one import at a time
Two knobs exist. __lazy_modules__ makes ordinary import statements for the listed modules lazy on 3.15+ and is ignored by older versions, which suits libraries and mixed 3.14/3.15 fleets. The global mode (-X lazy_imports=all, PYTHON_LAZY_IMPORTS=all or sys.set_lazy_imports) makes every module-level import potentially lazy; the sys docs say library authors should generally not call it. A filter keeps the blast radius to your own package.
# Option A: one source tree for 3.14 and 3.15 (older versions ignore the list)
__lazy_modules__ = ['numpy', 'tiktoken']
import numpy
import tiktoken
# Option B: flip the process to lazy, but only for your own package
import sys
def only_mine(importing, imported, fromlist):
return imported.startswith('myagent.') # True = may be lazy, False = force eager
sys.set_lazy_imports_filter(only_mine)
sys.set_lazy_imports('all') # same switch as -X lazy_imports=all
# Option C: no code change, whole process
# python -X lazy_imports=all agent_cli.py
# PYTHON_LAZY_IMPORTS=all python agent_cli.pyWhat lazy imports can break
Side effects move from import time to first use: decorator registries, plugin hooks and __init_subclass__ patterns can stop registering until something touches the module. Import errors move too — a broken install fails mid-run instead of at start-up. lazy is only legal at module scope (a SyntaxError inside functions, classes and try blocks), star imports stay eager, and circular imports are not magically solved. Linters and formatters need to learn the keyword, as the PEP itself says, so push one lazy import through your toolchain before rolling it out.
3. The Other Three: UTF-8, sentinel/frozendict and Tachyon
Same method: what the docs say, then the code I would write.
UTF-8 by default: still pass encoding explicitly
PEP 686 turns UTF-8 mode on by default. The docs say it applies when no encoding argument is given and recommend always passing one for compatibility across versions. On Windows, Python used to default text files to the ANSI code page; with UTF-8 mode on it ignores that and uses UTF-8, while the code page stays reachable through the mbcs codec. Console I/O and the filesystem encoding were already UTF-8 on Windows (PEP 528 and 529).
from pathlib import Path
import subprocess
# 1. Files: say what you mean, on every OS and every Python version
Path('memory.jsonl').write_text(payload, encoding='utf-8')
with open('legacy_export.csv', encoding='locale') as f: # the system code page, on purpose
rows = f.read()
# 2. Pipes: Python decodes, the child decides what bytes it writes
r = subprocess.run(['git', 'log', '-5'], capture_output=True, encoding='utf-8', errors='replace')
# 3. Find call sites that lean on the default (PEP 686 migration recipe)
# python -X utf8=0 -X warn_default_encoding -m pytest
# 4. Stopgap only: opt a process out of the new default
# python -X utf8=0 agent.py (or PYTHONUTF8=0)UTF-8 gotchas that bite agent code
Files written earlier with the Windows ANSI code page and read back through the new default can raise UnicodeError or produce mojibake; PEP 686 also warns about silent data corruption. Python only decides how to decode a pipe — the child process decides what bytes it writes, so a tool emitting a legacy code page needs an explicit decode. In UTF-8 mode stdin and stdout use the surrogateescape error handler, and .pth files must now be utf-8-sig.
sentinel and frozendict for tool-call and schema code
Tool calls hand you JSON where a key can be absent or explicitly null, and None cannot tell them apart. sentinel gives you a unique marker that survives copy and, for module-level names, pickle. frozendict is immutable and, per PEP 814, hashable when all keys and values are, so it can be a dict key or an lru_cache argument. It is not a dict subclass, so isinstance(x, dict) checks will not match it, and since 3.15 json.JSONEncoder encodes it as a JSON object.
MISSING = sentinel('MISSING') # builtin in 3.15; repr is just MISSING
def read_arg(args, key, default=MISSING):
value = args.get(key, MISSING)
if value is MISSING: # the model left the key out
if default is MISSING:
raise KeyError(f'tool call lacks required argument {key!r}')
return default
return value # None here = explicit null from the model
SEARCH_TOOL = frozendict(
name='web_search',
parameters=frozendict(type='object', required=('query',)),
)
CACHE_KEY = hash(SEARCH_TOOL) # works: every key and value is hashable
# SEARCH_TOOL['name'] = 'x' # TypeError: no item assignment
payload = json.dumps(SEARCH_TOOL) # JSONEncoder accepts frozendict as of 3.15Tachyon: find where the agent loop really spends its time
PEP 799 adds a profiling package: profiling.tracing (deterministic, with cProfile kept as an alias) and profiling.sampling, the statistical profiler called Tachyon. It samples stacks from outside the process, and the docs describe the overhead as virtually zero in the default non-blocking mode. Attaching needs privileges (root or CAP_SYS_PTRACE on Linux, root or a debugger entitlement on macOS, administrator or SeDebugPrivilege on Windows), and profiler and target must run the same Python minor version. The old profile module is deprecated and scheduled for removal in 3.17.
# profile a script from startup (default: wall-clock mode, text table) python -m profiling.sampling run agent.py # attach to a live worker by PID for 30 s and write a flame graph python -m profiling.sampling attach --flamegraph -d 30 -o profile.html 12345 # CPU only, or follow await chains in asyncio code (separate runs: they don't combine) python -m profiling.sampling run --mode=cpu agent.py python -m profiling.sampling run --async-aware --flamegraph -o out.html agent.py # one-shot stack snapshot of a hung process python -m profiling.sampling dump 12345
Golden rule for 3.15
Be explicit where the default just changed (encodings), be lazy only where -X importtime proves the gain, and let a profiler — not intuition — pick what to optimise in the agent loop.
4. Ecosystem Readiness and Should You Upgrade Now?
I checked PyPI metadata on 14 October 2026 for the libraries most LLM apps pull in. A classifier says what a project claims to test; wheels say what installs without a compiler. I installed nothing on 3.15, so treat this as a map, not a test result.
numpy
Lists 3.15 in its classifiers. The current release ships cp315 wheels and free-threaded cp315t wheels, including win_amd64 and win_arm64.
pydantic and pydantic-core
pydantic lists 3.15 and is a pure-Python wheel. Its compiled core, pydantic-core, publishes cp315 and cp315t wheels including Windows, although its classifier list still stops at 3.14.
openai and anthropic SDKs
Both are py3-none-any wheels with no upper Python bound, so pip can install them on 3.15, but their classifiers stop at 3.14. Their compiled dependency jiter lists 3.15 and ships cp315 wheels.
httpx and httpx2
httpx’s last stable release is 0.28.1 from December 2024, with classifiers up to 3.12. Both SDKs now depend on httpx2, Pydantic’s continuation of the project, which lists 3.15.
tiktoken and orjson
Both publish cp315 wheels, including win_amd64.
Linters and type checkers
ruff, mypy and black list 3.15. PEP 810 says linters and formatters need updates to understand lazy, and an April 2026 post on discuss.python.org says ruff sorts lazy imports since 0.15.6. I could not confirm how your type checker treats the keyword: test it first.
A missing classifier is not breakage
The SDKs are pure-Python wheels, so pip can resolve them on 3.15; their classifiers just have not reached it. That is a signal to run your own suite, not a reason to panic — or to assume it works.
✅ Upgrade, or start a branch, now if
- • You ship a CLI, hook or worker whose start-up is dominated by imports and you control its dependencies.
- • You maintain a library or framework: put 3.15 in the CI matrix now so your users are not the first to find breakage.
- • Windows laptops or runners keep hitting encoding bugs: one consistent default helps, as long as you also add explicit encodings.
- • Every native dependency in your lockfile already has a cp315 wheel.
⏳ Hold for a few weeks if
- • A native dependency in your lockfile has no cp315 wheel yet; compiling it in CI or containers is rarely worth a runtime bump.
- • You use APIs 3.15 removed, such as glob.glob0/glob1, importlib Loader.load_module, the sre_* modules or the NamedTuple keyword form: run your suite on 3.14 with warnings as errors first.
- • You read legacy Windows-code-page files through the default open() and no test covers them.
- • Your CI pins a linter or type checker that cannot parse lazy: you can still upgrade the runtime and simply not use the keyword.
5. What to Do This Week
A focused week beats an open-ended migration. This is the sequence I would run on any agent or LLM codebase:
☀️ Days 1–2: Measure and find the call sites
- 1. Create a throwaway 3.15 venv (on Windows, py install 3.15 uses the release manifest from python.org) and run your existing test suite with no code changes.
- 2. Run python -X importtime on your entry points (--help and the fastest subcommand) and list the ten most expensive imports.
- 3. Run the suite once with python -X utf8=0 -X warn_default_encoding and grep for open(, read_text, write_text and text-mode subprocess calls without encoding=.
📅 Days 3–4: Fix and adopt
- Encodings: add encoding='utf-8' (or 'locale' on purpose) at every flagged call site and decode subprocess output explicitly where the tool is not UTF-8.
- Lazy imports: mark the three to five heaviest, rarely used imports lazy at module scope, then re-run importtime to confirm they left the start-up path.
- Tool-call code: replace object() and magic strings for missing arguments with sentinel, and freeze tool schemas and default configs with frozendict.
📆 Day 5: Profile and decide
- Profile: attach Tachyon to one real agent run, wall mode first and then --async-aware, and write down the top three frames.
- Roll out: upgrade CLIs and workers first and long-running API services last; keep 3.14 and 3.15 in the CI matrix for a few weeks.
- Compatibility: if one source tree must serve both versions, use __lazy_modules__ instead of the lazy keyword.
Conclusion
Python 3.15 is not a rewrite. It is a set of well-scoped changes that map onto LLM app pain: slow cold starts, encoding surprises on Windows, ambiguous missing-versus-null arguments and agent loops nobody can profile. Two of them are opt-in, one is not, and the JIT and abi3t are background for now.
Run the one-week plan above and you will know, with your own numbers rather than mine or the PEP’s, whether lazy imports earn their place and whether your Windows paths survive the new default.
This week in one screen
Measure
- • importtime on entry points
- • Tachyon on one real run
- • cp315 wheels in the lockfile
Make explicit
- • encoding= on file I/O
- • Explicit decode for pipes
- • sentinel for missing args
Hold
- • JIT: still experimental
- • abi3t: extension authors
- • Global lazy mode: filter first
Sources
- Python.org — Python 3.15.0 release
- Python docs — What’s New in Python 3.15
- PEP 810 — Explicit lazy imports
- Python docs — Language reference: lazy imports
- PEP 686 — Make UTF-8 mode default
- Python docs — Using Python on Windows: UTF-8 mode
- PEP 661 — Sentinel values
- PEP 814 — Add frozendict built-in type
- Python docs — profiling.sampling (Tachyon)
- PEP 790 — Python 3.15 release schedule
- PEP 803 — abi3t stable ABI for free-threaded builds
- PyPI — pydantic-core (wheel list)
- PyPI — httpx2



