Updating Dependencies¶
depkeeper update applies the recommendations that check reports. It runs the same pipeline, then filters, previews, confirms and writes.
Full option list: CLI reference. Write mechanics: Write safety.
The safe sequence¶
depkeeper update --dry-run # 1. preview — writes nothing
depkeeper update --backup # 2. apply, keeping a timestamped copy
pip install -r requirements.txt # 3. verify the environment resolves
pytest # 4. verify the code still works
git diff requirements.txt # 5. review before committing
Steps 3 and 4 are not optional. depkeeper validates the packages you declared against each other; it does not build the full transitive graph. pip is the authority on whether the set installs.
Preview first¶
Update Plan (Dry Run)
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Package ┃ Current ┃ New Version ┃ Change ┃ Python Requires ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ requests │ 2.28.0 │ 2.34.2 │ minor │ >=3.10 │
│ certifi │ not specified │ 2026.7.22 │ new │ >=3.7 │
│ urllib3 │ 1.26.0 │ 1.26.20 │ patch │ !=3.0.*,... │
└──────────┴───────────────┴─────────────┴────────┴─────────────────┘
[WARNING]
Dry run mode - no changes applied
--dry-run executes everything except the write, so the plan is produced by the same code that would have written the file. Current: not specified marks a requirement that had no version specifier and will receive a pin.
Python Requires can be stale after conflict resolution
The column shows the requires_python recorded for the recommendation that the checker produced. If the resolver subsequently moved the package to a different version, this column is not refreshed. Verify with pip install rather than relying on it. Tracked in Known limitations.
Confirmation¶
Unless -y is passed, you are prompted:
- The default is yes — the plan has already been shown and you invoked
updateexplicitly. - Any input other than
y/ncauses Click to re-prompt. - Declining writes nothing and exits
0. Ctrl+Cat the prompt exits130.
Use -y in automation; keep the prompt interactively.
Selecting packages¶
depkeeper update -p flask
depkeeper update -p flask -p click -p requests
depkeeper update --packages Django # matched canonically
Names are compared in PEP 503 canonical form, so my_pkg, My.Pkg and my-pkg all select the same distribution. If no requirement matches, depkeeper prints [WARNING] No matching packages found: <names> and exits 0 without writing.
Selection filters the package list after conflict resolution, so the versions applied are still the mutually consistent ones. It does not re-resolve for the subset.
Range preservation (default)¶
Only the floor of a requirement moves. Everything else you declared is preserved:
requests==2.34.2
flask>=2.2.5,<2.3
celery[redis]>=5.6.3,<6.0
click~=8.4
certifi==2026.7.22
urllib3==1.26.20
The rules are in Version recommendation → Constraint preservation. Two behaviours worth internalising:
>is widened to>=so the selected version is itself installable.- A cap-only requirement gains a floor:
<3.0with target2.32.3becomes<3.0,>=2.32.3. The order is not normalised.
Skipped targets¶
If the resolver proposes a version your own constraints exclude, the package is skipped with an explanation rather than written as an unsatisfiable line:
[WARNING] Skipping flask: 2.3.3 is excluded by the declared constraint '<2.3'
(use --pin to replace the constraint)
Pin mode¶
Replaces the entire specifier set with an exact pin:
requests==2.34.2
flask==2.2.5
celery[redis]==5.6.3
click==8.4.2
certifi==2026.7.22
urllib3==1.26.20
Extras, markers, hashes-policy and comments are unaffected; only version semantics change.
Use --pin when | Avoid --pin when |
|---|---|
| The file describes a deployable application. | The file describes a library's dependencies. |
| You want reproducible installs from this file alone. | You rely on caps and exclusions as documentation of real incompatibilities. |
| The file is generated and regenerated wholesale. | The file is hand-maintained and reviewed. |
--pin also disables the declared-constraint check, because no declared constraint survives.
Hashed requirements¶
Requirements carrying --hash entries are refused by default:
[ERROR] Refusing to update requirement(s) with --hash entries: requests. Hashes are
version-specific and cannot be silently removed. Re-run with --allow-hash-removal to
proceed without hashes.
Exit code 1. Nothing is written.
The result is a partially hashed file
Only the lines that changed lose their digests. pip install --require-hashes rejects a file in which some requirements have hashes and others do not, so the resulting file will not install in hash-checking mode.
The supported workflow for hash-pinned files is:
Also note that pip-compile --generate-hashes output uses backslash line continuations by default, which depkeeper's parser does not join. See Requirements parsing → Known gaps.
Direct references are never updated¶
Editable installs and URL/VCS/local-path requirements are skipped silently (logged at DEBUG):
-e .
-e git+https://github.com/org/lib.git@v1.2.0#egg=lib
https://example.com/internal-1.0.tar.gz#egg=internal
./vendor/local-pkg
They are pinned to a source rather than a PyPI version; appending a specifier would produce an uninstallable line. Update these by editing the ref or URL yourself.
Multi-file updates¶
If your file uses -r includes, update writes every file that contains an updated requirement:
All affected files are rendered before any is written, and a failure part-way rolls back the files already committed. --backup backs up every affected file. Preview with --dry-run when the include graph is unfamiliar.
Backups¶
Creates requirements.<timestamp>_<uuid>.backup.txt next to each affected file before any write. If the update fails, depkeeper restores from these copies and reports:
Backups are never cleaned up automatically. In a git repository, git diff and git checkout are usually better tools; add *.backup.* to .gitignore if you use --backup anyway.
Restoring manually:
Conflict-aware updates¶
With conflict checking enabled (the default), the versions written are the resolver's output, and a warning precedes the plan when unresolved conflicts remain:
The plan can then legitimately contain a downgrade:
This rewrites flask>=2.3,<3.0 to flask>=2.2.5,<3.0 — the floor you declared is lowered. It is intentional (the alternative is a set that does not install), but it should never pass unreviewed. If you would rather fix it yourself, raise the other side's cap and re-run:
Idempotency¶
Re-running update on an already-updated file is a no-op:
Targets that would produce a byte-identical line are skipped, so click~=8.4 does not re-report 8.4.2 forever. This makes update safe to run on a schedule.
Failure modes¶
| Failure | Behaviour | Exit |
|---|---|---|
| File does not exist | Click usage error before depkeeper runs. | 2 |
| Parse error | Reported with line number and content; nothing written. | 1 |
| Hashed requirement without opt-in | Refused before any write. | 1 |
| Target excluded by declared constraints | That package is skipped with a warning; the rest proceed. | 0 |
| A file cannot be written | Committed files are rolled back; backups restored if present. | 1 |
| Network failure for one package | That package becomes an unavailable stub and is not updated; the run continues. | 0 |
| Total network failure | Every package becomes a stub; nothing to update. | 0 |
Ctrl+C | Aborts. Files are either fully old or fully new. | 130 |
Recovery¶
| You want to | Do |
|---|---|
| Undo an update, git-tracked | git checkout -- requirements.txt |
Undo an update, --backup used | Copy the .backup. file back over the original. |
| Undo an update, neither | Re-derive: pin the previous versions manually. depkeeper keeps no history. |
| Verify what changed | git diff requirements.txt before installing. |