Python Supply Chain Risk: What to Do When Critical Dependencies Get Abandoned
I discovered the HTTPX issue tracker was locked. No new issues. No discussions. A library powering 562,000+ projects on PyPI, and I couldn’t report a bug or ask a question.
That’s when a Reddit comment hit home: “HTTPX has become a dependency in so many libraries, with the inability to submit issues and have discussions, it has legitimately become a supply chain risk.”
I looked at my dependency tree. HTTPX wasn’t even a direct dependency. It was buried three levels deep, pulled in by FastAPI, which my async HTTP client library used. I had no idea it was there until I needed to understand why my API calls were timing out intermittently.
The Hidden Dependency Problem
Modern Python applications don’t just depend on what you put in requirements.txt. They depend on everything those packages depend on, and everything those depend on, recursively.
When I ran pipdeptree on a recent project, I found 147 packages. I directly installed 23. The other 124 were transitive dependencies I’d never heard of.
$ pipdeptree | wc -l147
$ pipdeptree --warn-silence | grep -v "^[a-zA-Z]" | head -20 - certifi==2024.2.2 - charset-normalizer==3.3.2 - h2==4.1.0 - hpack==4.0.0 - hyperframe==6.0.1 - httpcore==1.0.4 - certifi [required: Any, installed: 2024.2.2] - h11==0.14.0 - httpx==0.27.0 - anyio==4.3.0 - certifi [required: Any, installed: 2024.2.2] - httpcore [required: ==1.0.4, installed: 1.0.4]HTTPX was in there. And it was critical because FastAPI’s TestClient uses it. If HTTPX becomes unmaintained, every FastAPI application inherits that risk.
What Makes a Dependency “Abandoned”?
I needed a systematic way to evaluate dependency health. Through research and painful experience, I learned to watch for these signals:
Red flags (abandoned):
- No releases in 12+ months
- Issue tracker locked or ignored
- Pull requests sit unreviewed for months
- Security advisories unaddressed
- Single maintainer with no succession plan
Amber flags (at risk):
- Declining commit frequency
- Maintainer mentions burnout
- Major version stagnation (stuck on 0.x for years)
- Growing backlog of issues and PRs
The HTTPX situation showed multiple red flags: locked discussions, limited maintainer responsiveness, and a massive downstream impact if problems arise.
Step 1: Map Your Dependency Tree
I started by getting a complete picture of what my application actually depends on.
# Install pipdeptreepip install pipdeptree
# Visualize the full treepipdeptree
# Find what depends on a specific packagepipdeptree -r -p httpx
# Output:httpx==0.27.0 - httpx==0.27.0 - fastapi==0.110.0 - myproject==1.0.0This reverse dependency view is crucial. It shows the blast radius if a package fails.
I created a script to identify critical path dependencies:
import subprocessimport jsonfrom collections import defaultdict
def get_reverse_deps(): """Find packages depended on by multiple libraries.""" result = subprocess.run( ['pipdeptree', '--json'], capture_output=True, text=True ) deps = json.loads(result.stdout)
# Count how many packages depend on each dep dep_count = defaultdict(int) for pkg in deps: for dep in pkg.get('dependencies', []): dep_count[dep['package_name']] += 1
# Return packages with 3+ dependents critical = {k: v for k, v in dep_count.items() if v >= 3} return sorted(critical.items(), key=lambda x: -x[1])
for pkg, count in get_reverse_deps(): print(f"{pkg}: {count} dependents")Running this revealed my single points of failure:
httpx: 4 dependentspydantic: 6 dependentscertifi: 8 dependentsurllib3: 5 dependentsStep 2: Evaluate Dependency Health Metrics
I needed a scoring system. Here’s the rubric I developed:
| Factor | Healthy | Warning | Critical |
|---|---|---|---|
| Last release | < 3 months | 3-12 months | > 12 months |
| Open issues | < 50 | 50-200 | > 200 or locked |
| PR response time | < 1 week | 1-4 weeks | > 4 weeks |
| Active maintainers | 3+ | 1-2 | 0-1 |
| Security response | < 7 days | 7-30 days | > 30 days |
I automated this checking with a health assessment script:
import requestsfrom datetime import datetime, timedelta
def check_pypi_health(package_name): """Check PyPI for package health indicators.""" url = f"https://pypi.org/pypi/{package_name}/json" response = requests.get(url) data = response.json()
# Last release date releases = data['releases'] all_versions = [] for version, files in releases.items(): for f in files: all_versions.append(datetime.fromisoformat(f['upload_time'].replace('Z', '+00:00')))
last_release = max(all_versions) if all_versions else None days_since_release = (datetime.now(last_release.tzinfo) - last_release).days if last_release else 999
return { 'package': package_name, 'last_release_days': days_since_release, 'version_count': len(releases), 'health': 'healthy' if days_since_release < 90 else 'warning' if days_since_release < 365 else 'critical' }
# Check critical dependenciesfor pkg in ['httpx', 'pydantic', 'certifi', 'urllib3']: health = check_pypi_health(pkg) print(f"{health['package']}: {health['health']} ({health['last_release_days']} days since release)")For deeper analysis, I used libraries.io API which tracks maintainer activity:
# Check via libraries.io APIcurl "https://libraries.io/api/PyPI/httpx?api_key=YOUR_KEY" | jq '.status, .latest_release_published_at, .dependent_repos_count'Step 3: Vulnerability Scanning
Before worrying about abandonment, I needed to check for existing vulnerabilities:
# Install pip-auditpip install pip-audit
# Scan current environmentpip-audit
# Scan requirements filepip-audit -r requirements.txt
# Output example:Name Version Fix Versions Vulnerability Descriptionhttpx 0.26.0 0.27.0 PYSEC-2024-XX HTTP redirect to unintended hostpip-audit checks against the Python Packaging Advisory Database and OSV. It’s the fastest way to find known vulnerabilities.
For continuous monitoring, I added this to CI:
name: Dependency Auditon: [push, pull_request]
jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - run: pip install pip-audit - run: pip-audit -r requirements.txtStep 4: Risk Scoring Matrix
Not all abandoned packages are equal. I needed to prioritize by risk:
Risk Score = Likelihood x Impact
Likelihood (1-5):- 5: Locked issues, no commits in 12+ months, single maintainer- 4: No commits in 6-12 months, declining activity- 3: Sporadic activity, maintainer mentions burnout- 2: Active but single maintainer, no bus factor- 1: Active with multiple maintainers
Impact (1-5):- 5: Critical functionality, no alternatives, 10+ transitive dependents- 4: Core functionality, limited alternatives- 3: Important but alternatives exist- 2: Nice to have, easy to replace- 1: Optional, minimal usageI built a quick assessment:
def assess_risk(package, likelihood, impact): """Calculate risk score and recommend action.""" score = likelihood * impact
if score >= 20: action = "IMMEDIATE: Evaluate alternatives, plan migration" elif score >= 12: action = "HIGH: Monitor closely, document workaround" elif score >= 6: action = "MEDIUM: Add to watch list, quarterly review" else: action = "LOW: Annual review sufficient"
return {'package': package, 'score': score, 'action': action}
# Example assessmentspackages = [ ('httpx', 4, 5), # Locked issues, critical path ('certifi', 2, 5), # Active, but critical ('h2', 3, 3), # Sporadic, moderate impact]
for pkg, likelihood, impact in packages: result = assess_risk(pkg, likelihood, impact) print(f"{result['package']}: Score {result['score']} - {result['action']}")Output:
httpx: Score 20 - IMMEDIATE: Evaluate alternatives, plan migrationcertifi: Score 10 - MEDIUM: Add to watch list, quarterly reviewh2: Score 9 - MEDIUM: Add to watch list, quarterly reviewMitigation Strategy 1: Proactive Monitoring
I set up automated health checks that run weekly:
name: Dependency Health Checkon: schedule: - cron: '0 6 * * 1' # Monday 6 AM UTC workflow_dispatch:
jobs: health-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - run: pip install pipdeptree pip-audit requests
- name: Check dependency tree run: pipdeptree > dependency-tree.txt
- name: Audit for vulnerabilities run: pip-audit -r requirements.txt --format json > audit-report.json
- name: Check maintainer activity run: python scripts/check_dep_health.py > health-report.txt
- name: Create issue if critical if: contains(steps.audit.outcome, 'failure') uses: actions/github-script@v7 with: script: | github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title: 'Critical dependency vulnerability detected', body: 'Run `pip-audit` for details', labels: ['security', 'dependencies'] })Mitigation Strategy 2: Dependency Diversification
For critical packages, I created abstraction layers:
# Before: Direct dependency on httpximport httpx
async def fetch_data(url): async with httpx.AsyncClient() as client: response = await client.get(url) return response.json()
# After: Abstraction layerfrom abc import ABC, abstractmethod
class HTTPClient(ABC): @abstractmethod async def get(self, url: str) -> dict: pass
class HTTPXClient(HTTPClient): def __init__(self): import httpx self._client = httpx.AsyncClient()
async def get(self, url: str) -> dict: response = await self._client.get(url) return response.json()
class AiohttpClient(HTTPClient): """Fallback implementation.""" def __init__(self): import aiohttp self._session = None
async def get(self, url: str) -> dict: if not self._session: import aiohttp self._session = aiohttp.ClientSession() async with self._session.get(url) as response: return await response.json()
# Factory with fallbackdef get_http_client() -> HTTPClient: try: return HTTPXClient() except ImportError: return AiohttpClient()This abstraction means I can switch implementations without changing business logic.
Mitigation Strategy 3: The Migration Playbook
When I identified HTTPX as high-risk, I created a migration plan:
Phase 1: Assessment (Week 1)
- Document all HTTPX usage in codebase
- Identify HTTPX-specific features being used
- Evaluate alternatives (aiohttp, requests, httpcore)
- Create feature compatibility matrix
Phase 2: Proof of Concept (Week 2)
- Create abstraction layer for HTTP client
- Implement alternative client alongside HTTPX
- Write comparison tests
- Benchmark performance differences
Phase 3: Gradual Migration (Weeks 3-4)
- Switch new code to abstraction layer
- Migrate existing code incrementally
- Run parallel testing
- Monitor for behavioral differences
Phase 4: Validation (Week 5)
- Full integration tests
- Load testing with new client
- Remove HTTPX dependency
- Update documentation
I documented this in a MIGRATION.md file so the team has a playbook ready:
# HTTPX Migration Playbook
## Trigger Conditions- No release in 12 months- Unpatched security vulnerability- Breaking incompatibility with Python 3.13+
## Alternative: aiohttp- Pros: Mature, well-maintained, similar async patterns- Cons: Different API, no sync client
## Code Changes Required- Replace `httpx.AsyncClient()` with `aiohttp.ClientSession()`- Update timeout configuration format- Rewrite response handling (`.json()` is async in aiohttp)Quick Wins You Can Implement Today
1. Run a dependency audit right now:
pip install pip-audit pipdeptreepip-auditpipdeptree > deps.txt2. Identify your top 5 riskiest dependencies:
# Find packages with most dependentspipdeptree --json | python -c "import json, sysfrom collections import Counterdata = json.load(sys.stdin)counts = Counter()for pkg in data: for dep in pkg.get('dependencies', []): counts[dep['package_name']] += 1for name, count in counts.most_common(5): print(f'{name}: {count} dependents')"3. Set up a weekly health check script:
# Add to crontab or scheduled task0 9 * * 1 cd /path/to/project && pip-audit -r requirements.txtLong-Term Strategy
After implementing these practices, I established a quarterly dependency review process:
- Dependency inventory: Update full dependency tree
- Health assessment: Check maintainer activity, release frequency
- Vulnerability scan: Run pip-audit and Snyk
- Risk prioritization: Update risk scores
- Action items: Create tickets for high-risk packages
I also allocated budget for supporting critical maintainers through GitHub Sponsors and Tidelift. It’s cheaper than dealing with an emergency migration.
The Hard Truth About HTTPX
The HTTPX situation taught me that even “safe” libraries can become liabilities. 562,000 dependents means 562,000 potential victims of supply chain issues.
When I encounter locked issue trackers and unresponsive maintainers on critical packages, I don’t wait. I start planning alternatives. The cost of preparation is far lower than the cost of emergency migration.
My rule now: If a critical dependency shows two or more red flags, I spend a sprint building the abstraction layer and evaluating alternatives. It’s technical debt insurance.
Key Takeaways
- Map your dependency tree: You can’t manage what you don’t know. Use
pipdeptreeto discover transitive dependencies. - Score by risk: Not all dependencies need the same attention. Focus on high-impact, high-likelihood risks.
- Automate monitoring: Weekly health checks catch issues before they become crises.
- Build abstraction layers: For critical dependencies, design your code to allow swapping implementations.
- Have a migration playbook: When abandonment happens, you need a documented process, not panic.
The Python ecosystem’s strength—its vast package library—is also its supply chain vulnerability. Every dependency you add is a trust decision. Make those decisions consciously, monitor them continuously, and plan for when that trust is broken.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 HTTPX on PyPI
- 👨💻 pip-audit Documentation
- 👨💻 pipdeptree on GitHub
- 👨💻 libraries.io API
- 👨💻 Snyk Open Source
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments