Python 3.15 Upgrade Guide: Lazy Imports, frozendict, and More

Python 3.15 Upgrade Guide: Lazy Imports, frozendict, and More Python

The safest way to prepare for Python 3.15 is to add the release candidate to continuous integration now, while keeping production on a supported stable release. Python 3.15.0rc1 is close enough to the final ABI for meaningful dependency and extension testing, but the Python release team still describes it as a preview that should not be used for production workloads.

Python 3.15.0rc1 arrived on August 4, 2026. RC2 is scheduled for September 1 and the final release for October 1. This guide focuses on the changes application and library teams should test before that date: explicit lazy imports, the new frozendict and sentinel built-ins, unpacking in comprehensions, UTF-8 defaults, the reorganized profiling tools, and dependency compatibility.

Python 3.15 Release Status

The feature set has been frozen since beta 1. According to the official RC1 announcement, only reviewed bug fixes should land between the release candidates and the final build, and no further ABI changes are planned for the 3.15 series.

That makes RC1 useful for three kinds of work:

  • running an existing test suite against the next interpreter;
  • checking whether dependencies publish compatible wheels;
  • testing C extensions and free-threaded builds against the finalized ABI shape.

It does not make RC1 a production runtime. Keep the preview in a separate virtual environment or CI job, preserve your current production interpreter, and treat every failure as a compatibility signal rather than a reason to patch around the release candidate.

Add Python 3.15 to CI Without Replacing Production

Start with an allowed-failure or experimental job. On Windows with the Python install manager, a representative setup is:

py install 3.15
py -V:3.15 --version
py -V:3.15 -m venv .venv-315
.\.venv-315\Scripts\python -m pip install --upgrade pip
.\.venv-315\Scripts\python -m pip install -r requirements.txt
.\.venv-315\Scripts\python -m pip check

Run the same commands your stable job uses, then add a separate deprecation pass:

.\.venv-315\Scripts\python -X dev -m pytest
.\.venv-315\Scripts\python -W default -m pytest

Do not turn every third-party DeprecationWarning into an immediate release blocker. First identify whether the warning originates in your application, a direct dependency, or a transitive dependency. Record it, check the upstream project, and only make a local code change when your code owns the deprecated call.

For packages with native extensions, verify both source builds and wheels. The RC1 announcement specifically encourages maintainers to publish 3.15 wheels and states that binary wheels built against the release candidates will work with future Python 3.15 releases. Application teams should still test installation on every operating system and architecture they ship.

Use Explicit Lazy Imports Deliberately

PEP 810 adds the lazy soft keyword. It lets a module declare imports in the normal top-level location while deferring the actual import until the imported name is first used:

import sys

lazy import fractions

print("fractions" in sys.modules)  # False
value = fractions.Fraction(2, 3)
print(value)                        # 2/3
print("fractions" in sys.modules)  # True

This example was executed with Python 3.15.0rc1 and produced the comments shown above.

Failure Timing and Import Side Effects

Lazy imports can improve startup when a command-line tool or service imports large optional dependency trees. They also change when failures and side effects happen. A missing module, incompatible native extension, registration hook, or module-level configuration error can now surface at first use instead of at the import statement.

Adopt lazy imports selectively:

  1. Measure startup with the existing eager imports.
  2. Start with optional modules that are not used on every execution path.
  3. Exercise every path that first touches the lazy name.
  4. Keep imports eager when initialization order or registration side effects are part of the contract.
  5. Re-run CLI help, plugin discovery, worker startup, and application shutdown tests.

The lazy keyword is only valid at module scope. It cannot be used in a function, class body, try block, star import, or future import. Python also provides -X lazy_imports, PYTHON_LAZY_IMPORTS, runtime controls in sys, and __lazy_modules__ for compatibility-oriented adoption, but enabling lazy behavior globally deserves a broader test matrix than adding one explicit import.

Review Mapping Checks for frozendict

Python 3.15 adds an immutable, insertion-ordered frozendict built-in. It is hashable when its keys and values are hashable, but it is not a subclass of dict:

settings = frozendict(language="Python", version="3.15")

print(settings["version"])  # 3.15
print(hash(settings))        # valid because all entries are hashable

The practical migration issue is strict type checking. Code such as this rejects a valid immutable mapping:

if isinstance(settings, dict):
    consume(settings)

If the function only needs mapping behavior, prefer the interface it actually consumes:

from collections.abc import Mapping

