Dependencies are necessary. Dependency hell is not. Here's how to stay sane.
Pin or Range?
Pinning: Lock to exact versions
requests==2.31.0
Ranging: Allow compatible versions
requests>=2.28,<3.0
My approach: Range in pyproject.toml, pin in lockfile.
# pyproject.toml - flexible
[project]
dependencies = [
"requests>=2.28",
"pydantic>=2.0,<3.0",
]# requirements.lock - exact
requests==2.31.0
pydantic==2.6.1
Development uses the lockfile for reproducibility. The ranges allow flexibility when others depend on your package.
The Lockfile
Generate a lockfile for reproducible builds:
pip freeze > requirements.lockOr better, use pip-tools:
pip-compile pyproject.toml -o requirements.lockCommit the lockfile. CI and production install from it:
pip install -r requirements.lockUpdate Strategy
Don't: Update everything at once
pip install --upgrade * # Recipe for breakageDo: Update incrementally
# Update one package
pip install --upgrade requests
pip freeze > requirements.lock
# Test
# CommitSchedule updates:
- Security patches: immediately
- Minor versions: weekly
- Major versions: monthly, with testing
Dependabot / Renovate
Automate dependency updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5PRs arrive automatically. Review, test, merge.
Avoiding Dependency Hell
Minimize dependencies. Every package is a risk. Do you need requests or will urllib work?
Check maintenance status. Last commit 3 years ago? Find an alternative.
Audit transitive dependencies. Your 5 direct dependencies might pull in 50 more:
pip show requests | grep Requires
pipdeptreeWatch for conflicts. If package A needs foo>=2.0 and package B needs foo<2.0, you're stuck.
Version Constraints
package>=1.0 # Minimum version
package>=1.0,<2.0 # Range (recommended for libs)
package~=1.4 # Compatible release (>=1.4, <2.0)
package==1.4.* # Prefix match
package==1.4.2 # Exact (for lockfiles)
For libraries you publish: use ranges. For apps you deploy: use lockfiles.
Security Scanning
Check for known vulnerabilities:
pip-audit
safety checkAdd to CI:
- name: Security scan
run: pip-audit --require-hashes -r requirements.lockWhen Dependencies Break
- Check the changelog. What changed?
- Pin to last working version. Buy time.
- Report the issue. Help the maintainer.
- Fork if necessary. Last resort.
# Temporary pin while upstream is broken
dependencies = [
"broken-package==1.2.3", # TODO: unpin after issue #123 fixed
]My Workflow
- Add dependency to
pyproject.tomlwith range - Regenerate lockfile with
pip-compile - Test that everything works
- Commit both files together
- Review Dependabot PRs weekly
Tools I Use
- pip-tools: Compile
pyproject.tomlto lockfile - pip-audit: Security scanning
- pipdeptree: Visualize dependency tree
- Dependabot: Automated updates
The Goal
Dependencies should be:
- Explicit: Listed in one place
- Reproducible: Same versions everywhere
- Updatable: Easy to bump versions
- Secure: No known vulnerabilities
Get this right and dependencies become boring. Boring is good.