Skip to main content

Team Collaboration and Reproducible Environments with AI

The "Works on My Machine" Problem

You've created a fantastic Python project. Your code runs perfectly on your computer. You invite a teammate to work on it and... their environment doesn't match yours. They get errors. Dependencies are different versions. The Python version is different. Nothing works.

This is the "works on my machine" problem, and it's plagued software teams for decades.

The solution? Reproducible environments — using UV to ensure that every developer on your team has the exact same library versions, the exact same Python version, and the exact same environment setup. No surprises. No debugging environment differences.

This is the capstone lesson of the UV chapter: we're moving from individual development to team workflows.

Understanding: What Makes Environments Reproducible?

The Two Files Your Project Needs

Your UV project contains two critical files that work together:

File 1: pyproject.toml (Constraints)

This file says "what I want, roughly":

[project]
dependencies = [
"requests>=2.31.0",
"flask>=3.0",
]

[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=23.0",
]

Notice the >= signs. This means "I want requests, version 2.31.0 or higher." It's flexible—if a newer version appears, that's fine.

File 2: uv.lock (Pinned Versions)

This file says "here's exactly what I have":

requests==2.32.1
flask==3.0.2
pytest==7.4.0
black==23.12.0

Notice the == signs. This means "I tested with this exact version." It's strict—everyone gets this exact version.

Why Both Files?

Think of it like a recipe versus a specific meal:

  • pyproject.toml = Recipe ("Use flour, roughly 2-3 cups")
  • uv.lock = Exact meal ("2.5 cups of King Arthur flour, batch #2024-10")

When you're developing alone, pyproject.toml is flexible enough. You might use requests 2.31 or 2.32; both work fine.

But when you're on a team, you need the exact meal. If developer A used requests 2.31 and developer B used requests 2.32, and they have different behavior, you'll spend hours debugging an environment difference (not a code bug).

That's why UV automatically creates and updates uv.lock every time you run uv add or uv sync.

Real-World Scenario 1: Preparing Your Project for a Teammate

You've built a web scraper with UV. Now you want your teammate Maria to work on it.

Step 1: Ensure Both Files Exist

Verify your project has both pyproject.toml and uv.lock. If uv.lock doesn't exist:

uv lock

Step 2: Commit Both Files to Git

# These files go to Git:
git add pyproject.toml uv.lock .python-version

# These do NOT go to Git (already in .gitignore):
# .venv/ <- Virtual environment
# __pycache__/ <- Python cache
# *.pyc <- Compiled bytecode

Why not commit .venv? It's 50+ MB of dependencies. Everyone can recreate it in seconds using uv.lock.

Real-World Scenario 2: Teammate Clone and Setup

Maria just cloned your project. She has:

web-scraper/
├── pyproject.toml # Constraints
├── uv.lock # Exact versions
└── src/
└── scraper.py # Your code

She does not have .venv/ yet—she'll create it.

She Runs uv sync

uv sync

Output:

Resolved 15 packages in 0.25s
Downloaded 15 packages in 0.18s
Installed 15 packages in 0.08s

What happened:

  1. UV read uv.lock
  2. Downloaded exact versions (requests 2.32.1, beautifulsoup4 4.12.0, etc.)
  3. Created .venv/ with those exact packages

Maria's environment is now byte-for-byte identical to yours.

She Runs Your Code

uv run python src/scraper.py

Output:

Scraping https://example.com...
Found 42 items
Saved to results.json

Perfect. No "works on my machine" problems.


Real-World Scenario 3: Adding a New Dependency Mid-Project

Two weeks later, Maria needs pandas for data analysis.

Maria Adds the Dependency

uv add pandas

Output:

Resolved 52 packages in 0.40s
Downloaded 52 packages in 0.31s
Installed 52 packages in 0.12s

UV updates both pyproject.toml and uv.lock with pandas and its 50+ dependencies.

Maria Commits

git add pyproject.toml uv.lock
git commit -m "Add pandas for data analysis"

You Pull and Sync

git pull
uv sync

Now you have pandas too, with the exact same version Maria has.


Real-World Scenario 4: Production Deployment

When deploying to production, you don't need dev tools (pytest, black, etc.). Use:

