What is PEP 829?
PEP 829: Package Startup Configuration Files splits the two jobs that .pth files do today into two separate files. .pth keeps the sys.path extension job. A new <name>.start file takes over package initialization, replacing the import line that turns every .pth file into an exec() surface at interpreter startup. Authored by Barry Warsaw and accepted on 24 April 2026 for Python 3.15.
Separate the two jobs .pth files do
A .pth file in site-packages does two unrelated things. Lines that name a directory get added to sys.path, which is the benign use that editable installs and namespace packages rely on. Lines that begin with the word import are passed to exec() at every interpreter startup. Both jobs travel in the same file, with no schema and no boundary between them.
The PEP is direct about why that matters: “import lines are executed using exec() during interpreter startup, which opens a broad attack surface.”
The handbook already covers this surface in detail. Why installing a Python package can run code walks through the four execution surfaces a package gets, and the March 2026 litellm compromise is the canonical example of the .pth import-line job being weaponized: a file called litellm_init.pth ran on every python command, including python -c "print(1)" in unrelated terminals, and exfiltrated credentials before the import returned.
PEP 829 does not remove the surface from existing Python releases, but it ends its future. Python 3.15 introduces the replacement, and three releases later the old mechanism stops running.
Define the .start file format
A <name>.start file lives next to .pth files in site-packages. Each line is one entry point, written in the colon form that pkgutil.resolve_name() accepts, for example mypkg.startup:initialize. The interpreter resolves each name and calls it. The two-file split is intentional: .start files cannot extend sys.path, and .pth files no longer need an import line for any new use case.
The <name> prefix is arbitrary and need not match the package, though the PEP recommends matching for clarity. Files are encoded as UTF-8 with an optional byte-order mark (the utf-8-sig codec). Lines beginning with # are comments. Within a site-packages directory the interpreter sorts .start files alphabetically by filename and runs every entry point in order; entries are not de-duplicated, so a callable listed twice runs twice. Errors during parsing are skipped silently and only surface when Python is invoked with -v. Errors raised by an entry point print a traceback to sys.stderr and processing continues.
The order of work at startup is: collect every .pth file, apply all sys.path extensions, then collect every .start file and run its entry points. All path changes are visible by the time any callable runs, so a .start entry point can import modules from a sibling package’s .pth-extended path.
Walk the deprecation timeline
PEP 829 retires the .pth import line in three phases.
- Python 3.15 through 3.17. Both file formats are supported. A
<name>.pthfile’simportlines are still executed, unless a matching<name>.startfile exists in the same directory. When the names match, the.startfile shadows the.pthimport lines and only the.startentry points run. The.pthfile’s directory lines still extendsys.patheither way. - Python 3.18 and 3.19. Import lines in
.pthfiles are silently ignored. A package that still ships only a.pthfile withimportlines stops running its initialization code. No error, no warning unlesspython -vis set. - Python 3.20 and later. Python emits a warning whenever it sees an
importline in a.pthfile. The warning runs in addition to the silent ignore.
A .pth file that contains only sys.path directory lines is unaffected throughout. The deprecation targets import lines specifically, which is the line type that gets passed to exec().
Migrate as a package author
Most packages do not ship .pth files at all. The two common cases that do are editable installs (covered by PEP 660) and tools that need to run a bit of setup code at every interpreter startup. The first case is purely path extension, so it is unaffected. The second case is the one PEP 829 changes.
A package that needs initialization to run on every Python startup, and that needs to support both pre-3.15 and 3.15+ Pythons, ships both files:
- A
<name>.pthwhoseimportlines run on Pythons that predate PEP 829. - A
<name>.startwhose entry points run on Python 3.15 and later.
On 3.15 through 3.17 the matching <name>.start shadows the <name>.pth import lines, so the callable runs once per startup, not twice. On 3.18 and later the .pth import lines are ignored anyway, so only the .start entry points run. A package that drops support for Pythons predating 3.15 can ship only the .start file.
The migration also forces a small refactor: anything the .pth import line used to do has to live behind a real callable somewhere in the package, which is the point. An entry point referenced by a callable name is auditable and shows up in static analysis; an exec() of an import statement does not.
Defend before 3.18 lands
PEP 829 closes the .pth import-line surface from the runtime side, but the closure is gradual. The first release that silently ignores import lines is Python 3.18. Until 3.18 reaches the platforms a project actually runs on, a malicious release can still ship a .pth file that the interpreter dutifully executes, and the defenses laid out in the security explainer still apply: hash-pinned dependencies, a cooldown on recently-published versions, and a startup-file scanner like fetter.
The other three execution surfaces (install-time backends, import-time __init__.py, and sitecustomize/usercustomize) are not addressed by PEP 829 and remain available to a malicious release on every supported Python.
Learn More
- PEP 829 specification is the authoritative document.
- Python
sitemodule documentation describes.pth,sitecustomize, andusercustomizesemantics that PEP 829 modifies. pkgutil.resolve_name()documentation explains the colon-form names that.startfiles use.- Why installing a Python package can run code covers the four execution surfaces and why they matter.
- LiteLLM Got Owned, and Your Dependencies Might Be Next walks through the March 2026
.pth-importattack end to end. - Lightning Got Owned: When
import lightningSteals Your Credentials covers the import-time variant of the same playbook. - What is PEP 660? describes the modern editable-install standard, which uses
.pthforsys.pathextension only. - What is an editable install? gives the practical view of the most common legitimate
.pthconsumer. - Python Steering Council meeting summary, 2026-04-16 is the meeting where the PEP was queued for resolution.