Step 1: Understanding the Error
When migrating from pip-tools to uv, you'll often encounter dependency resolution failures that look like this:
$ uv pip compile requirements.in -o requirements.txt
error: Failed to resolve dependencies
Caused by: No solution found when resolving dependencies:
╰─▶ Because django==3.2.0 depends on sqlparse>=0.2.2 and
sqlparse==0.1.9 is available, django==3.2.0 cannot be satisfied.
This error appears even though the same requirements.in file worked perfectly with pip-tools. The migration breaks because uv has stricter dependency resolution rules and handles version constraints differently than pip-tools.
Here's a typical scenario that triggers this error:
# requirements.in (worked with pip-tools)
django==3.2.0
sqlparse==0.1.9 # Pinned to old version for legacy reasons
psycopg2-binary>=2.8
redis>=3.5.0
celery>=5.0.0
Running the pip-tools command succeeded:
$ pip-compile requirements.in # This worked fine
But uv fails immediately:
$ uv pip compile requirements.in # Throws dependency conflict error
Step 2: Identifying the Cause
The root cause stems from three key differences between pip-tools and uv:
Resolution Algorithm Differences: pip-tools uses pip's resolver which can be more lenient with conflicts, sometimes allowing incompatible versions to coexist. uv uses a stricter SAT solver that catches all dependency conflicts upfront.
# Check your current pip-tools generated lock file
$ cat requirements.txt | grep -E "django|sqlparse"
django==3.2.0
sqlparse==0.1.9 # pip-tools allowed this despite django needing >=0.2.2
Version Specifier Interpretation: uv interprets version specifiers more strictly. Let's reproduce the exact conflict:
# test_dependencies.py
import subprocess
import tempfile
import os
# Create a test requirements file
test_requirements = """
django==3.2.0
sqlparse==0.1.9
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.in', delete=False) as f:
f.write(test_requirements)
temp_file = f.name
try:
# Try with uv
result = subprocess.run(
['uv', 'pip', 'compile', temp_file],
capture_output=True,
text=True
)
print("uv output:", result.stderr)
finally:
os.unlink(temp_file)
Hidden Transitive Dependencies: pip-tools sometimes ignores transitive dependency conflicts that uv catches:
# View the full dependency tree with uv
$ uv pip tree --package django
django==3.2.0
├── asgiref>=3.3.2,<4
├── pytz
└── sqlparse>=0.2.2 # Here's the conflict!
Step 3: Implementing the Solution
Solution 1: Update Conflicting Packages
The cleanest approach updates the conflicting package versions:
# requirements.in (fixed version)
django==3.2.0
sqlparse>=0.2.2 # Updated to match django's requirement
psycopg2-binary>=2.8
redis>=3.5.0
celery>=5.0.0
# Generate new lock file with uv
$ uv pip compile requirements.in -o requirements.txt
# Verify the resolution
$ uv pip compile requirements.in --resolution=highest # Use highest compatible versions
Solution 2: Use Override Dependencies
When you must keep specific versions, use uv's override feature:
# requirements.in
django==3.2.0
psycopg2-binary>=2.8
redis>=3.5.0
celery>=5.0.0
# overrides.txt (new file)
sqlparse==0.4.4 # Force a specific compatible version
# Apply overrides during compilation
$ uv pip compile requirements.in \
--override overrides.txt \
-o requirements.txt
Solution 3: Gradual Migration with Compatibility Mode
For large projects, migrate gradually using a compatibility wrapper:
# migration_helper.py
import subprocess
import sys
import json
def migrate_requirements(input_file, output_file):
"""
Migrate pip-tools requirements to uv format
with automatic conflict resolution
"""
# First, try direct compilation
try:
result = subprocess.run(
['uv', 'pip', 'compile', input_file, '-o', output_file],
capture_output=True,
text=True,
check=True
)
print(f"✓ Successfully compiled {input_file}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Initial compilation failed: {e.stderr}")
# Parse error and attempt auto-fix
if "cannot be satisfied" in e.stderr:
# Extract conflicting packages from error message
lines = e.stderr.split('\n')
conflicts = []
for line in lines:
if "depends on" in line and ">=":
# Parse the constraint
parts = line.split("depends on")
if len(parts) == 2:
constraint = parts[1].strip()
conflicts.append(constraint)
# Create temporary override file
with open('temp_overrides.txt', 'w') as f:
for conflict in conflicts:
# Extract package and version
if '>=' in conflict:
pkg = conflict.split('>=')[0].strip()
# Use latest version as override
f.write(f"{pkg}>=0.0.0\n")
# Retry with overrides
try:
result = subprocess.run(
['uv', 'pip', 'compile', input_file,
'--override', 'temp_overrides.txt',
'-o', output_file],
capture_output=True,
text=True,
check=True
)
print(f"✓ Compiled with overrides")
return True
except subprocess.CalledProcessError as e2:
print(f"✗ Failed even with overrides: {e2.stderr}")
return False
# Usage
if __name__ == "__main__":
migrate_requirements('requirements.in', 'requirements.txt')
Step 4: Working Code Example
Here's a complete migration script that handles common pitfalls:
# uv_migration.py
#!/usr/bin/env python3
"""
Complete migration script from pip-tools to uv
Handles dependency conflicts automatically
"""
import os
import subprocess
import re
from pathlib import Path
class UvMigrator:
def __init__(self, requirements_in='requirements.in'):
self.requirements_in = Path(requirements_in)
self.requirements_txt = Path('requirements.txt')
self.backup_file = Path('requirements.backup.txt')
def backup_existing(self):
"""Create backup of existing requirements.txt"""
if self.requirements_txt.exists():
self.requirements_txt.rename(self.backup_file)
print(f"✓ Backed up existing requirements to {self.backup_file}")
def analyze_conflicts(self):
"""Analyze potential conflicts before migration"""
print("Analyzing dependency conflicts...")
# Try compilation to detect conflicts
result = subprocess.run(
['uv', 'pip', 'compile', str(self.requirements_in), '--dry-run'],
capture_output=True,
text=True
)
conflicts = []
if result.returncode != 0:
# Parse error message for conflicts
error_lines = result.stderr.split('\n')
for line in error_lines:
if 'depends on' in line:
# Extract package names and versions
match = re.search(r'(\w+)==([\d.]+) depends on (\w+)([><=]+)([\d.]+)', line)
if match:
conflicts.append({
'package': match.group(1),
'version': match.group(2),
'dependency': match.group(3),
'constraint': match.group(4) + match.group(5)
})
return conflicts
def fix_conflicts(self, conflicts):
"""Generate override file for conflicts"""
if not conflicts:
return None
override_file = Path('uv_overrides.txt')
with open(override_file, 'w') as f:
f.write("# Auto-generated overrides for uv migration\n")
for conflict in conflicts:
# Use the constraint from the error
f.write(f"{conflict['dependency']}{conflict['constraint']}\n")
print(f"✓ Generated override file: {override_file}")
return override_file
def compile_requirements(self, override_file=None):
"""Compile requirements with uv"""
cmd = ['uv', 'pip', 'compile', str(self.requirements_in),
'-o', str(self.requirements_txt)]
if override_file:
cmd.extend(['--override', str(override_file)])
# Add resolution strategy
cmd.extend(['--resolution', 'highest'])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"✓ Successfully compiled requirements with uv")
return True
else:
print(f"✗ Compilation failed: {result.stderr}")
return False
def verify_installation(self):
"""Verify the new requirements can be installed"""
print("Verifying installation...")
# Create virtual environment for testing
test_env = Path('.test_env')
subprocess.run(['python', '-m', 'venv', str(test_env)], check=True)
# Install requirements in test environment
pip_cmd = str(test_env / 'bin' / 'pip')
result = subprocess.run(
[pip_cmd, 'install', '-r', str(self.requirements_txt)],
capture_output=True,
text=True
)
# Clean up test environment
import shutil
shutil.rmtree(test_env)
if result.returncode == 0:
print("✓ Requirements can be installed successfully")
return True
else:
print(f"✗ Installation verification failed: {result.stderr}")
return False
def migrate(self):
"""Execute complete migration"""
print(f"Starting migration from pip-tools to uv for {self.requirements_in}")
# Step 1: Backup
self.backup_existing()
# Step 2: Analyze conflicts
conflicts = self.analyze_conflicts()
if conflicts:
print(f"Found {len(conflicts)} potential conflicts")
for c in conflicts:
print(f" - {c['package']}=={c['version']} → {c['dependency']}{c['constraint']}")
# Step 3: Fix conflicts
override_file = self.fix_conflicts(conflicts)
# Step 4: Compile
success = self.compile_requirements(override_file)
if success:
# Step 5: Verify
if self.verify_installation():
print("\n✓ Migration completed successfully!")
print(f" New requirements: {self.requirements_txt}")
if override_file:
print(f" Override file: {override_file}")
else:
print("\n⚠ Migration completed but verification failed")
print(f" You may need to manually adjust {self.requirements_txt}")
else:
print("\n✗ Migration failed")
print(f" Restoring backup from {self.backup_file}")
self.backup_file.rename(self.requirements_txt)
return success
# Run migration
if __name__ == "__main__":
migrator = UvMigrator()
migrator.migrate()
Step 5: Additional Tips & Related Errors
Common Related Errors and Fixes
Error: "No solution found when resolving dependencies"
# Add verbose output to see detailed resolution process
$ uv pip compile requirements.in -v
# Use lower-bounds resolution for more flexibility
$ uv pip compile requirements.in --resolution=lowest
Error: "Package requires Python version"
# Specify Python version explicitly
$ uv pip compile requirements.in --python-version 3.9
# Or use current Python
$ uv pip compile requirements.in --python-platform linux
Error: "Hash mismatch in lock file"
# Regenerate hashes
$ uv pip compile requirements.in --generate-hashes
# Verify hashes match
$ uv pip sync requirements.txt --require-hashes
Performance Optimization
uv is significantly faster than pip-tools. Leverage this speed:
# Parallel downloads (default in uv)
$ uv pip compile requirements.in --jobs 4
# Use cache effectively
$ uv pip compile requirements.in --cache-dir ~/.cache/uv
# Skip pre-releases unless needed
$ uv pip compile requirements.in --no-prerelease
Debugging Complex Dependencies
When dealing with complex dependency trees:
# dependency_analyzer.py
import subprocess
import json
def analyze_package(package_name):
"""Analyze a package's dependencies with uv"""
# Get package info
result = subprocess.run(
['uv', 'pip', 'show', package_name],
capture_output=True,
text=True
)
if result.returncode == 0:
print(f"Package: {package_name}")
print(result.stdout)
# Get dependency tree
tree_result = subprocess.run(
['uv', 'pip', 'tree', '--package', package_name],
capture_output=True,
text=True
)
print("\nDependency Tree:")
print(tree_result.stdout)
# Check problematic packages
analyze_package('django')
The migration from pip-tools to uv requires careful attention to dependency conflicts, but the performance improvements and stricter resolution make it worthwhile. Remember to always test your migrations in a separate environment before deploying to production.