Python Dependency Management¶
Purpose¶
Define strict, repeatable rules for Python dependency management to reduce upgrade risk while keeping dependencies current.
Scope¶
These rules apply to Python library dependencies managed with pyproject.toml,
uv.lock, and requirements exports derived from the lock file.
Legacy dependency tooling is deprecated. Repositories migrating from non-uv
tooling may temporarily retain legacy lockfiles, but must document the
exception in their repository overlay and remove the legacy tooling once
uv.lock is in use.
Sources of truth¶
pyproject.tomldeclares allowed dependency ranges.uv.lockpins exact versions compiled from those ranges.- Requirements files are exported from
uv.lockand must never drift from it.
Version specification rules¶
- Default dependency specs in
pyproject.tomluse*. - Do not anchor to a major or minor series by default (including pre-1.0 dependencies).
- Use constrained specs only when necessary and document them using the anchored dependency workflow.
- Avoid patch-level pinning in
pyproject.tomlunless an explicit exception is approved.
Example default specification:
Upgrade workflow¶
Patch-level¶
The first action after incrementing the application PATCH version is to
refresh dependencies.
Workflow:
- Increment
PATCHper the application versioning scheme. - Run
uv lock --upgradeto refreshuv.lockwithin the existing constraints. - Export requirements files from
uv.lockwhere required (for example,uv export --format requirements.txt --output-file requirements.txt). - Run the full validation and test suite (define the canonical command per repository).
- If validation passes, the lockfile versions remain fixed for the rest of the
PATCHcycle unless an exception is approved.
Do not change explicit version constraints in pyproject.toml as part of this
cycle-opening update.
Minor- or major-level¶
When incrementing the application MINOR or MAJOR version, perform the patch-level
workflow and also attempt to move toward the latest available dependency
releases when constraints have been tightened.
Workflow:
- Identify dependencies pinned below the current minor series (for example,
constrained to
1.1when1.2exists). - For each dependency, relax the constraint individually and run the full validation and test suite.
- If multiple dependencies were relaxed successfully, run the full validation and test suite with all relaxations combined.
- Identify dependencies anchored to a major version when a newer major
release exists (for example, constrained to
<4when5.xis available). - For each major upgrade candidate, expand the constraint individually and run the full validation and test suite.
- If multiple major upgrades are viable, run the full validation and test suite with all major upgrades combined.
The expected steady state is unconstrained (*) unless a documented exception
requires anchoring. If no newer major release exists, there is nothing to test.
If a dependency upgrade fails, determine root cause before deciding to pin or
defer. A regression in the dependency is a valid reason to stay on the prior
major version (for example, pinning pylint to the latest 3.x when 4.0.0
introduces a blocking bug).
In-cycle exception rules¶
Dependencies may change during a PATCH cycle only when necessary:
- New functionality requires additional dependencies.
- A dependency bug impacts the application and requires an upgrade or pin.
Each exception must:
- include a written rationale in the pull request
- minimize the scope of the dependency change
- update
uv.lockand any exported requirements - complete the full validation and test suite
Handling regressions and non-latest pins¶
When a uv lock --upgrade introduces failures:
- determine root cause before deciding to pin
- do not assume the dependency is at fault
- verify whether the application is compliant with the dependency's documented API and behavior
Pinning to a non-latest version is acceptable only when:
- a regression or compatibility break in the dependency is verified, and no fix is available within the current cycle, or
- the application depends on behavior removed or corrected upstream and a migration is required
If the application is at fault, fix the application and re-run the update instead of pinning.
Every pin must be accompanied by:
- a written rationale and evidence
- a clear exit condition and planned removal
- a review at the next
PATCHcycle
Anchored dependency documentation¶
When a dependency is anchored below the latest acceptable range, document it in two places:
pyproject.tomlcomment immediately above the dependency specification, noting the latest version that failed, pointing to the dependency record, and linking the related GitHub issue.- A dependency-specific record in
docs/dependencies/that captures failure evidence and preserves history across attempts.
Update both records every time an upgrade is tested and fails. Do not replace or delete prior failure evidence.
Example pyproject.toml comment:
# Anchor: 1.2.5 fails full test suite; issue https://github.com/<org>/<repo>/issues/123;
# see docs/dependencies/example-lib.md
example-lib = ">=1.1,<2.0"
Dependency record requirements:
- One file per dependency:
docs/dependencies/<dependency-name>.md. - Record the failing version and a concise description of the failure.
- Capture failure evidence (test command, error excerpt, and context).
- Append a new entry for each failed re-test.
- Keep the latest attempted version in the
pyproject.tomlcomment.
See Dependency anchor records for the required format.
Pre-release dependencies¶
Pre-release dependencies are allowed only as narrow, explicitly approved exceptions.
Rules:
- Pre-releases are allowed only in non-production environments.
- Production use requires an explicit, documented override (mechanism TBD).
- Pre-releases must be specified as exact versions only; no ranges or pre-release wildcard allowances are permitted.
- A pre-release dependency can only be added or removed with explicit human confirmation.
- The dependency must be tracked in a GitHub issue. If no issue exists, create one before adding the dependency.
pyproject.tomlmust include a comment immediately above the dependency specification that includes the issue URL.
During the start-of-cycle dependency update, always pause to confirm with a human owner that any pre-release dependency is still required.
When removing a pre-release dependency:
- If the issue was created solely to track the dependency, close it.
- If the issue tracks broader work, add a comment noting the removal.
Security-driven updates¶
If a vulnerability scan fails during a development cycle, update dependencies immediately:
- Update
pyproject.tomlas required. - Regenerate
uv.lock. - Re-export any requirements files derived from the lockfile.
- Run a dependency audit against the exported requirements before committing.
Locked dependency review¶
At the start of each upgrade cycle (PATCH, MINOR, or MAJOR), review any
pinned or tightly constrained dependencies and confirm each pin is still
required. Remove unnecessary pins before completing the cycle-opening update.
Enforcement¶
Violations are fatal exceptions that block merges, releases, and deployments.
CI must fail when:
uv.lockis out of sync withpyproject.toml- a dependency spec is constrained without documented justification
- a dependency anchor lacks the required
pyproject.tomlcomment - a dependency anchor lacks a record in
docs/dependencies/ - a pre-release dependency is used without an exact version pin
- a pre-release dependency lacks an issue link in
pyproject.toml - a pre-release dependency is included in production without a documented override
- dependency updates occur after a vulnerability scan failure without a documented dependency audit
- dependency updates occur without the required validation run
Examples (TODO)¶
- Default
*specifications and constrained exceptions. - Patch-cycle update checklist in practice.
- Justified pinning due to upstream regression.
- Dependency addition for new functionality.
Related documents¶
- Application versioning scheme: application-versioning-scheme.md