The most-downloaded Python package has a 0% share — in the agent most people use
requests is the most-downloaded package in Python history. Ask claude-opus-5 — inside Claude Code, the way most people actually use it — to build you an HTTP client or a web scraper, and it will reach for requests approximately never: 0 out of 240 samples on our scraping task, 0 out of 100 on our HTTP-client task, across every neutral phrasing we tried.
Ask any of four other Claude models the same questions and requests comes back in essentially 100% of answers.
That 0% is real, and it is also conditional, in a way we only understood after trying to break it. Ask the same opus-5 through the raw API with reasoning effort turned down, and requests comes back 58% of the time. The number is a property of a model and the configuration you meet it in — and Claude Code's default is a high-thinking configuration. We report every share with the condition that produced it: model × harness × effort × phrasing. That turned out to be the finding, not the fine print.
Nobody decided this in public. No changelog announced it. Somewhere between two model releases, the default HTTP stack that an AI coding agent hands to thousands of developers flipped from requests+beautifulsoup4 to httpx+selectolax+tenacity+pydantic — from one release to the next, from 100 to 0.
We built a harness to measure this systematically. This post is about what we found: which packages coding agents actually select, how violently that depends on which model you're on, why web search doesn't change it, and the most practically surprising result — that how you phrase your request partly decides your dependency tree.
Everything below is measured, not vibes: ~3,600 completed fresh Claude Code sessions at the time of writing (a further 12,500-session five-model run was still streaming in), every raw prompt and response logged and published.
What we measure and how
Selection share: given a realistic build request in a cold-start session, how often does the agent's proposed dependency set include package X?
The setup, in full:
- Each sample is one fresh
claude -p(headless Claude Code) session: empty working directory, no memory, no tools, no conversation history, session discarded afterwards. This is deliberately the agent environment — Claude Code's own system prompt included — because that's how most developers meet the model, not through the raw API. - The prompt is a realistic build request ("Build me a scraper that pulls every product listing from an e-commerce site and handles pagination…"), never a question ("what's the best scraping library?"). The system prompt asks for exactly two fenced blocks: the dependency manifest and the import block — no full implementation. Nothing in any prompt names, hints at, or excludes any library.
- We parse the manifest and imports, count packages, and compute shares with Wilson 95% intervals. n=50–240 per condition depending on the run.
- One methodological note up front: current Claude models expose no temperature control, so run-to-run variance comes from the model's default sampling. Three independent n=100 runs of the same condition agreed exactly on every headline package (0% and 100%); mid-band accessories drifted between runs by about 1.9× what binomial sampling predicts. So head picks are stable to the point; a single-run mid-band share should be read as roughly ±15–18 points, and the published category numbers average five phrasings for that reason.
The harness, task catalogue (128 tasks across 5 languages, with taxonomy), methodology, and raw per-call logs are public — links at the end.
Finding 1: Selection is model-specific, and it flips whole stacks at once
Same task, same prompt, same day, same Claude Code binary, n=100 per model:
"Build me a scraper… handles pagination until there are no more pages." (Python)
| package | opus-5 | fable-5 | sonnet-5 | haiku-4.5 | opus-4.5 |
|---|---|---|---|---|---|
httpx |
100% | 0% | 0% | 0% | 0% |
pydantic |
100% | 0% | 0% | 0% | 0% |
tenacity |
100% | 0% | 0% | 0% | 0% |
selectolax |
77% | 0% | 0% | 0% | 0% |
requests |
0% | 100% | 100% | 100% | 100% |
beautifulsoup4 |
29% | 100% | 100% | 100% | 100% |
lxml |
29% | 100% | 100% | 79% | 100% |
One model against four. Not a drift — a total, deterministic flip of the entire stack. And before you construct a tidy narrative ("newer models prefer modern libraries"): fable-5 is the newest and most capable model in the lineup, and it sides with the old guard here.
It's not a fixed personality trait either. On the HTTP-client task, fable-5 crosses the aisle:
"Write a small client for a REST API… bearer token, retries on 5xx with backoff, get/post helpers returning JSON." (Python)
| package | opus-5 | fable-5 | sonnet-5 | haiku-4.5 | opus-4.5 |
|---|---|---|---|---|---|
httpx |
100% | 94% | 0% | 0% | 0% |
tenacity |
100% | 94% | 96% | 69% | 30% |
requests |
0% | 6% | 100% | 100% | 100% |
urllib3 |
0% | 3% | 3% | 32% | 75% |
So the preference is a property of the (model × task-category) cell, not of the model. Each cell is its own fact. You cannot predict it from download counts, from model recency, or — we tried — by asking the model itself to predict its own choices (an early simulated baseline guessed requests would win on opus-5; it was wrong by 65 points).
Some categories, meanwhile, have no contest at all. Data validation:
| package | opus-5 | fable-5 | opus-4.5 |
|---|---|---|---|
pydantic |
100% | 100% | 100% |
email-validator |
83% | 95% | 97% |
phonenumbers |
100% | 100% | 1% |
zxcvbn |
61% | 0% | 0% |
pydantic has won validation so completely that marshmallow, cerberus, and friends simply do not appear — in 300 samples across three models, not once.
Finding 2: Reasoning effort decides which stack you get
We assumed the 0% was baked into the weights. Testing that assumption is what produced the most interesting result in this project.
Run the same scraping prompt at claude-opus-5 through the raw API, minimal system prompt, reasoning effort low (n=50):
| package | Claude Code (default, n=240) | raw API, effort low (n=50) | raw API, effort high (n=50) |
|---|---|---|---|
requests |
0% | 58% | 0% |
httpx |
100% | 44% | 100% |
beautifulsoup4 |
35% | 58% | — |
tenacity |
100% | 100% | — |
Two things to notice. First, the low-effort column is not a blend: of 50 answers, exactly one contained both httpx and requests. The model is not 58% confident in requests — it is running one of two discrete stacks, and effort decides which. Second, turning effort back up restores the Claude Code result exactly.
So the mechanism is thinking depth collapsing a bimodal prior: a fast answer reaches for the conventional stack, a considered answer for the modern one. That also explains Finding 3 below — casual phrasing produces a fast, conventional answer; senior/production phrasing produces a deliberate one. Register and effort are the same lever pulled from two ends.
The practical consequence for anyone reading a benchmark like ours: a selection share without a stated effort setting is not a measurement. Ours are now all pinned and reported. A sonnet-5 control ran requests 100% at every effort and in every harness we tried, so this is not a universal escape hatch either — some defaults really are locked.
Finding 3: Web search doesn't move the needle
Maybe the preference is just stale training data, and an agent that could check the web would answer differently? We tested four conditions on opus-5, scraping task, n=100 each:
| condition | actually searched | httpx |
requests |
|---|---|---|---|
| Claude Code system prompt (default) | 0/100 | 100% | 0% |
| bare minimal system prompt | 0/100 | 100% | 0% |
| web search available | 0/100 | 100% | 0% |
| web search required by the prompt | 100/100 | 100% | 0% |
Three things worth staring at. Given the tools, the model never bothered to search on its own. Told explicitly to search first, it searched every single time, read the 2026 "best Python scraping libraries" listicles — which overwhelmingly recommend requests, beautifulsoup4, and scrapy — and then chose httpx anyway, 100 times out of 100. And the system prompt (Claude Code's vs a bare one) contributed nothing.
The preference is embedded at post-training time. Retrieval decorates the edges (search nudged some secondary picks a few points); it does not touch the primary choice. If you were planning "SEO for AI agents" as a strategy — publish content, get retrieved, get recommended — this result is bad news delivered cheaply.
Finding 4: Your phrasing picks your dependencies
This is the one that changed how we run the whole benchmark.
We took the same scraping task and rephrased it five ways — same requirements, different voice. n=100 per phrasing, opus-5:
| package | canonical | terse | spec-style | casual | "senior engineer" |
|---|---|---|---|---|---|
httpx |
100% | 100% | 90% | 37% | 100% |
pydantic |
100% | 98% | 0% | 1% | 100% |
tenacity |
100% | 100% | 100% | 100% | 100% |
playwright |
0% | 0% | 0% | 100% | 0% |
pandas |
0% | 0% | 0% | 96% | 0% |
beautifulsoup4 |
29% | 15% | 24% | 82% | 0% |
structlog |
0% | 0% | 0% | 0% | 100% |
typer |
3% | 3% | 0% | 0% | 78% |
pydantic-settings |
0% | 0% | 0% | 0% | 84% |
aiolimiter |
0% | 0% | 0% | 0% | 49% |
Three distinct mechanisms are visible:
Implied requirements. The casual phrasing said the site "has like 40 pages so it needs to click through all of them." Click through reads as browser interaction — so playwright goes 0→100%. That's not noise; it's the model taking wording seriously. (It's also a curation lesson: that phrasing changed the task, so it gets quarantined from the average — more below.)
Register as audience inference. Phrase the request like a senior engineer ("production-quality, as a senior engineer…") and the model staffs the project accordingly: structured logging, a CLI framework, settings management, a rate limiter — an enterprise stack that plain phrasing never produces, at up to 100%. Phrase an HTTP-client request in lowercase-casual ("make me a python module for talking to a rest api… give back dicts from the json") and httpx 100% flips to requests 90%. The model appears to estimate who is asking and match the stack to the persona: beginners get the friendly library, professionals get the modern one.
Architecture words. Single words redirect stacks. Asking for a validation "function" (rather than a validation layer) dropped pydantic from 100% to 16% — a function implies no model class. Saying "registration endpoint" summoned fastapi at 100% into what was supposed to be a pure-validation task. A spec that mentioned CSV output killed pydantic entirely (0%).
To find out whether you can average across phrasings honestly, we ran 8 intent-preserving rephrasings each of three tasks (n=50–200 per phrasing) and decomposed the variance. The result: between-phrasing variance dominates sampling variance — the between-phrasing SD (τ) is 22–34 points for the decisive packages against a sampling SD of 2–5, and it stays dominant after inflating the sampling term for run-to-run drift. (For low-share accessories the two terms are of the same order and we do not claim a phrasing effect.) A single-prompt benchmark is measuring its own prompt as much as the model.
But averaging does work if you do it eyes-open. The phrasing-averaged share converges like τ/√k (τ = between-phrasing SD, ~26 points for mid-table packages): ±27 points with one phrasing, ±12 with five, ±9 with eight. Two caveats made it into our methodology as hard rules. First, means hide bimodality — httpx on the http-client task is "100% or 10%," never actually 60% — so we report the range and top-choice rate alongside the mean. Second, since sampling noise is the minor term, the budget goes to more phrasings, not more samples: 5 phrasings × n=50 beats 1 phrasing × n=250 at identical cost.
Finding 5: Some packages are phrasing-proof — most are not
Across all 8 intent-preserving phrasings of the scraping task, tenacity scored 100% in every single one (between-phrasing SD: 0.0). Across all 8 phrasings of the validation task, phonenumbers: 100%, every one. Meanwhile python-dotenv ranges 2–72% depending on wording; tqdm 0–70%; python-slugify 1–92%.
We call this fragility (τ, the between-phrasing SD), and we report it as a first-class metric next to the share. A package with a high average share and low fragility has genuinely won its category in the agent's head. A package whose share exists only in one register has a very different — and much more precarious — position. If you maintain a library, this distinction should matter more to you than the point estimate.
Honest failures: even curated phrasings drift
Two of our own 18 "intent-preserving" rephrasings turned out to shift the task's scope — the "function" one and the "endpoint" one above. We wrote them, we believed they were neutral, and the data said otherwise. The fix is now codified in the methodology: phrasing panels are linted against architecture-implying words (function, endpoint, module, class, CLI, "production", output formats), and any phrasing whose modal answer solves a different requirement is quarantined — kept in the published data, excluded from the category average, displayed as "what a slightly different ask gets you."
A second, larger one: we measured the same model in a third-party harness, saw the stack shift, and wrote it up as a harness effect. A reader pushed back; the raw-API test above showed the real cause was reasoning effort, and that the third-party product was simply running an intermediate thinking configuration. We had a mechanism story built on nine data points and no test of the obvious alternative. The rule we adopted from it: model claims come only from first-party harnesses, and the strongest alternative explanation gets tested before a finding gets narrated. Both the wrong version and the corrected one are in the published logs.
We flag this because it generalizes: if a team actively trying to hold requirements constant drifts 2 times out of 18, every informal "I asked the model and it said X" observation floating around is carrying enormous unmeasured phrasing variance.
What this means
For developers. Your phrasing partly decides your dependency tree, and nobody tells you. Write like a beginner, get a beginner's stack; mention production, inherit an ops stack you may not want; say "click through," get a full browser automation dependency. If you care about your dependencies, review the manifest the agent proposes — it was chosen for the person the model thinks you are.
For library maintainers. Your users increasingly don't choose you — their agent does, and the decision was frozen at some lab's post-training run months ago. Download statistics are now a lagging indicator: opus-5's requests share going to 0% in Claude Code's default configuration won't show up in PyPI numbers for a long time, because installed bases and humans still exist, but the cold-start flow of new projects has already flipped. Watching your selection share across models and releases is watching your future market share.
For the ecosystem. These preferences flip 100→0 between model releases, silently, with no changelog. A category can consolidate to a single winner (pydantic) so hard that alternatives stop existing in the agent's output space entirely. And because the preference is post-training-embedded and immune to retrieval, there is no fast content-marketing route to change it — the levers are the quality and presence of your library in the corpora and preference data that labs curate, which moves on release timescales.
Limitations
Single vendor in the tables above — five Claude models, because that's what we could sample at scale on day one. The same harness runs on any agent CLI; GPT-5.6 Sol via Codex CLI and six more model families via Cursor are measuring as this is published, and will appear in the live tables first. Cold-start only — a real repo with an existing requirements.txt will pull the agent toward incumbents; measuring that (how much an existing codebase moves the choice — "movability") is our next experiment. Selection ≠ endorsement — we measure what the agent reaches for, not whether it's right. Every number is conditional on model × harness × effort × phrasing, and we now consider a share quoted without all four to be unreportable — including several of our own earlier ones. The five-model × five-phrasing expansion (12,500 sessions) has since completed; its category-level panel estimates supersede the single-phrasing numbers in this post and live in the rankings.
What's next
The live rankings are public — every cell links to the raw session logs that produced it (full prompt, full response, request IDs), because a benchmark you can't audit is an opinion. You can run your own phrasing against any model and see whether your wording changes your stack, and publish your prompt as a task variant for others.
- Live rankings: fairlabs.ai/devrank
- Methodology, task taxonomy, and phrasing-panel rules: fairlabs.ai/devrank/methodology
- Raw logs: linked from every cell in the rankings — full prompt, full response, one JSON record per session
FAIR Labs is a nonprofit AI research lab. DevRank measurements were produced with the devrank open harness; all data in this post is reproducible from the published logs.