šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Dependency Management

The difference between declaring dependencies and locking them — and why 'it works on my machine' is a dependency-management failure, not bad luck.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

Does httpx>=0.27 in pyproject.toml guarantee the exact same version is installed every time?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Declaring `httpx>=0.27` in pyproject.toml tells pip what's acceptable, not what's actually installed. Two developers running that same declaration weeks apart can end up with different transitive dependency versions entirely. Lock files close that gap — this lesson explains why they exist and how to use one correctly.

1Ranges Are Constraints, Not Guarantees

dependencies = ["httpx>=0.27"] in pyproject.toml expresses a genuine intent — 'any version 0.27 or newer is acceptable' — but it is deliberately *not* a pin to one exact version. This flexibility is useful at the declaration level: it lets your package remain installable alongside other packages that might require a slightly different but still-compatible version of the same dependency, rather than forcing an exact-version conflict.

The cost of that flexibility is that dependency *resolution* — the actual process of picking one specific version that satisfies every constraint across your entire dependency graph — can legitimately produce a different result at different points in time, purely because new compatible releases keep being published. Installing your project today might resolve httpx to 0.27.0; installing the exact same pyproject.toml, unchanged, three weeks later might resolve it to 0.27.2, simply because that newer, still-compatible version now exists and the resolver has no reason to prefer the older one.

This is the direct cause of a large fraction of 'works on my machine' bugs: two developers, or a developer's laptop versus CI, running pip install against the exact same declared ranges at different times, ending up with a subtly different resolved dependency tree — and one of those trees happens to trigger a bug the other doesn't.

āœ•
—
+
# pyproject.toml
[project]
dependencies = ["httpx>=0.27"]

# Week 1: resolves to httpx 0.27.0
# Week 3: a new httpx 0.27.2 is released -> resolves to 0.27.2 instead
# Both installs technically satisfy ">=0.27", but they're not IDENTICAL
localhost:3000
Resolution Drift
Same pyproject.toml, different install times
Can resolve to different actual installed versions

2Transitive Dependencies Multiply the Problem

Your project's *direct* dependencies — the ones you actually wrote in [project.dependencies] — are typically a short, manageable list. But each of those dependencies has its own dependencies (httpx depends on httpcore, which depends on h11, and so on), and each of *those* is typically declared as a range too. The full resolved dependency tree for even a modest project routinely includes dozens of transitive packages you never directly named and may never have heard of.

A resolver's job is to find one specific version for every package in that entire tree that simultaneously satisfies every constraint from every other package that depends on it — a genuinely hard constraint-satisfaction problem, which is why modern resolvers (uv, poetry, pip's newer resolver) can take real, sometimes noticeable time on a large dependency graph, and why two different resolver runs, even with identical inputs, could theoretically make different valid choices when multiple versions satisfy all constraints equally.

This is precisely why relying on 'just run pip install from pyproject.toml every time' does not give you reproducibility: you're asking the resolver to redo this entire constraint-satisfaction process fresh, against a set of available package versions that keeps growing on PyPI over time, every single time you install.

āœ•
—
+
httpx>=0.27
  └─ httpcore (range)
       └─ h11 (range)

# Your direct dependency is one line.
# The FULL resolved dependency tree can be 30+ packages.
localhost:3000
Dependency Tree
A handful of direct deps
→ can resolve to 30+ packages in the full tree

3Lock Files: Freezing the Resolution, Not Just the Declaration

A lock file (uv.lock, poetry.lock, or a pip-compiled requirements.txt with pinned versions) is the *output* of running dependency resolution once, recorded exactly: every package in the full tree, direct and transitive, pinned to one specific version, typically alongside a cryptographic hash of the exact package artifact. It is not something you write by hand — it's generated by your dependency-management tool from your declared ranges, and then committed to version control alongside pyproject.toml.

The critical workflow change this enables: instead of pip install . (which re-resolves from ranges every time), you run an install command that reads *from the lock file* — uv sync, or pip install -r requirements.lock.txt with pinned pip-compile output — which skips resolution entirely and installs exactly the versions the lock file specifies. Two machines, two points in time, running that same lock-file-based install command, get byte-for-byte identical dependency trees, hashes and all.

The hash verification specifically defends against a subtler risk than version drift: it guarantees the *exact bytes* of each installed package match what was originally resolved and tested, protecting against a compromised or tampered package artifact being silently substituted — a real supply-chain security consideration, not just a reproducibility one. The professional workflow is: declare ranges in pyproject.toml for flexibility, regenerate the lock file deliberately when you want to pick up updates, and always install from the lock file everywhere else — local dev, CI, and production.

