Docs menu

Python on Primcloud

How Primcloud detects a Python app, picks the package manager, installs Python and dependencies, caches layers, and runs Django's collectstatic when it applies.

On this page

Primcloud detects Python apps from the files in your repo, picks a package manager based on which lockfile (or requirements.txt) is present, installs the requested Python version into its own layer, creates a virtual environment, installs the app's dependencies into it, and, for Django apps, runs collectstatic at the end of the build. This page covers each of those phases and the config knobs that shape them.

Detection#

Primcloud runs the Python toolchain when the root of your repo contains any of these files:

  • .python-version
  • requirements.txt
  • poetry.lock
  • uv.lock
  • pyproject.toml
  • Pipfile or Pipfile.lock
  • pdm.lock
  • runtime.txt
  • setup.py, setup.cfg, manage.py, app.py, main.py, server.py, or __init__.py
  • A __pycache__/ or venv/ directory

Detection is deliberately loose so that partially configured Python projects still trigger the toolchain. The build then surfaces a specific error during the build if the essentials for a supported package manager are missing.

Which package manager runs. Detection triggers the toolchain, but the actual install path is chosen by the package manager file at the root:

  • requirements.txt → pip
  • poetry.lock → Poetry
  • uv.lock → uv

Exactly one of those three files must exist at the root. If more than one is present, the build fails with a "multiple Python package manager files were found" error. If none is present, the build fails with a "couldn't find any supported Python package manager files" error. Pipfile / Pipfile.lock is recognized during detection but not currently supported; see Package managers for the details.

Python wins a tie across languages. Primcloud checks for Python ahead of the other language toolchains, so a repo carrying both a Python file and, say, a package.json builds as Python. If you want a mixed repo to build as something else, keep the Python markers out of the deploy root.

Install phase#

Python. The build reads .python-version at the repo root to decide which Python version to install. When there's no .python-version, it installs the latest patch of Python 3.14. uv builds require .python-version; the build fails otherwise. See Runtime versions for pinning and the full supported list.

Package manager. The chosen manager (pip, Poetry, or uv) is installed into its own layer at pinned versions the Primcloud Runtime ships. You don't pick these versions. They update as the Runtime is updated.

Virtual environment. All three managers install app dependencies into a PEP 405 virtual environment created at the layer path, with VIRTUAL_ENV pointing at it at runtime. This keeps the app's site-packages isolated from the system Python and makes the layer independently cacheable.

Dependencies. The command that runs depends on the package manager:

  • pip: pip install --no-input --progress-bar off --requirement requirements.txt
  • Poetry: poetry sync --only main --compile --no-interaction
  • uv: uv sync --locked --no-default-groups --compile-bytecode --no-progress

poetry sync and uv sync --locked fully manage the environment against the lockfile: packages removed from the lockfile are uninstalled on the next build. pip install -r requirements.txt does not; if you drop a package from requirements.txt, it stays installed until the venv layer is rebuilt from scratch.

Compile phase#

There is no distinct compile step for Python. Dependency installation and bytecode compilation happen together during the install phase.

Both Poetry (--compile) and uv (--compile-bytecode) pre-compile .pyc files up front to speed up app boot. pip does this by default. SOURCE_DATE_EPOCH is set during the build so that generated wheels and bytecode are reproducible across builds.

If your project includes native extensions (via C sources in setup.py, or through wheels that need to be built from an sdist), the compile happens during pip install / poetry sync / uv sync. C toolchain and headers for the base image are available at build time; see Customizing the build → Native dependencies.

Cache behavior#

The Python build writes cached layers that Primcloud persists in a per-app cache image between builds:

  • Python runtime layer: the installed Python interpreter. Cache invalidates when the Python version changes, when the CPU architecture changes, or when the base image OS changes. You'll see Using cached Python 3.14.6 or Discarding cached Python 3.14.5 since ... in the build log.
  • Package manager layer: pip, Poetry, or uv. Invalidates when the Primcloud Runtime ships a new pinned version of the manager.
  • Virtual environment layer (Poetry, uv): the fully-installed venv. Cached across builds and reused as long as the Python version, package manager version, architecture, and base image OS all match. Because poetry sync and uv sync fully reconcile the environment against the lockfile on each build, caching the venv is safe.
  • pip HTTP/wheel cache (PIP_CACHE_DIR): pip's own download and wheel cache is cached in a separate layer. The pip-installed venv itself is not cached, because pip doesn't manage removals, but the HTTP/wheel cache still avoids re-downloading and re-wheeling unchanged packages.
  • uv cache: uv's download/wheel cache is written to a build-only layer so hardlinks into the venv layer work. It doesn't persist between builds; the cached venv covers repeat installs.

Release phase conventions#

Procfile is required. Unlike some Primcloud languages, Python gets no inferred web command. Every Python app on Primcloud ships a Procfile at the repo root that names each process type and the command that runs it. See Pod Formations for the file format. A typical Django app looks like:

Procfile
web: gunicorn myapp.wsgi
worker: celery -A myapp worker --loglevel=info
release: python manage.py migrate --noinput

The web process must bind to the port in $PORT. Primcloud sets PORT on the web formation and the pod isn't healthy until something is listening on it. Read the value from the environment; don't hard-code a number.

Django collectstatic. If Django is installed in the venv and the repo has a manage.py at the root, the build runs python manage.py collectstatic --noinput at the end of the build. Skipped automatically when:

  • No manage.py is at the repo root.
  • Django is installed but django.contrib.staticfiles isn't enabled in the app's settings.

If collectstatic runs and fails, the build fails. Django surfaces the error, most often a settings misconfiguration or a missing dependency needed to import the settings module.

Runtime environment. The following variables are set on every process at runtime:

Variable Value Purpose
PATH With the venv's bin/ first, then Python's bin/ So python, pip, gunicorn, and any installed console scripts resolve.
VIRTUAL_ENV Path to the app's venv layer Mirrors what source bin/activate sets; tools that respect it (like pip) target the venv.
PYTHONUNBUFFERED 1 Disables Python's output buffering so logs aren't lost when a process crashes.
PORT Set by Primcloud Present on web process types only; non-web processes get no PORT.

Anything you set in the Secrets panel is layered on top of these at runtime, with one exception: the build rejects a small set of env vars that would break the build (VIRTUAL_ENV, PYTHONHOME, PIP_TARGET, POETRY_HOME, UV_PROJECT_ENVIRONMENT, and similar). If you set one of these, the build fails during the environment check. See Troubleshooting Python → Unsafe environment variable for the list.