AI Package Hallucination Rate: 96 Names Checked On PyPI
BrassCoders ran a first-party probe: 96 package names a frontier AI model suggested for Python tasks, checked live against PyPI. 95 existed; 1 didn't.
Ninety-six Python coding tasks. Ninety-six package names a frontier AI model handed back, each with the confidence it uses for import os. Ninety-five of those names exist on PyPI. One doesn’t, and never has. BrassCoders, the scanner that catches what AI assistants structurally miss, ran that check against the live registry, and the single miss is the point of this post.
The probe was small and first-party: 96 package names one commercial frontier model produced across diverse Python tasks, each checked with a single request to the PyPI JSON API. The result was a roughly 1% hallucination rate, one invented name in 96. Set that against the number everyone cites: USENIX Security 2025 measured 19.7% across 16 models. Both numbers are real. This post is about why they differ, and why the low one still matters.
What The Probe Measured
BrassCoders generated 96 package names by prompting one frontier commercial model with 96 well-specified Python tasks, then checked each name against the live PyPI registry. Ninety-five existed. One, python-feature-flags, returned a 404.
The method is a shell loop, not a study. Each task named a concrete job — an async HTTP client, a Postgres driver, a PDF reader, a geocoder, a circuit breaker — and the model returned the package it would pip install. Every name went to https://pypi.org/pypi/<name>/json. A 200 means the package is registered and real; a 404 means it isn’t. The corpus spanned mainstream libraries and a long tail of niche jobs, because the published research finds hallucination concentrates in the obscure corners.
Call this what it is: a replication in miniature, not a substitute for the large-scale work. It’s one model, one pass, 96 names I generated deliberately rather than sampling hundreds of thousands of completions at temperature. The N is tiny next to USENIX’s 16-model corpus. What it buys you is a number you can re-derive in about thirty seconds, and a single concrete failure to look at.
The One Package That Didn’t Exist
BrassCoders found exactly one invented name, python-feature-flags, and nothing about it looks wrong: lowercase, hyphenated, semantically obvious. It’s the kind of dependency you’d approve in a diff without a second look.
Here’s the detail that makes it worse. For that same feature-flag task, the model also produced flagsmith, which is a real, published client. So within one job it emitted a real name and an invented one, side by side, with identical confidence. The invented name follows a pattern the registry has trained everyone to trust: python-dotenv, python-dateutil, python-slugify, python-docx are all real packages, so python-feature-flags reads as one more of the family.
That’s the mechanic in one example. A language model completes patterns; it doesn’t look anything up. A name that fits the shape of real package names gets generated whether or not the registry has ever seen it. The model has no channel to check and no signal that it’s guessing. The failure is invisible precisely because the output is well-formed.
Where 1% Sits Against The Published Rates
BrassCoders measured about 1% because the probe used one commercial frontier model on well-specified, mostly-mainstream tasks — the lowest-hallucination corner of the whole problem. The published rates climb from there: 5.2% for commercial models and 21.7% for open-source models in the USENIX Security 2025 study, blending to a 19.7% headline.
The USENIX paper, by Spracklen and colleagues at UT San Antonio and collaborators, ran 16 code-generating models and catalogued 205,474 unique hallucinated package names. That last figure is the one that should stick. Each unique invented name is a slot an attacker can register. The 19.7% is the frequency of the opportunity; the 205,474 is the size of the target list.
Frontier models have improved, and the improvement is measured. A 2026 re-evaluation on the current cohort, The Range Shrinks, the Threat Remains, tested five frontier models across roughly 200,000 prompts and found rates between 4.62% and 6.10% — tighter and lower than the 2025 spread, still well above zero. My deliberate, well-specified single-package prompts land under even that range, which is what you’d expect from the easiest version of the task.
So three numbers, three conditions. My 1% is a floor: a careful model, common tasks. Around 5% is where frontier models sit under controlled testing. The 19.7% is the blended reality once you include open-source models and harder prompts. The rate is a function of model and task, and the plain reading is that it never reaches zero.
Why One Missing Import Is A Supply-Chain Problem
BrassCoders treats a non-existent import as a HIGH-severity finding, the same tier it assigns SQL injection, because an unregistered package name is a live attack surface the moment anyone registers it. A name that returns 404 today is a name an attacker can own tomorrow.
The attack chain is short. An AI assistant suggests a name that doesn’t exist. A developer pastes the import in. An attacker who has watched which names models invent registers one on PyPI with a setup.py that runs code on install — Python executes install-time hooks with no sandbox, by design. The next pip install completes the attack. This is slopsquatting, and it exploits the one property that makes AI hallucination different from human typos: the model produces the same plausible name every time anyone asks the same question, so the target list is precomputable.
It’s not theoretical. Bar Lanyado at Lasso Security noticed models repeatedly recommending huggingface-cli when the real install is pip install -U "huggingface_hub[cli]". He registered the hallucinated name as a benign, empty package. It drew more than 30,000 downloads over three months, and Alibaba pasted the invented command straight into a public repository README. The payload was harmless because Lanyado made it harmless. An attacker registering that same name would not. The pattern maps onto OWASP A08:2021, Software and Data Integrity Failures, with one added property: the AI-generated name is consistent, which makes the whole attack repeatable.
Run the math on the low rate. A team merging a few hundred AI-suggested imports a week, at 1%, ships a handful of unresolvable names every week. Each one is a coin flip on whether someone got there first.
How BrassCoders Flags It
BrassCoders runs a package-hallucination check that walks each Python file’s imports with an AST parse, checks the local environment first, then queries the PyPI JSON API and flags any name that 404s at HIGH severity with its file and line number. The finding carries the package name, the import type, and a note that a malicious package under that name would be a supply-chain risk.
The check is opt-in for a reason. BrassCoders’s OSS core is offline-first and makes zero outbound calls by default; the registry lookup is the single path that has to talk to the network, so you turn it on with --check-package-hallucination, and --offline overrides it back off. What leaves your machine is the bare package name, nothing else — no source, no project context, no telemetry. The same check covers npm and pkg.go.dev, so a hallucinated JavaScript or Go import gets caught the same way.
The result is deterministic. The name either resolves on the registry or it doesn’t, and there’s no model in the loop deciding what counts, so the same imports produce the same flags on every run. That’s the division of labor BrassCoders is built on: the scanner reports the fact that a name doesn’t resolve, and the AI assistant reading the YAML — Claude Code, Cursor, whatever you run — decides whether to swap the import, remove the code, or accept the risk. BrassCoders is the current release, 2.0.12, Apache 2.0, Python 3.10+.
Reproduce The Probe Yourself
BrassCoders built the probe as a one-line loop anyone can run: pass a list of package names to curl against the PyPI JSON API and flag every 404. On the 96-name corpus below, that loop returns exactly one miss, python-feature-flags.
while read -r name; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/${name}/json")
[ "$code" = "404" ] && echo "MISSING: $name"
done < packages.txt
The full corpus, so the number is checkable and not just asserted:
requests httpx aiohttp fastapi djangorestframework pandas polars matplotlib
plotly scikit-learn transformers huggingface-hub tiktoken openai anthropic
langchain llama-index chromadb pinecone-client python-dotenv pydantic-settings
typer rich tqdm faker sqlalchemy alembic sqlmodel psycopg2-binary asyncpg
aioredis boto3 google-cloud-storage pillow pypdf pdfkit python-docx openpyxl
beautifulsoup4 playwright reppy requests-retry pyjwt passlib tenacity slowapi
pybreaker geopy pydub SpeechRecognition kafka-python loguru sentry-sdk rapidfuzz
orjson diskcache arq python-feature-flags flagsmith phonenumbers pycountry
better-profanity python-slugify humanize bleach deepdiff forex-python user-agents
strawberry-graphql tortoise-orm gino icalendar qrcode exifread langdetect
html2text asyncio-mqtt dateparser minio pyarrow python-magic respx email-validator
aiolimiter aioretry cachetools croniter humanfriendly openapi-python-client
sseclient-py jsonschema strictyaml redlock gql opentelemetry-api flag
The shell loop is the manual version, and it only catches what you remember to paste into it. BrassCoders runs the same check against every import in a scan, on every commit, and writes the flagged names into .brass/ai_instructions.yaml for your assistant to act on. Install it and point it at a repo your AI helped write:
pip install brasscoders
brasscoders scan --check-package-hallucination /path/to/your/project
One name in ninety-six is a rate you can live with right up until the week it’s the name someone was waiting for.
Frequently Asked Questions
How often does AI recommend packages that don't exist?
BrassCoders ran an original 96-package probe against the live PyPI API and found 1 name that didn't exist — about 1%. That is the best case for a careful frontier model on well-specified tasks; the large-scale USENIX Security 2025 study measured a 19.7% blended rate across 16 models, climbing from 5.2% on commercial models to 21.7% on open-source ones. The rate depends on the model and the prompt, and it is never zero.
What is slopsquatting?
BrassCoders treats slopsquatting as the attack that turns an AI hallucination into malware: an attacker registers a package name that AI assistants invent repeatedly, so the next developer who runs pip install on that suggested name fetches attacker-controlled code that runs at install time. The name never existed until the attacker created it, which is what separates slopsquatting from ordinary typosquatting that preys on misspellings of real packages.
Does a low hallucination rate mean it's safe to skip the check?
BrassCoders found 1 invented name in 96, and that single miss is indistinguishable from the 95 real ones until you query the registry — it's lowercase, hyphenated, semantically obvious, and it passes a human diff review. One percent across a team that merges hundreds of AI-suggested imports a week is a steady supply of squattable names, which is why BrassCoders checks every import rather than sampling.
How does BrassCoders detect hallucinated packages?
BrassCoders parses every import in a scanned Python file with an AST walk, checks the local environment first, then queries the PyPI JSON API at pypi.org/pypi/<name>/json; any name that returns 404 becomes a HIGH-severity finding with the file and line number. The check is opt-in via --check-package-hallucination because it's the one scan path that makes an outbound call, and passing --offline forces it back off.
Why did the frontier model score better than the published 19.7%?
BrassCoders generated the 96 names from one commercial frontier model on well-specified, mostly-mainstream tasks, which is the lowest-hallucination corner of the problem. The USENIX 19.7% headline blends 16 models including open-source ones that hallucinated around 21.7%, and a 2026 re-evaluation of five frontier models still measured 4.62% to 6.10%. Different models and harder prompts push the rate up; the floor is low, but it isn't zero.
Does the check replace pip-audit or Snyk?
BrassCoders's phantom-package check and CVE scanners cover different windows: BrassCoders flags names that don't exist before pip install runs, while pip-audit and Snyk scan already-installed packages for known vulnerabilities. A hallucinated name isn't in any vulnerability database yet — there's nothing to audit until an attacker registers it — so the two layers are complementary, not redundant.