Stop Using requirements.txt
The requirements.txt is a legacy dependency management tool that is no longer fit for modern Python projects. We need a better dependency management tool.
It’s 3 AM, and your production deployment just failed. Again. The same code that worked perfectly on your local machine is now throwing mysterious import errors on the server. Sound familiar?
The 3 AM Production Nightmare
Let me paint you a picture that might feel uncomfortably familiar. You’re running a Flask web application in production using Docker containers. Your team just deployed what should have been a routine update—running your test suite with pytest to ensure everything works before the new release.
Then the container crashes on startup.
ImportError: cannot import name 'url_quote' from 'werkzeug.urls'
(/opt/conda/lib/python3.10/site-packages/werkzeug/urls.py)
Your heart sinks. The exact same code that runs perfectly when you execute python run.py is now failing during pytest execution. You haven’t changed any Flask or Werkzeug code—so what went wrong?
Your requirements.txt looked innocent enough:
Flask==2.2.2
pytest>=7.0.0
# ... other dependencies
The problem? Flask 2.2.2 specified Werkzeug>=2.2.0 in its dependencies, which seemed reasonable. Your local environment had been running Werkzeug 2.3.7 for weeks without issues. But when pytest was installed fresh in the Docker container, it pulled in the latest available Werkzeug version: 3.0.0.
Here’s the kicker: Werkzeug 3.0.0 removed the deprecated url_quote function that Flask 2.2.2 still relied on. Your “compatible” dependency specification had just broken your entire application.
The impact was immediate and severe. Your CI/CD pipeline was blocked, preventing any deployments. The development team couldn’t run tests locally after pulling the latest changes. What should have been a 5-minute deployment turned into a 3-hour emergency debugging session, with the fix being a simple line: Werkzeug==2.3.7.
This scenario plays out in Python teams worldwide, every single day. The root cause isn’t developer incompetence or bad luck—it’s the fundamental limitations of requirements.txt as a dependency management solution. What worked fine for simple scripts in 2008 simply cannot handle the complexity of modern Python applications with dozens of dependencies, each with their own sub-dependencies and version constraints.
The good news? There’s a better way, and migrating is easier than you might think.
The Fundamental Problems with requirements.txt
Dependency Resolution Hell
The most critical flaw in requirements.txt is its complete lack of intelligent dependency resolution. When you run pip install -r requirements.txt, pip installs packages in the order they appear, attempting to satisfy version constraints as it goes. But it has no global view of all requirements and cannot backtrack when conflicts arise.
Consider this real-world scenario:
# requirements.txt
django==4.2.0
django-extensions==3.2.1
celery==5.3.0
kombu==5.2.4
This looks reasonable, but there’s a hidden time bomb. django-extensions 3.2.1 requires Django>=3.2,<4.2, while you’ve specified Django==4.2.0. The installation might succeed if Django is installed first, but you’re running with an unsupported configuration that could break at any time.
Poetry’s dependency resolver would catch this immediately:
$ poetry add django==4.2.0 django-extensions==3.2.1
Because django-extensions (3.2.1) depends on Django (>=3.2,<4.2)
and no versions of django-extensions match >3.2.1,<4.0.0,
django-extensions is forbidden.
So, because your-project depends on django-extensions (^3.2.1),
version solving failed.
This upfront conflict detection prevents the production surprises that plague requirements.txt users.
Reproducible Environment Issues
requirements.txt only captures your direct dependencies, not the entire dependency tree. This creates a reproducibility nightmare that every Python developer has experienced.
When you install requests==2.31.0, you’re also installing:
requests==2.31.0
├── certifi>=2017.4.17
├── charset-normalizer>=2.0.0,<4
├── idna>=2.5,<4
└── urllib3>=1.21.1,<3
Your requirements.txt doesn’t capture these sub-dependency versions. Six months later, when urllib3 releases version 2.1.0 with breaking changes, your production deployment might install this newer version while your development environment still runs the older one.
The “works on my machine” syndrome isn’t a developer joke—it’s a systematic failure of dependency management that costs teams thousands of hours annually.
Development vs Production Separation
Most Python projects struggle with cleanly separating development tools from production dependencies. You end up with solutions like:
# requirements.txt (production)
flask==3.0.0
sqlalchemy==2.0.23
# requirements-dev.txt (development)
-r requirements.txt
pytest==7.4.3
black==23.12.0
mypy==1.7.1
This approach has several problems. First, it’s easy to accidentally install development tools in production or miss them in development. Second, managing two files with overlapping content creates maintenance overhead. Third, there’s no standardization—every project uses different conventions.
Modern Alternatives: Poetry and uv
The Python ecosystem has evolved significantly, and two tools have emerged as superior alternatives: Poetry (mature and stable) and uv (cutting-edge and fast).
Poetry: The Mature Solution
Poetry revolutionized Python dependency management by bringing patterns from other ecosystems (like Rust’s Cargo and Node’s npm) to Python.
Key Features:
- Intelligent dependency resolution with proper conflict detection
- Lock files (
poetry.lock) ensuring reproducible environments - Clear separation between dependencies and dev-dependencies
- Standardized
pyproject.tomlconfiguration - Virtual environment management built-in
Installation:
curl -sSL https://install.python-poetry.org | python3 -
Basic Usage:
# Initialize a new project
poetry init
# Add dependencies
poetry add requests flask
# Add dev dependencies
poetry add --group dev pytest black mypy
# Install all dependencies
poetry install
# Run commands in the virtual environment
poetry run python app.py
pyproject.toml Structure:
[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A fantastic Python package"
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.31.0"
flask = "^3.0.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.0"
mypy = "^1.7.1"
Poetry’s lock file captures the exact versions of all dependencies and sub-dependencies, ensuring that everyone on your team (and your CI/CD pipeline) uses identical package versions.
uv: The Fast New Contender
Released by Astral (creators of Ruff), uv is a reimagining of Python packaging built in Rust. It’s 10-100x faster than pip and offers a modern, batteries-included approach.
Key Features:
- Blazing fast dependency resolution (written in Rust)
- Compatible with Poetry and standard Python packaging
- Built-in virtual environment management
- Cross-platform binary (no Python required for installation)
- Drop-in replacement for pip in many scenarios
Installation:
curl -LsSf https://astral.sh/uv/install.sh | sh
Basic Usage:
# Create a virtual environment and install dependencies
uv venv
source .venv/bin/activate # or `.venv\Scripts\activate` on Windows
# Install dependencies (much faster than pip)
uv pip install -r pyproject.toml
# Add a dependency
uv add requests
# Sync dependencies
uv sync
Performance Comparison (installing Django + common dependencies):
- pip: ~45 seconds
- Poetry: ~35 seconds
- uv: ~3 seconds
Which Should You Choose?
Choose Poetry if:
- You want maximum ecosystem compatibility and stability
- Your team is migrating from requirements.txt and needs a proven solution
- You prefer comprehensive documentation and community support
- You’re working on a library that will be published to PyPI
Choose uv if:
- Performance is critical (large dependency trees, frequent installs)
- You’re starting a new project and want cutting-edge tooling
- You appreciate Rust-based tooling (similar to Ruff)
- You want a single, fast tool for both dependency management and virtual environments
My Recommendation: Start with Poetry for production projects due to its maturity. Experiment with uv for new projects or personal work. Many teams are transitioning from Poetry to uv as it matures.
Migration Guide: From requirements.txt to Poetry
Let’s walk through a practical migration for a typical Flask application.
Step 1: Audit Your Current Setup
# Generate a complete list of installed packages
pip freeze > requirements_full.txt
# Review your requirements.txt
cat requirements.txt
Step 2: Install Poetry
curl -sSL https://install.python-poetry.org | python3 -
# Add Poetry to your PATH (follow installation instructions)
export PATH="$HOME/.local/bin:$PATH"
# Verify installation
poetry --version
Step 3: Initialize Poetry in Your Project
# Navigate to your project
cd your-project
# Initialize Poetry (interactive prompt)
poetry init
# Or create pyproject.toml manually
Step 4: Add Your Dependencies
You have two options:
Option A: Import from requirements.txt
poetry add $(cat requirements.txt | grep -v '#' | grep -v '^$' | tr '\n' ' ')
Option B: Add dependencies individually with proper versioning
poetry add flask==3.0.0
poetry add sqlalchemy^2.0.0 # Allow minor/patch updates
poetry add requests
For development dependencies:
poetry add --group dev pytest black mypy
Step 5: Generate Lock File
# Poetry automatically generates poetry.lock when adding dependencies
# Or explicitly:
poetry lock
Step 6: Update Your CI/CD Pipeline
Before (GitHub Actions):
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
After:
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Install dependencies
run: poetry install --with dev
Step 7: Update Your Dockerfile
Before:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
After:
FROM python:3.11-slim
WORKDIR /app
# Install Poetry
RUN pip install poetry==1.7.1
# Copy dependency files
COPY pyproject.toml poetry.lock ./
# Install dependencies (without dev dependencies)
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
COPY . .
CMD ["python", "app.py"]
Step 8: Update Documentation
Update your README.md:
## Development Setup
### Prerequisites
- Python 3.10+
- Poetry 1.7+
### Installation
\```bash
# Install dependencies
poetry install
# Activate virtual environment
poetry shell
# Run the application
poetry run python app.py
\```
### Adding Dependencies
\```bash
# Production dependency
poetry add package-name
# Development dependency
poetry add --group dev package-name
\```
Real-World Benefits: Case Study
Let me share a concrete example from a mid-sized SaaS company that migrated from requirements.txt to Poetry.
Project Context:
- FastAPI backend with ~40 direct dependencies
- Team of 8 developers
- Docker-based deployment
- Bi-weekly release cycle
Before Migration (requirements.txt):
- Average time to set up dev environment: 15-20 minutes
- Dependency-related incidents: 2-3 per month
- Production deployment failures due to dependencies: 1-2 per quarter
- Time spent debugging dependency issues: ~8 hours per month
After Migration (Poetry):
- Average time to set up dev environment: 3-5 minutes (thanks to
poetry.lock) - Dependency-related incidents: Less than 1 per quarter
- Production deployment failures: Zero in 6 months
- Time spent on dependencies: ~2 hours per month (mostly adding new packages)
Key Improvements:
- Onboarding: New developers could clone the repo and run
poetry installto get a working environment - CI/CD: Build times decreased by 40% due to better caching
- Confidence: The team could update dependencies confidently, knowing Poetry would catch conflicts
- Security: Integrated
poetry auditinto CI to catch known vulnerabilities
ROI Calculation:
- Developer time saved: ~48 hours/month across team
- At $75/hour average rate: $3,600/month savings
- One-time migration cost: ~16 hours of work ($1,200)
- Break-even: Within 2 weeks
Common Migration Challenges and Solutions
Challenge 1: Git Dependencies
Problem:
# requirements.txt
git+https://github.com/user/private-repo.git@main#egg=private-package
Solution (Poetry):
[tool.poetry.dependencies]
private-package = {git = "https://github.com/user/private-repo.git", rev = "main"}
Challenge 2: Editable Installs
Problem:
# requirements.txt
-e ./local-package
Solution (Poetry):
[tool.poetry.dependencies]
local-package = {path = "./local-package", develop = true}
Challenge 3: Platform-Specific Dependencies
Problem:
# requirements.txt
pywin32==305; sys_platform == 'win32'
Solution (Poetry):
[tool.poetry.dependencies]
pywin32 = {version = "^305", platform = "win32"}
Challenge 4: Optional Dependencies
Problem:
# requirements-optional.txt
pandas==2.1.4
matplotlib==3.8.2
Solution (Poetry):
[tool.poetry.dependencies]
pandas = {version = "^2.1.4", optional = true}
matplotlib = {version = "^3.8.2", optional = true}
[tool.poetry.extras]
data-science = ["pandas", "matplotlib"]
Install with: poetry install --extras data-science
Advanced Tips and Best Practices
1. Version Constraints Strategy
Be specific for application code:
[tool.poetry.dependencies]
flask = "3.0.0" # Exact version for stability
Be flexible for libraries:
[tool.poetry.dependencies]
requests = "^2.31.0" # Allow compatible updates
2. Dependency Groups
Organize dependencies logically:
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.0"
[tool.poetry.group.docs.dependencies]
sphinx = "^7.2.0"
sphinx-rtd-theme = "^2.0.0"
[tool.poetry.group.test.dependencies]
pytest-cov = "^4.1.0"
hypothesis = "^6.92.0"
Install selectively:
poetry install --with dev
poetry install --with dev,docs
poetry install --only test
3. Pre-commit Integration
Add to .pre-commit-config.yaml:
- repo: local
hooks:
- id: poetry-check
name: Poetry check
entry: poetry check
language: system
pass_filenames: false
- id: poetry-lock
name: Poetry lock check
entry: poetry lock --check
language: system
pass_filenames: false
4. Automated Dependency Updates
Use Dependabot or Renovate:
.github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
5. Poetry Plugins
Extend Poetry’s functionality:
# Export to requirements.txt format (useful during migration)
poetry self add poetry-plugin-export
poetry export -f requirements.txt --output requirements.txt
# Audit dependencies for security issues
poetry self add poetry-audit-plugin
poetry audit
The Bottom Line
The migration from requirements.txt to modern dependency management isn’t just about using a trendy new tool—it’s about fundamentally improving how you build and deploy Python applications.
Benefits Recap:
- Reliability: Lock files ensure identical environments across all deployments
- Security: Cryptographic verification and vulnerability scanning
- Performance: Dramatically faster installation and resolution times
- Developer Experience: Simplified workflows and better tooling integration
- Maintainability: Automated dependency management and conflict resolution
My Recommendations:
- For new projects: Start with uv. Its performance and modern architecture make it the best choice for greenfield development
- For existing projects: Migrate to Poetry first if you need maximum stability and ecosystem support, then consider uv once your team is comfortable with modern dependency management
- For teams: Begin with a pilot project and gradually roll out to build confidence and expertise
Learning Resources:
- Poetry Documentation
- uv Documentation
- PEP 621 - Storing project metadata in pyproject.toml
- Python Packaging User Guide
Take Action Today:
- Experiment: Create a small test project with Poetry or uv
- Measure: Benchmark installation times on your current projects
- Plan: Identify a pilot project for migration
- Share: Discuss these tools with your team and start building consensus
The transition away from requirements.txt isn’t just a technical upgrade—it’s an investment in your team’s productivity, your application’s reliability, and your project’s long-term maintainability. The tools exist, the ecosystem is ready, and the benefits are immediate.
Stop fighting with dependency conflicts at 3 AM. Your future self will thank you.