pip's vendored code carries the highest structural debt — 5 functions frozen for months

An analysis of pypa/pip finds its riskiest functions sitting untouched for over four months inside vendored rich and urllib3 code, plus one dormant-but-actively-owned hotspot in pip's own wheel installer.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk17.5Low
Hottest Functionrender

Antipatterns Detected

god_function7long_function6exit_heavy5complex_branching5deeply_nested4

Run this on your own codebase

See if your own repo has a render-style hotspot — run this in any local git repo:

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in pip?

A god function is one that has taken on too many responsibilities — high cyclomatic complexity, deep nesting, and broad fan-out combined in a single block of code. I found 7 functions carrying this tag in my top hotspots, including `_traverse` in rich's pretty-printer with 113 branching paths and `_install_wheel` in pip's own wheel installer with a fan-out of 84 distinct calls. The problem is concrete: a function this size can't be unit-tested in isolation without mocking most of its dependencies, and any change to it risks touching behavior the original author never intended to couple together.

How do I reduce cyclomatic complexity in python?

The standard technique is extract-method: pull cohesive chunks of branching logic out into named helper functions so each piece can be tested and reasoned about independently. As a rule of thumb, a cyclomatic complexity above 15 warrants splitting, and above 30 warrants immediate attention — `_traverse` in rich's pretty.py sits at 113, which is extreme by any threshold. A concrete first step: `_install_wheel` already defines several inline closures like `assert_no_path_traversal` and `root_scheme_file_maker`. Promoting those to module-level functions with explicit parameters, rather than closures capturing shared state, would cut the effective complexity of the parent function by a meaningful margin without changing behavior.

Is pip actively maintained?

Yes, but the top five structural hotspots I found are all in the 'debt' quadrant, not 'fire' — meaning they're complex but currently quiet, with the most dormant, `_traverse` and `extract`, sitting untouched for 136 days. That's not a sign of neglect: pip's own `_install_wheel` shows 3 distinct authors across 8 commits even though it's been quiet for 62 days, and functions outside my top five, like `get_scheme` in the sysconfig locations module, show active recent work. Active development and high structural debt coexist here — the debt is concentrated in vendored dependencies pip doesn't control, while pip's own code shows ongoing, distributed ownership.

How do I reproduce this analysis?

I ran the open-source hotspots CLI against pypa/pip at commit `2b28a81`. The exact steps: `git checkout 2b28a81` followed by `hotspots analyze . --mode snapshot --explain-patterns --force`. The same command runs against any local git repository with no project-specific configuration required.

What does activity-weighted risk mean?

Activity-weighted risk multiplies structural complexity — cyclomatic complexity, nesting depth, and fan-out combined — by how frequently a function has changed recently. A function with cyclomatic complexity of 80 that hasn't been touched in two years scores lower than one with cyclomatic complexity of 20 touched every week, because the dormant function carries lower near-term regression risk even though it's harder to read. In this pip analysis, that's exactly why my top five all land in the 'debt' quadrant rather than 'fire': the underlying complexity, like the 113 branching paths in `_traverse`, is real, but none of these five functions have been edited in the last 30 days, so the danger is what happens on the next touch, not what's happening today.

The top finding in pip isn’t a function under active churn — it’s structural debt that has sat untouched for months. The riskiest function I found, render in the vendored rich markup parser, carries an activity-weighted risk score of 17.5 and hasn’t been changed in 136 days, which means whoever touches it next inherits 45 branching paths and 6 levels of nesting with zero recent context to work from. Pip totals 4,536 functions, of which 449 land in the critical band, and four of my top five hotspots share this same profile: complex, dormant, and vendored. I’d start triage there, not because these functions are misbehaving, but because their next edit carries the highest blast radius in the repository.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
rendersrc/pip/_vendor/rich/markup.py17.545630
extractsrc/pip/_vendor/rich/traceback.py17.058529
_install_wheelsrc/pip/_internal/operations/install/wheel.py17.034584
_traversesrc/pip/_vendor/rich/pretty.py16.8113635
urlopensrc/pip/_vendor/urllib3/connectionpool.py16.763438

Codemod / Tooling Files in Results

Four of my five hotspots live in pip’s vendored dependencies — src/pip/_vendor/rich/ (markup.py, traceback.py, pretty.py) and src/pip/_vendor/urllib3/connectionpool.py. These are bundled copies of third-party libraries that pip ships to avoid runtime dependency conflicts, and they score high because pip’s own team doesn’t maintain their internals, so the code accumulates complexity without local review pressure. I’d exclude vendored paths from active review scope with { "exclude": ["src/pip/_vendor/"] } in .hotspotsrc.json, then re-run the analysis to see pip’s own code — like _install_wheel — ranked on its own terms.

Where the risk actually sits

Triage Band Distribution
Fire6Debt1445Watch20OK3065

4,536 functions analyzed

Out of 4,536 functions, only 6 fall into the ‘fire’ quadrant — complex and being actively changed right now. Compare that to 1,445 functions sitting in ‘debt’: complex, but quiet. That ratio is the real story here. Pip’s acute regression risk is small and contained; its structural debt is not.

Detected Antipatterns
God Function×7God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×6Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.
Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.

Seven of my flagged functions are tagged as god functions and six as long functions. That’s the dominant shape of risk in this codebase: not tangled logic so much as functions that took on too many responsibilities and grew past the point where a reviewer can hold the whole thing in their head.

