Setup#

Python is on every modern Linux distribution. The operator’s setup problem is rarely “install Python”, it is picking the right Python, getting it on PATH without touching the system interpreter, and standing up a project you can ship.

This page walks the two stages the operator actually does on a fresh host, install Python and set up the project.

Install#

Pick an interpreter, install it under the operator’s account, and leave the system Python alone.

1. Pick the interpreter. CPython 3.10 or later is the default. PyPy is for long-running CPU-bound services where the operator cannot rewrite the hot path. MicroPython is for embedded targets.

2. Install it with a version manager. uv python is the operator’s default. It installs a managed interpreter under ~/.local/share/uv/python/ and never touches /usr/bin/. pyenv is the fallback when uv is not allowed on the host.

Version Manager

Command

uv

uv python install 3.12

pyenv

pyenv install 3.12.4

mise

mise install python 3.12

asdf

asdf install python 3.12.4

$ uv python install 3.12
$ uv python list
$ pyenv install 3.12.4
$ pyenv global 3.12.4

3. Verify.

$ which python
$ python --version
/home/operator/.local/share/uv/python/cpython-3.12.4-linux-x86_64-gnu/bin/python
Python 3.12.4

Never replace /usr/bin/python3. The distribution depends on it.

Setup project#

With Python installed, stand up the project.

1. Bootstrap.

$ mkdir operator-tool && cd operator-tool
$ uv init
$ uv venv
$ source .venv/bin/activate

uv init writes pyproject.toml and .python-version. uv venv creates .venv/ against the version pinned in the project.

2. Add dependencies. Runtime first, dev next.

$ uv add requests click
$ uv add --dev pytest ruff mypy

This updates pyproject.toml and writes uv.lock with hash digests for every transitive dependency.

3. Lay out the source. The src/ layout keeps imports honest (the package is not importable until it is installed).

operator-tool/
├── pyproject.toml
├── .python-version
├── uv.lock
├── README.md
├── .gitignore
├── src/
│   └── operator_tool/
│       ├── __init__.py
│       └── main.py
└── tests/
    └── test_main.py

4. Lock and sync. Lockfiles pin every transitive dependency to a hash so uv sync reproduces the environment exactly on every host.

$ uv lock
$ uv sync

Commit pyproject.toml and uv.lock. Re-audit the lockfile on every dependency bump.

5. Run.

$ uv run python -m operator_tool
$ uv run pytest

Warning

PyPI is a supply-chain attack surface. Anyone can publish a package with any name. The operator must assume the registry is adversarial.

  • Typosquatting, rqeusts / request3 / requests- instead of requests. Type the name once and copy from there.

  • Dependency confusion, attacker publishes a public package with the same name as one of the operator’s private internal packages; the resolver picks the public one. Pin an index URL or vendor internal packages.

  • Compromised maintainers, legitimate package gets a malicious release pushed (event-stream, ua-parser-js, ctx, and others). Pin versions; review changelogs before bumping.

  • Post-install hooks, setup.py runs arbitrary Python during install, including in CI. Prefer wheel-only installs (--only-binary=:all:).

Mitigations. Pin with hash digests in lockfiles. Audit with pip-audit, safety, or trivy fs. Sandbox untrusted packages in containers or fresh venvs.

Distro gotchas#

What bites the operator on each distro when installing Python.

Distro

Gotcha

Ubuntu / Debian

System pip install is blocked by PEP 668 (externally-managed-environment). The right answer is always a venv.

Fedora / RHEL

Same PEP 668 protection. dnf install python3.12 for newer interpreters via Software Collections or modular streams.

Alpine

musl libc, not glibc. Many wheels are glibc-only and compile from source on Alpine. Use python:3.12-alpine only when image size matters more than build speed.

macOS

System Python 3 lags. brew install python@3.12 or uv python install keep current.

Air-gapped install#

When the target has no internet, package wheels on the operator’s box and copy them across.

# On the connected box
$ uv pip download -r requirements.txt --dest ./wheels
$ pip download -r requirements.txt -d ./wheels \
    --platform manylinux2014_x86_64 --only-binary=:all:

# On the air-gapped box
$ uv pip install --no-index --find-links ./wheels -r requirements.txt

Match the target’s platform tag (manylinux2014_x86_64, musllinux, and so on) when downloading or the wheels will be skipped.

Common Tasks#

Install a specific Python version.

$ uv python install 3.12

Bootstrap a new project in the current directory.

$ uv init
$ uv venv
$ uv add requests pytest

Pin and lock dependencies.

$ uv lock
$ uv sync

Run a script in an ephemeral environment.

$ uv run --with requests script.py

Install a CLI tool globally without polluting any venv.

$ pipx install ruff
$ pipx install httpie

Inspect what is installed in the active venv.

$ uv pip list
$ uv pip show requests
$ python -m pip freeze

Audit dependencies for known CVEs.

$ uv pip install pip-audit
$ pip-audit

References#