if isinstance(settings, Mapping):
    consume(settings)

Do not mechanically replace every dictionary with frozendict. It is useful for constants, cache keys, immutable configuration snapshots, and APIs that benefit from an explicit no-mutation contract. Mutable request state and progressively assembled payloads should remain regular dictionaries.

Replace Hand-Rolled Missing Markers with sentinel

The new sentinel built-in creates a unique marker with a concise representation. It preserves identity when copied and can participate in type expressions:

import copy

MISSING = sentinel("MISSING")

def read_timeout(value: int | MISSING = MISSING) -> int:
    if value is MISSING:
        return 30
    return value

assert copy.copy(MISSING) is MISSING

This removes much of the boilerplate around object() markers while producing clearer signatures and debugging output. Keep the sentinel at module scope when it needs stable import and pickle behavior, and continue to compare it with is, not equality.

Test New Comprehension Unpacking

PEP 798 extends unpacking to list, set, and dictionary comprehensions and to generator expressions:

parts = [[1, 2], [3, 4]]
flattened = [*part for part in parts]

print(flattened)  # [1, 2, 3, 4]

The example above was executed successfully on RC1. The syntax can replace a nested comprehension or some uses of itertools.chain, but readability should decide whether to use it. For teams supporting Python 3.14 or earlier, keep existing syntax until the minimum supported version moves to 3.15.

Audit Text Encoding Assumptions

Python 3.15 uses UTF-8 as the default encoding regardless of the system environment. This reduces cross-platform surprises, especially on Windows, but applications that intentionally process locale-encoded files must now say so explicitly.

Continue to specify encodings at data boundaries:

from pathlib import Path

config = Path("config.json").read_text(encoding="utf-8")
legacy = Path("legacy.txt").read_text(encoding="locale")

Our Windows RC1 smoke test reported utf-8 for a text-mode temporary file. That confirms the new default in the tested runtime, but it is not a reason to remove explicit encoding="utf-8" from file formats whose encoding is part of their contract.

To discover code that depends on implicit encoding before upgrading, run tests on the current interpreter with Python's encoding warning enabled, then fix owned call sites deliberately.

Update Profiling Commands and Documentation

PEP 799 introduces the profiling package as a common namespace for built-in profiling tools:

  • profiling.tracing provides deterministic call tracing and replaces the implementation previously centered on cProfile;
  • profiling.sampling provides the new Tachyon statistical sampling profiler;
  • cProfile remains available as a compatibility alias;
  • the old profile module is deprecated and scheduled for removal in Python 3.17.

The RC1 runtime successfully imported profiling, profiling.tracing, and profiling.sampling. We did not publish benchmark numbers: a portable Windows embeddable runtime is useful for feature validation but is not a representative production benchmark environment.

Before changing observability tooling, check operating-system support and permissions for attaching to another process. Keep existing cProfile integrations working while you evaluate sampling in a non-production environment, and document the exact interpreter build, workload, sampling mode, and duration for any measurements you later publish.

Build a Practical Upgrade Checklist

Use this sequence for an application or library:

  1. Pin Python 3.15 RC1 or the latest available release candidate in an experimental CI job.
  2. Install dependencies from a clean environment and run pip check.
  3. Run unit, integration, CLI startup, serialization, and shutdown tests.
  4. Review warnings from your code separately from dependency warnings.
  5. Check wheel availability for every native dependency and target platform.
  6. Audit isinstance(value, dict) checks that really mean Mapping.
  7. Test file boundaries with explicit UTF-8 and locale encodings.
  8. Introduce lazy imports only after measuring startup and mapping side effects.
  9. Keep the production runtime on a stable Python release until 3.15 final.
  10. Re-run the complete matrix against RC2 and the October final release.

If you are already evaluating free-threaded Python, keep that as a separate matrix dimension. A standard 3.15 build and a free-threaded 3.15 build can expose different extension and concurrency issues. The existing Free-Threaded Python 3.14 guide explains how to distinguish real parallel execution from code that merely runs on a no-GIL build.

Conclusion

Python 3.15 is close enough to final for serious compatibility testing, but RC1 should be a CI target rather than a production upgrade. Start with dependencies and test coverage, then adopt lazy imports, frozendict, sentinel, and the new profiling namespace where they solve a measured problem. This approach gives maintainers time to report ecosystem issues before October without coupling production reliability to a preview interpreter.

Official References

Comment

Copied title and URL