Recent development · Practical technical guide · 17 October 2026

Python 3.15.0 was released on 9 October 2026. A migration guide to the two changes most teams feel first, UTF-8 as the default encoding and explicit lazy imports, plus the porting notes, removals and the new sampling profiler.

The release in one paragraph

Python 3.15.0 was released on 9 October 2026, according to the python.org release page. The What's New document lists the headline changes: explicit lazy imports (PEP 810), UTF-8 as the default encoding (PEP 686), new frozendict and sentinel built-in types (PEPs 814 and 661), unpacking in comprehensions (PEP 798), a stable ABI for free-threaded builds, frame pointers enabled by default, and a new profiling package containing Tachyon, a statistical sampling profiler (PEP 799). This guide covers the two changes that affect the most existing code, how to find what breaks, and a staged upgrade plan.

UTF-8 by default: what actually changes

Python now uses UTF-8 as the default encoding regardless of the system locale. In practice the change is felt wherever text I/O happens without an explicit encoding argument: open(path), Path.read_text(), and similar calls now read and write UTF-8. On Linux and macOS systems already configured with a UTF-8 locale, little changes. On Windows, where the locale encoding was often a legacy code page such as cp1252, behaviour changes for any code that relied on the default.

Two failure patterns show up. Reading an existing file written in a legacy encoding now raises UnicodeDecodeError on the first non-ASCII byte, or worse, decodes without error into the wrong characters when the bytes happen to be valid UTF-8. And files written by 3.15 are now UTF-8, which an older consumer reading with the locale encoding will misinterpret. Both are mismatches between writer and reader that the old default happened to hide on one machine.

Finding affected code before you upgrade

The opt-in encoding warning, available since Python 3.10, reports every place that relies on the default. Run your test suite on your current Python with the warning enabled and turned into an error, and fix each hit by stating the encoding explicitly.

Illustrative python, checked for syntax; adapt names and storage to your system.

# Run the suite on 3.12-3.14 first:
#   python -X warn_default_encoding -W error::EncodingWarning -m pytest
from pathlib import Path

def read_config(path: Path) -> str:
    # Explicit and identical on every Python version and platform.
    return path.read_text(encoding="utf-8")

def read_legacy_export(path: Path) -> str:
    # A file you know was produced with the machine's locale encoding.
    return path.read_text(encoding="locale")

The special value encoding="locale" keeps the old behaviour deliberately for the files that need it, and works on 3.10 and later. If you must ship 3.15 before the code is fixed, the What's New document gives the escape hatch: PYTHONUTF8=0 or -X utf8=0 restores the previous default. Treat that as a temporary flag with an owner and a removal date, not a configuration.

Explicit lazy imports

PEP 810 adds a lazy soft keyword. A lazy import creates a lightweight proxy and defers loading the module until the name is first used. Startup time drops for programs that import many modules but use only some of them in a given run, which describes most command-line tools and many services' cold paths.

Python 3.15 syntax; this does not parse on earlier versions.

lazy import json
lazy from pathlib import Path

def main(argv):
    if argv[1:] == ["--version"]:
        print("tool 2.4.0")       # json and pathlib are never loaded on this path
        return 0
    data = json.loads(Path(argv[1]).read_text(encoding="utf-8"))
    return len(data)

The rules are strict and worth knowing before you start. lazy is allowed only at module scope; using it inside a function, a class body, or a try/except/finally block is a SyntaxError, so the common "try to import an optional dependency" pattern cannot be lazy. Star imports and __future__ imports cannot be lazy. If loading fails, the exception is raised where the name is first used, and the traceback shows both that location and the original import statement.

For libraries that must also run on older versions, a module can list names in __lazy_modules__; ordinary import statements for those modules then behave lazily on 3.15 and normally elsewhere. A global mode exists too: -X lazy_imports=all or PYTHON_LAZY_IMPORTS=all makes imports lazy by default, and sys.set_lazy_imports_filter() narrows that to modules you choose.

Illustrative python, checked for syntax; adapt names and storage to your system.

import sys

__lazy_modules__ = ["json", "myapp.reports"]   # honoured on 3.15, harmless on older versions

import json            # lazy on 3.15
import os              # eager

def only_my_modules(importing, imported, fromlist):
    return imported.startswith("myapp.")

if hasattr(sys, "set_lazy_imports_filter"):
    sys.set_lazy_imports_filter(only_my_modules)

Where lazy imports bite

  • Import-time side effects. Plugin registries, ORM model registration, signal handlers and codec registration often run when a module is imported. Make that module lazy and the side effect happens later, or never. Keep such modules eager or call an explicit registration function.
  • Moved errors. A missing dependency used to fail at startup, where deployment health checks catch it. Now it fails on the first request that touches it. Add a startup check that touches critical modules, or keep them eager.
  • Latency moves, it does not vanish. The cost of loading a heavy module lands on the first request that uses it. For services, warm critical paths during startup.
  • Measuring the wrong thing. Use python -X importtime before and after to see which imports matter; marking cheap modules lazy adds little.

Other porting notes from the release notes

ChangeEffect
sqlite3.connect()All parameters except database are keyword-only
argparse add_argument with a short option and a single-dash long optiondest is now inferred from the long option, for example foo instead of f
base64.urlsafe_b64decode()Padding is no longer required; pass padded=True to require it
unittest assertWarns and assertWarnsRegexNon-matching warnings are no longer swallowed, so tests may leak warnings
Removed modules and functionssre_compile, sre_constants and sre_parse; glob.glob0 and glob.glob1; CGIHTTPRequestHandler; PurePath.is_reserved; platform.java_ver
profile moduleDeprecated, to be removed in 3.17; cProfile remains as an alias of the new tracing profiler

A profiler you can attach in production

The new profiling.sampling module, Tachyon, samples stack traces from a running process without code changes or a restart. python -m profiling.sampling attach PID attaches to a live process, dump PID prints a one-shot stack of every thread, which is useful for a hung worker, and run profiles a script from the start. Output formats include pstats-compatible tables, collapsed stacks for flame graph tools, a self-contained HTML flame graph and Firefox Profiler format; --mode chooses wall-clock, CPU, GIL-holding or exception time. Attaching to another process needs the usual operating system permissions, so plan how operators get them before an incident.

A staged upgrade plan

  1. Add 3.15 to the CI matrix as allowed-to-fail and see which dependencies lack 3.15 wheels.
  2. On your current version, run tests with the encoding warning as an error and fix every hit.
  3. Make 3.15 required in CI once dependencies install and tests pass.
  4. Measure startup with -X importtime and introduce lazy imports only where they save measurable time, keeping side-effect modules eager.
  5. Roll out to a canary environment, watching first-request latency and import errors that now surface late.
  6. Remove any PYTHONUTF8=0 escape hatches before declaring the upgrade done.

Sources and scope

Checked against these sources on 17 October 2026. Event: Python 3.15.0 release, 9 October 2026; recheck it before acting. Examples and figures are illustrative, not measurements.

Make every text encoding explicit before upgrading, and adopt lazy imports where import timing shows a gain, keeping side-effect modules eager.