Unit 02.04: Pinning what you depend on
A loose version pin means the build that passed CI is not the build that deployed.
Exact pins and a lock file
Four dependency specifications, two loose and two exact.
The code lists them with the risk.
PINNING = [
("fastapi", ">=0.100", "loose", "minor releases change behaviour"),
("fastapi", "==0.140.13", "exact", "reproducible"),
("pydantic", ">=2", "loose", "v1 to v2 was a rewrite"),
("uvicorn[standard]", "==0.40.0", "exact", "reproducible"),
]
print(f"{'package':20} {'spec':14} {'kind':7} note")
for package, spec, kind, note in PINNING:
print(f"{package:20} {spec:14} {kind:7} {note}")
print("""
Pin exactly, including transitive dependencies via a lock file. A loose pin
means the build that passed CI on Tuesday is not the build that deployed on
Thursday, and the difference is invisible in your diff.
Upgrade deliberately, one package at a time, with the test suite as the gate.
""")
The Pydantic row is the cautionary one: version 1 to version 2 was effectively a rewrite, and a specification of >=2 would have accepted it. Frameworks in this area move quickly and change behaviour in minor releases.
The difference is invisible in your diff. Nothing in your commit history records that a transitive dependency changed between Tuesday and Thursday.
The mistake this prevents
The mistake is pinning direct dependencies and letting transitive ones float. Most of your installed packages are transitive, and any of them can change behaviour - so the lock file is what actually makes the build reproducible.
Takeaway
Pin exactly, including transitive dependencies via a lock file, and upgrade one package at a time with the test suite as the gate.