render — src/pip/_vendor/rich/markup.py

render
src/pip/_vendor/rich/markup.py
17.5
critical
CC 45
ND 6
FO 30
touches/30d 0

This is the markup-to-Text renderer inside the vendored rich library. The excerpt shows a tag-parsing loop with a nested closing-tag branch (explicit close vs. implicit close, each with its own exception path) plus a separate pop_style helper that walks a style stack in reverse. That’s consistent with the 45 branching paths and 6 levels of nesting the metrics report, and with the ‘exit_heavy’ tag — I count at least three distinct raise points in the excerpt alone, each a required test case. Fan-out of 30 means this function reaches into 30 other functions or constructors, which in a duck-typed codebase means more coupling than the number implies, since none of those call sites are checked at compile time. It hasn’t been touched in 136 days, and the single historical commit shows no bug-linked activity, so this reads as inherited complexity from upstream rich, not something pip’s own maintainers are actively wrestling with. My recommendation: if pip doesn’t need to patch rich internals, this is a candidate for exclusion from active review scope entirely — see the vendor note below.

extract — src/pip/_vendor/rich/traceback.py

extract
src/pip/_vendor/rich/traceback.py
17.03
critical
CC 58
ND 5
FO 29
touches/30d 0

Same vendored library, same dormancy window: 136 days untouched, one historical commit, zero authors in the last 90 days. The excerpt shows a while True loop building a chain of Stack objects for exception groups and causes, with a nested safe_str guard to keep __str__ failures from propagating, plus a version-gated branch for BaseExceptionGroup handling on Python 3.11+. A cyclomatic complexity of 58 is high even by god-function standards — that’s 58 independent paths a test suite would need to cover to fully exercise this traceback extractor. Fan-out of 29 confirms broad coupling to the rest of the rich rendering machinery. This is textbook structural debt: nobody’s actively breaking it, but if a Python version bump or exception-handling change ever forces a touch, whoever does it is working against 58 paths with no recent commit history to lean on.

_install_wheel — src/pip/_internal/operations/install/wheel.py

_install_wheel
src/pip/_internal/operations/install/wheel.py
17.01
critical
CC 34
ND 5
FO 84
touches/30d 0

This is the one function in my top five that actually lives in pip’s own code, not vendored, and it’s the standout on fan-out: 84 distinct functions called, nearly triple the next-highest entry in this list. The source is annotated noqa: C901, PLR0915 function is too long — pip’s own maintainers have already flagged this with linter suppressions. The excerpt shows the function defining a cluster of nested closures (record_installed, is_dir_path, assert_no_path_traversal, root_scheme_file_maker, data_scheme_file_maker) inline, each capturing shared state like lib_dir and installed — a pattern that inflates both nesting depth and the effective surface area of the function even though each closure is individually simple. It hasn’t been touched in 62 days, but 3 distinct authors worked on it across 8 total commits, so this isn’t abandoned code — it’s actively-owned code that happens to be quiet right now. Given the path-traversal check and the wheel-file-writing logic, this is exactly the kind of function I’d want covered by characterization tests before extracting the closures into named module-level helpers.

_traverse — src/pip/_vendor/rich/pretty.py

_traverse
src/pip/_vendor/rich/pretty.py
16.78
critical
CC 113
ND 6
FO 35
touches/30d 0

This is the highest cyclomatic complexity in the entire dataset — 113 independent paths, more than double the next-closest function I found. It’s a recursive object-tree walker for rich’s pretty-printer, and the excerpt shows why the number is so extreme: a nested iter_rich_args generator with three arity-dependent branches, a try/except guard for attribute probing, conditional formatting for angular versus parenthesized reprs, and recursive calls back into _traverse itself for each child. Recursion plus generator-driven branching is exactly the kind of invisible control-flow layering that inflates path count without necessarily inflating visible line count. It’s been dormant for 136 days with a single historical commit — this is vendored debt pip inherited, not a function pip’s team is iterating on. If pip ever needs to debug pretty-printing behavior, this is the function that will eat the most review time relative to its apparent size.

urlopen — src/pip/_vendor/urllib3/connectionpool.py

urlopen
src/pip/_vendor/urllib3/connectionpool.py
16.65
critical
CC 63
ND 4
FO 38
touches/30d 0
Cyclomatic Complexity 63
threshold: 30

The core HTTP request path in vendored urllib3. Sixty-three branching paths against a nesting depth of only 4 tells me the complexity here comes from breadth, not depth — lots of sibling conditionals for retries, redirects, timeouts, and connection-release behavior rather than deeply staircased logic, which the docstring’s own list of parameters (retries, redirect, assert_same_host, timeout, pool_timeout, chunked, body_pos, preload_content, decode_content) partly explains. Two historical commits, no authors active in the last 90 days, 92 days since the last change. This is the connection-pooling engine underneath every pip network call, so its fan-out of 38 matters even in dormancy — if pip ever needs to patch retry or redirect behavior for a networking edge case, this function is where that patch lands, and it will land in unfamiliar territory.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
god_function7
long_function6
exit_heavy5
complex_branching5
deeply_nested4

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function.

Reproduce This Analysis

git clone https://github.com/pypa/pip
cd pip
git checkout 2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5
hotspots analyze . --mode snapshot --explain-patterns --force

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Was this useful? Let me know →

Related Analyses