When ‘It Works on My Machine’ Hit Production: Python Dependency Management with uv and pyproject.toml

Last Monday started with a message no engineer enjoys: “Production is green, but staging just failed with an import error we can’t reproduce locally.” The team had done everything that looks responsible from a distance, pinned a few top-level packages, checked in test configs, and ran CI on every pull request. But one transitive dependency had moved, one laptop had a different Python patch version, and suddenly the same repository behaved like three different codebases.

If that story feels familiar, this post is for you. This is a practical guide to Python dependency management with uv, grounded in current docs and real tradeoffs. We will use pyproject.toml as the source of truth, enforce a uv lockfile, and make reproducible Python environments the default for laptops and CI, not a best-effort wish.

The real problem is not installation speed, it is dependency drift

Most teams start caring about dependencies when installs get slow. That matters, but speed is usually not what causes incidents. Drift does. Drift shows up in four predictable places:

  • Developers run different Python versions without noticing.
  • Loose dependency ranges pull different transitive versions over time.
  • CI installs from a different toolchain than local machines.
  • Environments are treated as artifacts to preserve, not disposable state to recreate.

The Python docs for venv are clear that environments are disposable and not portable. Rebuilding them should be normal, not scary. The missing piece for many teams is deterministic resolution and one workflow that works the same everywhere.

Why uv changes the day-to-day workflow

According to Astral’s official documentation, uv is a fast Python package and project manager with a lockfile-based workflow. In practice, that gives you three operational wins:

  • One command surface for project setup, sync, script execution, and Python version management.
  • A committed lockfile (uv.lock) that captures resolved versions so installs are repeatable.
  • A cleaner CI contract: CI can fail fast if the lockfile and project metadata diverge.

That does not mean you must throw away pip knowledge. uv includes a pip-compatible interface, which helps teams migrate incrementally. But the best reliability gains appear when you run the full project workflow around pyproject.toml and uv lock.

A production-friendly baseline you can adopt this week

Start with a minimal but explicit pyproject.toml. The PyPA specification defines this file as the standard location for project metadata and build configuration, so you’re not tying your project to one tool’s private format.

[project]
name = "order-service"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "fastapi>=0.111,<0.112",
  "uvicorn[standard]>=0.30,<0.31",
  "pydantic>=2.7,<2.8",
  "httpx>=0.27,<0.28"
]

[dependency-groups]
dev = [
  "pytest>=8.2,<8.3",
  "ruff>=0.6,<0.7",
  "mypy>=1.11,<1.12"
]

Then codify the team workflow:

# one-time project setup
uv python pin 3.11
uv lock

# day-to-day
uv sync --locked --dev
uv run pytest
uv run ruff check .

# when intentionally changing dependencies
uv add "httpx[socks]>=0.27,<0.28"
uv lock
git add pyproject.toml uv.lock
git commit -m "Add SOCKS support with locked dependency update"

The key behavioral shift is subtle but powerful: dependency changes become reviewable diffs, not accidental side effects of fresh installs.

CI: fail on drift, cache on purpose

uv’s GitHub Actions guide recommends astral-sh/setup-uv, which can install uv and optionally persist cache. Pair that with lockfile-based installs and your CI becomes both faster and more predictable.

name: test

on:
  pull_request:
  push:
    branches: [main]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python from project constraints
        uses: actions/setup-python@v6
        with:
          python-version-file: "pyproject.toml"

      - name: Install uv with cache enabled
        uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true

      - name: Sync exactly what is locked
        run: uv sync --locked --dev

      - name: Test
        run: uv run pytest -q

Two tradeoffs to keep in mind:

  • Strict lock enforcement catches mistakes earlier, but it will fail PRs when someone edits dependencies without refreshing uv.lock. That friction is intentional.
  • Caching helps speed, but stale assumptions around cache keys can hide real changes. Hashing on uv.lock is the safest default.

If you are also tightening delivery reliability, these related 7tech guides fit nicely with this workflow: backend reliability in 2026, change-intelligent delivery pipelines, Git monorepo performance tactics, and self-healing batch pipeline patterns.

A low-risk migration path for teams already on pip-tools or Poetry

You do not need a big-bang migration. The safest pattern is to migrate one service first and preserve behavior while changing tooling. Keep your current production image and deployment flow exactly as-is for the first iteration, and only change dependency resolution and local/CI setup. That gives you a clean before-and-after on build stability.

A practical sequence looks like this:

  • Move metadata into pyproject.toml if it is still fragmented.
  • Generate and commit uv.lock.
  • Switch CI install step to uv sync --locked.
  • Keep test commands unchanged so you isolate one variable at a time.

The tradeoff is temporary duality, two ways of thinking about dependencies while the team transitions. That is acceptable for a short window. What you should avoid is mixed authority, where one file controls local installs and a different file controls CI installs. Pick one source of truth and make drift impossible to ignore.

Troubleshooting: the issues teams hit in week one

1) “CI says lockfile mismatch”

Why it happens: someone changed pyproject.toml but did not regenerate uv.lock.

Fix: run uv lock, commit both files, and re-run CI. Do not bypass --locked in CI to “get green”.

2) “Works locally, fails in container”

Why it happens: Python version mismatch, often patch or minor drift.

Fix: pin Python intentionally (uv python pin 3.11), and ensure CI reads the same constraint via python-version-file.

3) “Private package index auth fails in CI”

Why it happens: credentials are configured on a developer machine but not in runner secrets.

Fix: configure credentials via CI secrets, avoid storing tokens in repository files, and verify no secrets leak into caches.

4) “Install got slower over time”

Why it happens: cache growth and low hit-rate keys.

Fix: align cache keys to uv.lock, and periodically prune cache in CI-oriented paths when needed.

FAQ

Do we need to delete pip from our workflow on day one?

No. A phased migration is usually safer. You can start by adopting uv for lock and sync in one service, then expand once the team is comfortable.

Should we commit .venv to Git to guarantee consistency?

No. Keep environments out of source control. Commit pyproject.toml and uv.lock, then recreate environments as needed.

Is this overkill for a small internal app?

Not if more than one person touches it or CI deploys it. The smallest useful baseline is pinned Python, committed lockfile, and uv sync --locked in CI.

Actionable takeaways

  • Make Python dependency management with uv explicit: commit pyproject.toml and uv.lock together.
  • Pin Python version deliberately and use the same constraint in local and CI environments.
  • Use uv sync --locked in CI so drift fails early, not during deploy or runtime.
  • Treat virtual environments as disposable, reproducible state, never as portable artifacts.
  • Cache dependencies with lockfile-aware keys, and prune cache where runner storage can grow indefinitely.

Dependency management gets framed as tooling preference debates, but incidents rarely care which team won that argument. They care whether your environment can be rebuilt predictably under pressure. If your team adopts just one habit this quarter, make it this: lock intentionally, sync consistently, and let reproducibility become routine.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Privacy Policy · Contact · Sitemap

© 7Tech – Programming and Tech Tutorials