# What is PEP 829 (Package Startup Configuration Files)?


[PEP 829: Package Startup Configuration Files](https://peps.python.org/pep-0829/) splits the two jobs that `.pth` files do 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, accepted on 24 April 2026, and now Final, the PEP is implemented in 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 [March 2026 litellm compromise](https://pydevtools.com/blog/litellm-supply-chain-attack-and-securing-python-dependencies.md) weaponized exactly this line type; [why installing a Python package can run code](https://pydevtools.com/handbook/explanation/why-installing-a-python-package-can-run-code.md) covers that incident and the other three execution surfaces.

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()`](https://docs.python.org/3/library/pkgutil.html#pkgutil.resolve_name) accepts, for example `mypkg.startup:initialize`. The interpreter resolves each name and calls it with no arguments. 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 are reported at two levels. A file that cannot be read (missing, hidden, or not valid UTF-8) is skipped, and the reason appears only when Python runs with `-v`. Once a file is read, every problem with an entry point prints to stderr and processing continues with the next entry: `Invalid entry point syntax in <file>` for a line without a colon, `Error resolving entry point <name> from <file>` when the module or attribute cannot be imported, and `Error in entry point <name> from <file>` with a traceback when the callable raises.

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.

## Try a `.start` file on Python 3.15

The whole mechanism fits in two files. Create a Python 3.15 environment with [uv](https://pydevtools.com/handbook/reference/uv.md), drop a module and a `.start` file into its `site-packages`, and run any Python command (macOS/Linux shell):

```bash
uv venv --python 3.15
SITE=$(uv run --no-project python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")
cat > "$SITE/hello_start.py" <<'EOF'
import sys


def init():
    sys.stderr.write("hello_start.init ran\n")
EOF
echo "hello_start:init" > "$SITE/hello_start.start"
uv run --python 3.15 --no-project python -c "print('hi')"
```

The entry point runs before the script does:

```console
hello_start.init ran
hi
```

Nothing imported `hello_start`; the `.start` file alone made it run. Passing `-S` skips `site` processing and prints only `hi`. If `init()` raises, Python reports the failure and keeps going:

```console
Error in entry point hello_start:init from .venv/lib/python3.15/site-packages/hello_start.start
  Traceback (most recent call last):
    File "<frozen site>", line 566, in _execute_start_entrypoints
    File ".venv/lib/python3.15/site-packages/hello_start.py", line 5, in init
      raise RuntimeError("boom")
  RuntimeError: boom
hi
```

## 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>.pth` file's `import` lines are still executed, *unless* a matching `<name>.start` file exists in the same directory. When the names match, the `.start` file shadows the `.pth` import lines and only the `.start` entry points run. The `.pth` file's directory lines still extend `sys.path` either way. Running Python with `-v` shows which case applied: `import lines in <name>.pth are deprecated, use entry points in a <name>.start file instead.` when they ran, or `import lines in <name>.pth are suppressed due to matching <name>.start file.` when they did not.
- Python 3.18 and 3.19. Import lines in `.pth` files are silently ignored. A package that still ships only a `.pth` file with `import` lines stops running its initialization code, with no error and no warning.
- Python 3.20 and later. Python emits a warning whenever it sees an `import` line in a `.pth` file. 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 (the editable-wheel hook that [PEP 660](https://pydevtools.com/handbook/explanation/what-is-pep-660.md) defines) and tools that need to run a bit of setup code at every interpreter startup. A src-layout editable install writes a bare path line and is unaffected. A flat-layout editable install under setuptools writes an `import` line that installs a module finder (`__editable__.<name>.pth`), so backends that use that strategy must ship a `.start` file before the deprecation ends. The second case, startup code, is the one PEP 829 was written for.

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>.pth` whose `import` lines run on Pythons that predate PEP 829.
- A `<name>.start` whose 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

Until Python 3.18 reaches the platforms a project actually runs on, a malicious release can still ship a `.pth` file that the interpreter executes, and PEP 829 leaves the other three execution surfaces untouched on every Python. The [defenses in the security explainer](https://pydevtools.com/handbook/explanation/why-installing-a-python-package-can-run-code.md) still apply.
