Python 3.15 gives developers a new way to make a command start faster: do not load the parts it never uses. That sounds like a small implementation detail until the command has a large dependency tree and gets invoked repeatedly. The important change is not that imports become universally cheaper. The language now lets an author explicitly postpone an import until the imported name is needed. That shifts both the expense and the moment at which a broken dependency becomes visible.

Released on October 9, Python 3.15 combines this control over startup with a built-in sampling profiler, a new default for text encoding and a stable binary interface for free-threaded extensions. These changes deserve different adoption decisions. Installing the runtime, changing a project's minimum supported version, enabling deferred imports and porting native extensions are not one upgrade. Treating them as separate engineering decisions makes the release more useful and its risks easier to isolate.

There is also a less glamorous milestone beside the launch. Python's developer guide lists October 1 as the end of support for 3.10. Version 3.11 remains supported for security fixes. In an October 12 account, independent developer Simon Willison explains that his policy of supporting every supported Python version now lets him stop accommodating 3.10. His practical example is telling: heavy dependencies in plugins for his LLM command-line tool could add multiple seconds to each invocation. That is a maintainer's reported experience, not a universal performance measurement.

For a library maintainer, the oldest supported interpreter determines which language features can appear in shared source files. The newest interpreter available to customers answers a different question. A project can test its existing code on 3.15 while retaining an older source-compatibility floor. Conversely, retiring an unsupported version can simplify maintenance without requiring every user to adopt the newest release immediately. The support calendar creates an opportunity to remove compatibility work, not an instruction to collapse those decisions.

Explicit lazy imports are designed around that distinction. An ordinary import remains eager unless an opt-in mechanism changes it. Adding the lazy keyword at module scope initially binds a proxy instead of loading the target. When code first uses the imported name, Python resolves the import and replaces the placeholder with the actual object. The established import machinery still does the work. This is a scheduling change, not a different implementation of the library being imported.

A command with several subcommands provides a useful hypothetical. If displaying help never needs an expensive data-processing module, deferring that module can avoid loading it for that invocation. If the selected command does need it, the loading work moves to first use. Measuring only the time until the process announces that it has started would miss that distinction. Builders should measure the path to a useful result, including the first operation that touches a deferred dependency, as well as subsequent operations.

The same timing change applies to behavior that previously happened as a side effect of import. PEP 810, the design document behind the feature, specifically discusses plugin registries populated when modules are imported. A module whose only purpose is to register a plugin may never perform that registration if it is made lazy and its binding is never accessed. A faster launch that silently loses a plugin is not an optimization. The design recommends explicit discovery and initialization instead of depending on incidental import order.

Errors also move. A missing module or an exception raised while loading it can emerge when the imported name is first used, after other application work has already happened. The release documentation says the traceback records both that access and the original import location. That helps diagnosis, but it does not restore an earlier failure boundary. A migration test should therefore exercise optional commands and deferred paths, not merely confirm that the main module imports successfully.

The feature does not load a single function out of a file while ignoring the rest. A lazy from-import still loads and executes the complete target module when needed. Nor does making one import lazy automatically make its dependency graph lazy. These limits keep the mechanism understandable: authors control particular import sites. They also prevent a misleading promise that a small syntax change will prune arbitrary work throughout a large application.

Python supplies a compatibility route for projects that cannot yet use the new syntax. A module can list fully qualified module names in __lazy_modules__ and retain ordinary import statements. Older Python versions ignore that declaration; 3.15 uses it to defer the selected imports. This creates two timing behaviors that need testing. There is also an interpreter-wide mode and a filtering mechanism, but broad activation should be treated as a separate experiment, especially where third-party packages rely on registration or other initialization effects.

The design history helps explain the caution. PEP 810 uses placeholder objects rather than embedding lazy behavior into Python's fundamental dictionary operations, an approach explored in the earlier, rejected PEP 690. Keeping that behavior out of the core mapping abstraction avoids making unrelated dictionary access responsible for import semantics. For an application team, the corresponding discipline is local adoption: identify a measured startup cost, change the relevant import, and verify the operations whose timing has changed.

The new profiling package offers another way to make performance discussions more specific. Its tracing branch contains the relocated cProfile implementation, with the old name retained as an alias. Its sampling branch, called Tachyon, periodically inspects execution stacks rather than instrumenting every function call. The documented ability to attach to an already running process without changing its code or restarting it makes this a materially different diagnostic option, not just a tidier module name.

Choosing the measurement still matters. The release notes distinguish wall-clock observation, which includes waiting, from CPU-focused observation. An application waiting on an external resource presents a different optimization problem from one occupied with computation. Sampling is evidence about where execution is observed, not a substitute for an end-to-end outcome measurement. We have not independently benchmarked the profiler or reproduced the documentation's strongest overhead claims. Its inclusion is useful without treating those claims as a guarantee for every production system.

The text-encoding change has a different risk profile because it changes a default. Python 3.15 enables UTF-8 mode by default, so text operations without an explicit encoding no longer depend on the environment in the same way. PEP 686 identifies inconsistent defaults as a source of cross-platform bugs. Code written on a UTF-8 system can otherwise assume that a file will be interpreted the same way elsewhere. A shared default removes one source of disagreement, but existing files do not change their bytes when an interpreter is upgraded.

The PEP expressly warns that programs depending on the previous default may encounter decoding errors, garbled text or even silent corruption. A legacy file in another encoding therefore needs an explicit decision, not a hopeful retry. Tests containing only ordinary ASCII characters will not demonstrate that the intended non-ASCII text survives a read-and-write cycle. Representative filenames and data files belong in migration checks, with expected decoded values and output bytes made explicit.

The documented migration tools are practical. Opt-in EncodingWarning can locate operations that rely on an implicit encoding. Applications can specify UTF-8 when that is the file format, or use encoding='locale' when following the locale is genuinely intended. UTF-8 mode can also be disabled with the documented environment variable or interpreter option to retain the earlier behavior. That escape hatch helps isolate a compatibility problem; the durable fix is to make the data contract clear rather than leave it to whichever machine happens to run the job.

For native-extension maintainers, the bigger structural change is abi3t, the stable application binary interface for free-threaded builds. A stable ABI lets an extension target a defined compatibility contract across interpreter minor versions instead of requiring a fresh binary for every version. The existing abi3 contract serves GIL-enabled builds. PEP 803 adds the corresponding route for free-threaded CPython beginning with 3.15. It addresses distribution and maintenance work, not simply the speed of a loop.

The promise comes with source changes. In the new interface, the core PyObject structure is opaque, so extensions cannot depend on directly embedding or inspecting that layout in the old ways. Module initialization and extension-defined types must use the supported APIs. The release notes describe this as potentially non-trivial migration work. They also preserve an alternative: extensions unable to adopt the stable interface can continue building version-specific binaries for free-threaded Python while retaining their existing stable-ABI path for GIL-enabled versions.

PEP 803 records maintainers' concern about expanding binary build matrices and the pressure to prepare packages for each interpreter release. It also records reservations about performance tradeoffs and API coverage. That is the actual bargain: accept a narrower interface and migration cost in exchange for a more durable compatibility boundary. A project that needs functionality outside that boundary may rationally keep version-specific builds. There is no basis here for claiming that the entire package ecosystem has migrated or that every extension benefits equally.

Binary compatibility is also not thread safety. PEP 803 explicitly distinguishes making an extension load successfully from ensuring it behaves safely without a global interpreter lock. The new stable ABI does not retroactively provide compatibility with 3.14 free-threaded builds, either. Maintainers need to state which interpreters a wheel targets and separately test concurrent behavior. Reducing the number of artifacts to distribute is valuable, but it does not remove the responsibility to establish that the code works correctly.

The experimental just-in-time compiler remains another separate evaluation. Python's release notes report improvements while showing that individual benchmark outcomes vary, including regressions. An aggregate result from a specified interpreter build and platform should not become an assumed gain for a different application's workload. The release provides reasons to test, not a single speed multiplier to put into a capacity plan.

The useful adoption plan follows the boundaries of the changes. Verify existing behavior on the new runtime before combining it with new import timing. Check real text data before relying on the new encoding default. Decide whether the stable ABI fits each native extension rather than assuming a new wheel tag solves concurrency. Then use profiling to identify what still consumes time. Python 3.15 offers more control over when work happens and how compatibility is maintained. The payoff depends on preserving the behavior that made the software worth running.

LaunchPad positionSeparate the runtime upgrade from changes to import timing, text encoding and native-extension compatibility.
Reporting standard

This report draws on the linked primary sources and reputable reporting. Company statements are treated as claims until independently demonstrated.