āœ•
—
+
# uv.lock (simplified concept)
[[package]]
name = "httpx"
version = "0.27.0"
hash = "sha256:..."

[[package]]
name = "httpcore"
version = "1.0.5"
hash = "sha256:..."
localhost:3000
Reproducible Install
uv sync (from uv.lock)
Identical dependency tree on every machine, every time

4Step-by-Step Breakdown

"Works on my machine" is almost always a dependency-locking problem wearing a different name. Let's fix that permanently.

A version RANGE like httpx>=0.27 is a constraint, not an exact version. Two installs weeks apart can resolve to different actual versions.

Checkpoint: Does httpx>=0.27 in pyproject.toml guarantee the exact same version is installed every time?

  • →No — it only guarantees a version satisfying that range, which can differ across installs over time
  • →Yes — >= always resolves to the exact same version once written

Transitive dependencies compound this: httpx itself depends on other packages, each with their own ranges — the resolved tree can shift even more.

A lock file pins the EXACT resolved version (and hash) of every package in the tree, direct and transitive — reproducible, byte for byte.

Installing FROM a lock file (not just from pyproject.toml directly) is what makes two machines end up byte-for-byte identical.

Checkpoint: Why install from a lock file rather than directly from the version ranges in pyproject.toml for reproducible environments?

  • →A lock file pins exact versions (and hashes) for the entire dependency tree, guaranteeing identical installs
  • →Installing from a lock file is always significantly faster, which is the main reason

With dependencies declared and locked reproducibly, virtual environments are the last piece — isolating this project's dependencies from every other project on the same machine.

Reproduce a Real Non-Reproducible Install. Finish resolve_at_time(): the same range constraint can resolve differently at different points in time.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Commit the lock file to version control alongside pyproject.toml

The lock file is what actually guarantees reproducibility across developers and CI — without it committed, everyone is silently re-resolving from ranges independently, reintroducing the exact drift problem locking exists to solve.

Install from the lock file in CI and production, not by re-resolving from pyproject.toml directly

Use uv sync (or the equivalent pinned-install command for your tool) so every environment — local, CI, production — installs the exact same resolved, hash-verified dependency tree.

Frequent Bugs

THE BUG

Committing pyproject.toml but not the generated lock file, so CI and every developer's machine independently re-resolve dependencies and can silently diverge.

THE FIX

Always commit the lock file (uv.lock, poetry.lock, or equivalent) to version control, and configure CI to install from it rather than re-resolving fresh.

Real-World Examples

Diagnosing a "Works on My Machine" Bug via Dependency Drift

A bug reproduces in CI but not locally, and the team eventually discovers the two environments have different transitive versions of a logging library, despite identical pyproject.toml files, because the lock file was never committed.

# Diagnosis:
$ pip freeze > local-versions.txt   # on the working machine
$ diff local-versions.txt ci-versions.txt
> structlog==24.2.0     (local)
> structlog==24.4.0     (CI, resolved independently, weeks apart)

# Fix: commit a lock file and have both environments install from it

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Adding .lock files to .gitignore (mistaking them for a build artifact), causing every environment to re-resolve dependencies independently and silently drift apart.

# Wrong: .gitignore *.lock __pycache__/ .venv/ # Correct: only ignore genuinely generated/local artifacts __pycache__/ .venv/ # uv.lock is committed, NOT ignored

The Solution //

Never gitignore the lock file — commit uv.lock, poetry.lock, or your tool's equivalent alongside pyproject.toml; it is the artifact that actually guarantees reproducibility.

Lesson Glossary

[01]Version range

A dependency constraint (e.g. >=0.27) specifying which versions are acceptable, without pinning to one exact version.

Code Preview
// Version range context

[02]Transitive dependency

A dependency of one of your project's own dependencies, not declared directly by you but required for the tree to resolve.

Code Preview
// Transitive dependency context

[03]Dependency resolution

The process of finding one specific version for every package in a dependency tree that satisfies all declared constraints simultaneously.

Code Preview
// Dependency resolution context

[04]Lock file

A generated file (e.g. uv.lock) recording the exact resolved version and hash of every direct and transitive dependency, enabling reproducible installs.

Code Preview
// Lock file context

Continue Learning