Introduction
pip's dependency resolver performs backtracking to find a set of compatible package versions, but complex dependency trees with conflicting version requirements can cause it to enter extremely long backtracking loops or fail entirely with ResolutionImpossible. This is common in projects with many dependencies where transitive dependencies require incompatible versions of shared packages (like requests, urllib3, or numpy). The resolver's default behavior tries every possible version combination, which can take hours for deeply nested dependency trees.
Symptoms
``` ERROR: Cannot install package-a==2.1 and package-b==3.0 because these package versions have conflicting dependencies.
The conflict is caused by: package-a 2.1 depends on requests>=2.25.0 package-b 3.0 depends on requests<2.25.0 ```
Or backtaking loop:
INFO: pip is looking at multiple versions of package-a to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of package-b to determine which version is compatible with other requirements. This could take a while.
# Continues for minutes or hoursCommon Causes
- Conflicting version pins: Two packages require incompatible versions of a shared dependency
- Transitive dependency conflicts: Indirect dependencies clash, not direct ones
- Outdated pip version: Old pip uses legacy resolver without backtracking
- Pre-release versions: Packages using alpha/beta versions not included by default
- Platform-specific dependencies: Different requirements on different OS
- Requirements file with loose constraints: >= without upper bounds allows incompatible versions
Step-by-Step Fix
Step 1: Identify the conflicting dependency
```bash # Use pip's verbose resolution pip install --dry-run -r requirements.txt 2>&1 | grep -A 10 "conflict"
# Show dependency tree pip install pipdeptree pipdeptree --reverse -p requests # Shows which packages depend on requests and their version requirements
# Show full tree pipdeptree --warn silence ```
Step 2: Use constraints to guide resolution
```bash # constraints.txt requests>=2.28.0,<2.32.0 numpy>=1.24.0,<1.27.0 urllib3>=1.26.0,<2.0.0
# Install with constraints pip install -r requirements.txt -c constraints.txt
# Or pin specific versions in requirements.txt requests==2.31.0 numpy==1.26.3 ```
Step 3: Use pip-compile for reproducible builds
```bash # Install pip-tools pip install pip-tools
# requirements.in (loose requirements) flask>=2.0 sqlalchemy>=1.4 requests
# Compile to locked requirements pip-compile requirements.in --output-file requirements.txt
# Output is a fully pinned requirements.txt with all transitive deps # flask==3.0.0 # # via -r requirements.in # werkzeug==3.0.1 # # via flask # requests==2.31.0 # # via -r requirements.in
# Update specific package pip-compile --upgrade-package requests ```
Prevention
- Use pip-tools (pip-compile) to generate fully pinned lock files
- Set upper bounds on critical shared dependencies in constraints files
- Run pip install --dry-run in CI to catch resolution failures before deployment
- Use pipdeptree to audit dependency trees before adding new packages
- Keep pip updated to benefit from improved resolution algorithms
- Prefer well-maintained packages with stable dependency requirements
- Consider using Poetry or uv for faster, more reliable dependency resolution