uv sync --no-dev

This installs only production dependencies (requests, beautifulsoup4, pandas) and skips dev tools. Keeps your production environment lean, fast, and secure.


Lockfile Updates: When and Why

Scenario 1: Adding a New Dependency

When you run uv add new-package, UV automatically updates uv.lock:

uv add requests
# uv.lock is updated automatically
git add pyproject.toml uv.lock
git commit -m "Add requests"

Scenario 2: Updating Existing Packages

After months, you want to update dependencies:

# Check what's available
uv pip compile --upgrade pyproject.toml

# Actually update (test thoroughly before committing!)
uv lock --upgrade

This re-resolves dependencies with latest versions (respecting constraints in pyproject.toml) and updates uv.lock.


Onboarding a New Team Member

New developer Alex joins the team:

# Step 1: Clone
git clone https://github.com/yourorg/web-scraper.git
cd web-scraper

# Step 2: Sync (recreates exact environment)
uv sync

# Step 3: Verify
uv run pytest

In 30 seconds, Alex has your exact environment. If tests pass, they're ready to develop.


Handling Lockfile Conflicts (Advanced)

Scenario: You added package-a on your branch. Maria added package-b on hers. Now uv.lock has a Git conflict.

Fix:

# Don't manually edit uv.lock. Instead:
git merge --abort # Undo merge
git pull # Get latest
uv lock # Regenerate lockfile with all changes
git add uv.lock
git commit -m "Merge dependencies from both branches"

Why: uv lock re-resolves all dependencies and creates a consistent lockfile automatically. Never manually edit uv.lock.


Git Integration: What to Commit, What to Ignore

Here's the complete picture of what goes to Git and what doesn't:

File/DirectoryCommit?Why
pyproject.tomlYESProject configuration; needed for dependency resolution
uv.lockYESExact versions; needed for reproducibility
.python-versionYESPython version specification; needed for consistency
src/YESYour code
.venv/NOEnvironment directory; 50+ MB, recreated by uv sync
__pycache__/NOPython cache; recreated when code runs
*.pycNOCompiled bytecode; recreated automatically
.envNO (with care)Secrets; use .env.example instead

UV creates .gitignore automatically when you run uv init, so most of this is handled for you.


The Collaboration Workflow Summary

Here's the complete team workflow with UV:

Developer A (You):
1. Create project with uv init
2. Add dependencies with uv add
3. Code and test
4. Commit pyproject.toml + uv.lock to Git

Developer B (Teammate):
1. Clone repository (gets pyproject.toml + uv.lock)
2. Run uv sync (recreates exact environment)
3. Code and test in identical environment
4. Add new dependency with uv add
5. Commit updated pyproject.toml + uv.lock

Developer C (New Team Member):
1. Clone repository
2. Run uv sync (3-second setup)
3. Run uv run pytest (verify everything works)
4. Start developing

No environment mismatches. No "works on my machine" problems. Every developer, every branch, every deployment—identical environment.


Try With AI

Use your AI companion (ChatGPT, Claude Code, Gemini CLI, etc.) for these exercises.

Prompt 1: Understanding Lockfiles

Explain the difference between pyproject.toml and uv.lock. Why do we need both files? What happens if my teammate only has pyproject.toml without the lockfile?

Expected outcome: You'll understand that pyproject.toml defines constraints (flexible) while uv.lock pins exact versions (reproducible), and why both are essential for team collaboration.

Prompt 2: Simulating Team Workflow

I have a UV project with requests==2.31.0. My teammate Maria adds pandas to the project and commits both pyproject.toml and uv.lock. Walk me through the complete workflow:
1. What did Maria do?
2. What do I need to do after pulling her changes?
3. How do we verify we have the same environment?

Expected outcome: You'll understand the complete cycle: add dependency → commit both files → pull → sync → verify.

Prompt 3: Handling Lockfile Conflicts

I added package-a on my branch. My teammate added package-b on their branch. Now we have a Git merge conflict in uv.lock. How do I resolve this safely? Why shouldn't I manually edit the lockfile?

Expected outcome: You'll learn to use uv lock to regenerate the lockfile instead of manually editing it during conflicts.