Skip to content

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.

Terminal window
$ pipdeptree | wc -l
147
$ 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.

Terminal window
# Install pipdeptree
pip install pipdeptree
# Visualize the full tree
pipdeptree
# Find what depends on a specific package
pipdeptree -r -p httpx
# Output:
httpx==0.27.0
- httpx==0.27.0
- fastapi==0.110.0
- myproject==1.0.0

This reverse dependency view is crucial. It shows the blast radius if a package fails.

I created a script to identify critical path dependencies:

find_critical_deps.py
import subprocess
import json
from 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 dependents
pydantic: 6 dependents
certifi: 8 dependents
urllib3: 5 dependents

Step 2: Evaluate Dependency Health Metrics

I needed a scoring system. Here’s the rubric I developed:

FactorHealthyWarningCritical
Last release< 3 months3-12 months> 12 months
Open issues< 5050-200> 200 or locked
PR response time< 1 week1-4 weeks> 4 weeks
Active maintainers3+1-20-1
Security response< 7 days7-30 days> 30 days

I automated this checking with a health assessment script:

check_dep_health.py
import requests
from 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 dependencies
for 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:

Terminal window
# Check via libraries.io API
curl "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:

Terminal window
# Install pip-audit
pip install pip-audit
# Scan current environment
pip-audit
# Scan requirements file
pip-audit -r requirements.txt
# Output example:
Name Version Fix Versions Vulnerability Description
httpx 0.26.0 0.27.0 PYSEC-2024-XX HTTP redirect to unintended host

pip-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:

.github/workflows/dependency-audit.yml
name: Dependency Audit
on: [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.txt

Step 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 usage

I built a quick assessment:

risk_assessment.py
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 assessments
packages = [
('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 migration
certifi: Score 10 - MEDIUM: Add to watch list, quarterly review
h2: Score 9 - MEDIUM: Add to watch list, quarterly review

Mitigation Strategy 1: Proactive Monitoring

I set up automated health checks that run weekly:

.github/workflows/dep-health.yml
name: Dependency Health Check
on:
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 httpx
import httpx
async def fetch_data(url):
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# After: Abstraction layer
from 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 fallback
def 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)

  1. Document all HTTPX usage in codebase
  2. Identify HTTPX-specific features being used
  3. Evaluate alternatives (aiohttp, requests, httpcore)
  4. Create feature compatibility matrix

Phase 2: Proof of Concept (Week 2)

  1. Create abstraction layer for HTTP client
  2. Implement alternative client alongside HTTPX
  3. Write comparison tests
  4. Benchmark performance differences

Phase 3: Gradual Migration (Weeks 3-4)

  1. Switch new code to abstraction layer
  2. Migrate existing code incrementally
  3. Run parallel testing
  4. Monitor for behavioral differences

Phase 4: Validation (Week 5)

  1. Full integration tests
  2. Load testing with new client
  3. Remove HTTPX dependency
  4. 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:

Terminal window
pip install pip-audit pipdeptree
pip-audit
pipdeptree > deps.txt

2. Identify your top 5 riskiest dependencies:

Terminal window
# Find packages with most dependents
pipdeptree --json | python -c "
import json, sys
from collections import Counter
data = json.load(sys.stdin)
counts = Counter()
for pkg in data:
for dep in pkg.get('dependencies', []):
counts[dep['package_name']] += 1
for name, count in counts.most_common(5):
print(f'{name}: {count} dependents')
"

3. Set up a weekly health check script:

Terminal window
# Add to crontab or scheduled task
0 9 * * 1 cd /path/to/project && pip-audit -r requirements.txt

Long-Term Strategy

After implementing these practices, I established a quarterly dependency review process:

  1. Dependency inventory: Update full dependency tree
  2. Health assessment: Check maintainer activity, release frequency
  3. Vulnerability scan: Run pip-audit and Snyk
  4. Risk prioritization: Update risk scores
  5. 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 pipdeptree to 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments