Python 3.15 introduces .start files as the explicit replacement for executable import lines in .pth files. If your package runs initialization code during interpreter startup, move that code into a zero-argument callable and reference it as package.module:callable from a matching .start file.
The change is defined by PEP 829 and is available in Python 3.15. It does not remove the useful path-extension role of .pth files. Instead, it separates path configuration from startup code so that startup behavior is easier to audit, test, and eventually control.
This guide shows the migration pattern, the compatibility strategy for older Python releases, and the checks package maintainers should add before Python 3.15 reaches its final release.
Why Python Is Replacing Executable .pth Lines
A .pth file traditionally has two unrelated jobs:
- add directories to
sys.path; - execute any line that begins with
importduring interpreter startup.
The second behavior is powerful but opaque. A package can place multiple statements after an import by separating them with semicolons, and site.py passes that source to exec(). The behavior occurs before the user's first line of Python code, often without an obvious entry point to inspect.
PEP 829 keeps ordinary path entries in .pth files, but moves startup code to a dedicated format:
package.module:callable
Python resolves that reference with pkgutil.resolve_name(..., strict=True) and calls it with no arguments. The callable's return value is ignored.
This is a narrower and more reviewable mechanism, but it is not a security sandbox. A startup callable still runs Python code with the process's permissions. Treat every installed .start file as executable configuration.
The Minimal Migration
Assume a package currently installs this file in site-packages:
acme_bootstrap.pth:
import acme_bootstrap; acme_bootstrap.initialize()
Keep the initialization logic in an importable package module and make the entry function accept no arguments:
acme_bootstrap/__init__.py:
import os
def initialize() -> None:
os.environ.setdefault("ACME_RUNTIME", "enabled")
Then install a matching startup file beside the existing .pth file:
acme_bootstrap.start:
acme_bootstrap:initialize
The matching base name matters. When Python 3.15 finds both acme_bootstrap.pth and acme_bootstrap.start, it ignores executable import lines in that .pth file and runs the entry points from the .start file instead. Ordinary path lines in the .pth file still work.
Both files belong in a directory that the site module treats as a site-packages directory. A .start file buried inside the package directory will not be discovered as startup configuration.
Support Older and Newer Python Versions Together
Many packages cannot require Python 3.15 immediately. PEP 829 provides a compatibility pattern for the transition period:
acme_bootstrap.pth:
import acme_bootstrap; acme_bootstrap.initialize()
acme_bootstrap.start:
acme_bootstrap:initialize
On Python 3.14 and earlier, the interpreter does not know about .start files, so it executes the legacy .pth import line. On Python 3.15 and newer, the matching .start file takes precedence and suppresses that import line. The same callable is used in both paths, which keeps behavior aligned while avoiding two separate implementations.
Do not change the two files to different base names during this period. For example, legacy_bootstrap.pth and acme_bootstrap.start do not match, so Python 3.15 can run both and initialize the package twice.
The current removal timeline is deliberately gradual:
- Python 3.15 through 3.17 continue to process legacy import lines when no matching
.startfile exists. - Python 3.18 and 3.19 ignore those import lines.
- Python 3.20 reports their presence as a warning.
Path-only lines in .pth files are not being removed by PEP 829.
Verify the Startup Hook in an Isolated Environment
Test the built artifact, not only the source tree. Create a clean virtual environment with Python 3.15, install the wheel, and inspect the resulting site-packages directory:
py -3.15 -m venv .venv
.venv\Scripts\python -m pip install --upgrade pip
.venv\Scripts\python -m pip install .\dist\your_package.whl
.venv\Scripts\python -c "import sys; print(sys.version)"
Your test should prove three things:
- the
.startfile was installed at the site-packages root; - its target resolves to a callable that accepts zero arguments;
- startup happens exactly once when a matching legacy
.pthfile is present.
For a side-effect-free test, let the startup callable increment an in-memory counter exposed by the package, append one line to a temporary file, or register a test-only audit event. Avoid tests that mutate user-wide configuration or depend on network access during interpreter startup.
The following reduced test mirrors the PEP 829 transition behavior:
from pathlib import Path
import site
site_dir = Path("demo-site")
state = site.StartupState()
state.addsitedir(str(site_dir))
state.process()
I ran this pattern on the official 64-bit Windows build of CPython 3.15.0rc2. The isolated directory contained a matching .pth import line and .start entry point. The .start callable created its marker, while the legacy .pth import marker remained absent. That confirms the precedence behavior without treating the result as a performance benchmark.
Understand Ordering and Failure Behavior
Python processes startup configuration in phases:
- it reads
.pthfiles and collects valid path extensions; - it adds those paths to
sys.path; - it reads
.startfiles; - it invokes entry points and any remaining legacy import lines.
Applying all path extensions first means a startup callable can live on a path introduced by a .pth file. File names are processed in alphabetical order, but duplicate startup entry points are not deduplicated. If the same callable appears twice, Python calls it twice.
Design startup code to be idempotent even when you believe the packaging layout is unique. Editable installs, layered environments, and duplicated files can expose assumptions that a clean wheel test misses.
If Python cannot resolve or invoke an entry point, it prints the error to stderr and continues with the remaining entries. That makes a clear diagnostic essential. Keep the callable small, avoid swallowing exceptions, and move expensive work behind a normal application entry point whenever startup execution is not strictly required.
Using python -S disables site processing, including .pth and .start files. A program that requires the startup hook should either document that constraint or provide an explicit initialization API as a fallback.
Common Migration Failures
The Callable Requires an Argument
This will fail because .start entry points receive no arguments:
def initialize(config_path: str) -> None:
...
Use a zero-argument adapter and read configuration through a documented mechanism:
def initialize() -> None:
config_path = os.environ.get("ACME_CONFIG")
configure(config_path)
The Reference Omits the Callable
Python 3.15 requires the strict colon form. acme_bootstrap is not enough; use acme_bootstrap:initialize or a fully qualified reference such as acme_bootstrap.startup:initialize.
The File Uses the Wrong Encoding
Write .start files as UTF-8. PEP 829 permits UTF-8 with an optional byte-order mark (utf-8-sig). Do not rely on the machine's locale encoding.
Startup Performs Expensive Work
Every normal interpreter launch processes these entry points. Network requests, large imports, database connections, or filesystem scans can slow every CLI command and worker start. Use .start only for work that must happen before user code, and measure its cost in a realistic environment.
Release Checklist for Package Maintainers
Before shipping the migration:
- confirm the built wheel installs
<name>.startat the site-packages root; - keep only path-extension lines in
.pthwhen older-Python compatibility is no longer needed; - use identical base names while
.pthand.startcoexist; - make every startup callable zero-argument, small, deterministic, and idempotent;
- test Python 3.14 and Python 3.15 in separate clean environments;
- run a verbose Python startup during CI to catch parsing and resolution diagnostics;
- test with
python -Sif your application supports site-disabled execution; - document why startup execution is necessary and how users can disable or replace it.
Python 3.15.0rc2 was released on September 1, 2026, and the final 3.15.0 release is scheduled for October 1, 2026. Release candidates are intended for compatibility testing rather than production deployment. This is the right window to inspect built wheels, exercise startup paths, and report regressions before the final release.
Conclusion
Python 3.15 .start files make package startup hooks explicit: keep path configuration in .pth, put startup behavior in a zero-argument callable, and reference it with package.module:callable. Packages supporting older Python versions can ship matching .pth and .start files temporarily, then remove the executable .pth line after the compatibility window.
The migration is small, but the validation should be deliberate. Test the installed wheel, prove that initialization occurs once, and keep startup code minimal and auditable.


Comment