Skip to main content

depverify

Checks whether the packages an LLM told you to pip install actually exist.

Advisory only. Zero LLM calls. Never installs, deletes, halts, or quarantines anything.


The problem, in 30 seconds

An LLM suggests this fix for you:

To build a quick CLI progress tracker with retry logic, install these:

pip install requests
pip install retry-backoff-pro
pip install tqdm

Then in your script:

import requests
import tqdm
from retry_backoff_pro import RetryPolicy

policy = RetryPolicy(max_attempts=3)

requests and tqdm are real. retry-backoff-pro reads like a real, small retry-helper library. It isn't -- it does not exist on PyPI. If an attacker registers that exact name with malware before you run the install (a real technique known as slopsquatting), pip install will happily fetch it and your script will happily import it.

$ depverify check llm_answer.txt
✅ requests  [pip_install]  EXISTS
✅ tqdm  [pip_install]  EXISTS
➖ retry_backoff_pro  [import]  LOCAL_OR_UNKNOWN
❌ retry-backoff-pro  [pip_install]  NOT_FOUND

checked=4  not_found=1  suspicious=0  cant_verify=0

That's the whole tool. Deterministic report, one command.

Why

LLM coding models hallucinate package names. USENIX Security 2025 (Spracklen et al.) measured ~19.7% of recommended packages as hallucinated across 16 models (~5% commercial, ~21% open-source); a May 2026 replication found frontier models compressed to ~4.6-6.1%, but quantized local models remain far worse. Attackers register hallucinated names on PyPI with malware ("slopsquatting").

Every verdict is deterministic. Extraction is regex/AST, existence is a PyPI lookup, reputation is arithmetic over metadata. No LLM judging calls anywhere in this codebase -- the tool that checks for hallucinations doesn't itself hallucinate.

depverify Content-scanning tools (Socket, Snyk, etc.)
Checks name exists usually assumed, not the focus
Checks package content/behavior ❌ (by design -- see What this does NOT do)
Blocks installs / fails CI ❌ (report only) often yes
Model calls involved 0 varies by product
npm / yarn ❌ (Python/PyPI only) often yes

Install

pip install depverify              # core
pip install depverify[flask]       # + flask itself, for the HTTP integration below

depverify[flask] installs the flask dependency only. The HTTP integration itself (integrations/cortexfeed/flask_blueprint.py) is source-only, deliberately not part of the installed depverify package -- see Flask HTTP integration below.

Dev: pip install -e ".[flask,dev]" (pytest, pandas, flask).

Usage

depverify check path/to/answer.txt
cat answer.txt | depverify check -
depverify check answer.txt --json
depverify check answer.txt --symbols
depverify check answer.txt --ignore my-internal-lib --ignore 'acme-*'

A successful check's exit code is always 0, regardless of what it finds (NOT_FOUND, suspicious, etc.) -- this is an advisory tool, not a linter that fails your build over the contents of a report. That guarantee covers verdicts, not invocation: a CLI usage error (bad arguments, a missing/unreadable input file, non-UTF-8 input) is reported as a clean one-line message on stderr and exits non-zero, the same way depverify check with a missing required argument already does -- it is not itself a verdict, so it is not covered by the "always 0" guarantee.

If depverify finds nothing at all, it says so and explains why -- it only reads literal pip install <name> lines and import x / from x import y statements from raw text, not a requirements.txt-style file of bare name==1.2.3 entries, and not pyproject.toml/Pipfile/setup.py dependency tables. Point it at the actual text an LLM gave you, not your dependency manifest.

--symbols additionally checks statically-resolvable attribute chains (e.g. pandas.DataFrame.flatten) against packages already installed in the local environment. Security note: this is the one depverify feature that imports locally installed code, which can execute that code's import-time side effects -- see Security note: --symbols and local code execution before enabling it. Off by default; normal depverify check never imports or executes anything locally installed.

Internal / private packages

If you run a private package index (Artifactory, Nexus, an internal PyPI mirror), your own package names will otherwise come back NOT_FOUND or LOCAL_OR_UNKNOWN -- depverify only ever checks public PyPI, and has no way to know a name is legitimate on your infrastructure. Recognize those names so they're reported as INTERNAL instead, never queried against PyPI at all:

depverify check answer.txt --ignore my-internal-lib
depverify check answer.txt --ignore 'foo,bar,acme-*'   # repeatable and/or comma-separated
depverify check answer.txt --ignore-file .depverify-ignore

Or via a .depverify.toml in your project root (auto-discovered; no flag needed), or an explicit path with --config:

[internal]
names = ["my-internal-lib", "acme-*"]

All three sources are mergeable. Matching is name-only (never a network call), case-insensitive, PEP-503-normalized (so My_Internal.Lib, my-internal-lib, and MY-INTERNAL-LIB are all the same pattern), and supports shell-style glob wildcards via fnmatch (acme-*, not regex). INTERNAL applies to a matched name from either a pip install line or a bare import -- unlike LOCAL_OR_UNKNOWN, it's a confirmed verdict, not a "might be yours" guess. This feature is entirely opt-in: with no --ignore/--ignore-file/--config/.depverify.toml in play, behavior, verdicts, and Report.summary()'s shape are unchanged from before this feature existed (summary()'s internal key is present only when at least one INTERNAL verdict actually occurs).

Library

from depverify import verify_text

report = verify_text(llm_answer_text)
print(report.to_json())

Verdict model

Per detected dependency:

Verdict Meaning
EXISTS Confirmed on PyPI. Carries a risk object (see below).
NOT_FOUND Confirmed not on PyPI. Only assigned to pip install lines.
STDLIB_SKIPPED Part of the Python standard library -- not a PyPI lookup at all.
LOCAL_OR_UNKNOWN Unresolved import. May be a local module, a relative import, or an unconfigured private package (see INTERNAL below to stop guessing and configure it).
INTERNAL Matched a configured internal/private-package name or pattern (--ignore/--ignore-file/.depverify.toml, see Internal / private packages) -- not a PyPI lookup at all, applies to both pip install and import sources.
CANT_VERIFY Lookup itself failed (network, rate limit, etc.) -- not a verdict on the package.

EXISTS packages additionally carry a risk object: level ∈ ok | suspicious, with reasons ⊆ {young_package, low_downloads, near_name:<popular-package>, version_missing}.

Existence and reputation are orthogonal. A package that EXISTS can still be suspicious. depverify never folds "sketchy" into "missing" -- those are different questions with different implications.

The core asymmetry

A name that 404s on PyPI is interpreted differently depending on how it was detected:

Detected via Verdict Why
pip install <name> NOT_FOUND The LLM told you to install this; it doesn't exist; that's the signal slopsquatting exploits.
import <name> LOCAL_OR_UNKNOWN Might be your own module, a relative import target, or a name that isn't on PyPI for a legitimate reason. Flagging every unresolvable import as "not found" would bury real warnings under false positives from ordinary project code (import myapp.models, etc.).
Report shape (click to expand)
{
  "packages": [
    {"name_raw": "cv2", "resolved": "opencv-python", "source": "import",
     "verdict": "EXISTS", "risk": {"level": "ok", "reasons": []}},
    {"name_raw": "pdfreader-pro", "resolved": "pdfreader-pro", "source": "pip_install",
     "verdict": "NOT_FOUND", "risk": null}
  ],
  "summary": {"checked": 2, "not_found": 1, "suspicious": 0, "cant_verify": 0}
}

summary.cant_verify counts only package-level CANT_VERIFY verdicts. It does not include symbol-level CANT_VERIFY results (e.g. a --symbols chain that couldn't be checked because the package isn't installed locally) -- those live under each package's own symbols list (see --symbols below) and are a separate count from the top-level summary. This is deliberate: summary() predates the symbol-checking feature and was kept unchanged so existing consumers of Report.summary()/to_dict() see byte-identical output when check_symbols is left at its default.

Reputation signals (EXISTS packages only)

Signal How it's computed
Age Days since the earliest PyPI release.
Downloads Last-30-day count from pypistats.org. If that API is unreachable or rate-limited, depverify silently omits download-based reasons -- a missing download count is never treated as CANT_VERIFY.
Near-name difflib.SequenceMatcher.ratio() >= 0.88 (tunable, see reputation.py) against a vendored list of popular package names (depverify/top_packages.json), flagged as near_name:<popular-package> when the candidate itself isn't already a popular package.

Default suspicion rule (a tunable default, not ground truth): suspicious if (age < 60 days AND downloads < 1000/month) OR any near_name hit. Tune the constants in depverify/reputation.py for your risk tolerance.

What this does NOT do

  • No package-content scanning. depverify checks whether a name exists and looks reputable by metadata -- it does not download, sandbox, or static-analyze package code. For that, see dedicated tools like Socket or Snyk.
  • No blocking. Nothing here gates a pip install, a CI job, or an LLM response. It's a report.
  • No npm/yarn. Python/PyPI only, by design.
  • No LLM-based judging. Every verdict is deterministic: extraction is regex/AST, existence is a PyPI lookup, reputation is arithmetic over metadata. No model calls anywhere in this codebase.
Security note: --symbols and local code execution (click to expand)

Security note: --symbols and local code execution

Normal depverify check (no --symbols) never imports or executes any locally installed package code. Existence checks are PyPI metadata lookups over HTTP; reputation checks are pypistats.org lookups and arithmetic. Nothing in that path touches your local Python environment's installed packages, and nothing ever executes the LLM-generated code being scanned.

--symbols is different, and deliberately scoped narrowly because of it:

  • To check whether an attribute chain like pandas.DataFrame.flatten really exists, --symbols uses importlib.import_module() to import the already-installed package and getattr() to walk the chain. Importing a Python module runs that module's top-level code -- this is ordinary Python behavior, not something depverify adds, but it means --symbols is the one code path in this tool that executes local code as a side effect of scanning.
  • This is not the same as executing the LLM's code. --symbols never runs the snippet being checked; it only imports packages by name using importlib, and only ever calls getattr/hasattr on the resulting module or class objects -- never instantiates a class, never calls a function or method. (Confirmed in tests/test_symbols.py: walking a chain that reaches a property or method never triggers the property getter, the method body, or the class's __init__.)
  • --symbols never installs anything. If a package exists on PyPI but is not already installed locally, the result is CANT_VERIFY, not an install-then-import. depverify's "never installs, deletes, halts, or quarantines anything" guarantee holds for --symbols too.
  • The residual risk: if a malicious or already-compromised package happens to be installed in the same environment running depverify, --symbols will trigger that package's import-time code the same way a plain import thatpackage anywhere else in that environment would. depverify does not sandbox, isolate, or vet locally installed packages before importing them for a symbol check.
  • Consequently: only enable --symbols in an environment where the installed package set is already trusted -- the same trust you'd already extend to running python -c "import <installed package>" in that environment. Do not run --symbols as a way to safely inspect an environment whose installed packages you don't already trust, and do not present or rely on --symbols as a sandbox or security boundary around untrusted local installs -- it is not one.
Limitations (click to expand)
  • Dynamic imports are not detected. importlib.import_module("name") string literals are invisible to the AST/regex extraction in extract.py. Only literal import x / from x import y statements and pip install lines are found.
  • Only pip-style dependency declarations are parsed. setup.py, pyproject.toml ([project.dependencies]), Conda environment files, and Poetry's pyproject.toml dependency tables are not parsed.
  • The import-name -> distribution-name mapping table is incomplete by nature. depverify/mapping_table.py is hand-curated (71 entries as of this writing) and will always miss some real-world aliases. Unmapped import names fall back to querying PyPI with the import name as-is, which works for the common case (import name == distribution name) but not for every alias.
  • Verification is point-in-time. A name that 404s right now can be registered on PyPI minutes later -- including by an attacker watching for exactly this kind of hallucinated name (slopsquatting). Nothing here caches a "safe" verdict indefinitely; the cache TTLs (24h for EXISTS, 6h for NOT_FOUND) reflect that names that don't exist yet are the more time-sensitive case.
  • EXISTS ≠ safe: compromised legitimate packages are invisible to this tool. A package that exists, is old, and has millions of downloads can still ship malware in a compromised release. depverify's existence and reputation checks say nothing about supply-chain compromise of an otherwise-legitimate package.
  • depverify/top_packages.json was hand-vendored, not fetched live, because the environment this project was built in could not reach raw.githubusercontent.com, api.github.com, or any CDN mirror (only pypi.org / files.pythonhosted.org / bare github.com were reachable). It's a ~450-name curated list of well-known packages written from training knowledge, not the real top-5000 hugovk/top-pypi-packages dataset. Run scripts/fetch_top_packages.py from a network with GitHub raw-content access to regenerate the real list before relying on near-name detection for anything beyond obvious cases.
  • Attribute-chain verification (--symbols) only covers simple, statically-resolvable chains rooted directly in an imported name (e.g. pandas.DataFrame.flatten). It does not do instance attribute inference, signature/arity checking, type inference, .pyi stub parsing, or follow chains past a function call (requests.get(url).json is not checked past requests.get) or past a reassigned import. See depverify/symbols.py's module docstring for the full scope, and Security note: --symbols and local code execution before enabling it -- unlike the rest of depverify, it imports locally installed code.

Flask HTTP integration

integrations/cortexfeed/flask_blueprint.py is a small, source-only Flask Blueprint exposing verify_text over HTTP (POST /verify). It is not part of the installed depverify package -- pip install depverify[flask] only pulls in the flask dependency it needs, not this file itself. That's intentional: this blueprint is meant to be copied into your own Flask app (it imports only from depverify, nothing from cortexfeed, so it has no hidden dependency on the project it's named after), not imported from a pip-installed depverify. To use it, copy integrations/cortexfeed/flask_blueprint.py from a source checkout into your own project and register the blueprint it defines:

from flask_blueprint import depverify_bp   # after copying the file in
app.register_blueprint(depverify_bp)

See the file's own module docstring for the request/response shape.

Project layout

depverify/
├── depverify/                 # the library + CLI -- this is what `pip install depverify` ships
├── integrations/cortexfeed/   # source-only Flask blueprint; copy into your app, not pip-installed
├── tests/                     # unit tests (mocked, zero network) + eval/ (live network)
└── scripts/                   # fetch_top_packages.py: regenerate the vendored top-package list

Development

pip install -e ".[flask,dev]"
pytest tests/ --ignore=tests/eval     # unit tests, zero network
python tests/eval/run_eval.py         # eval, live network against PyPI/pypistats

No blocking. No LLM calls. Just an honest answer to "does this package exist?"

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

depverify-0.2.0.tar.gz (63.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

depverify-0.2.0-py3-none-any.whl (47.6 kB view details)

Uploaded Python 3

File details

Details for the file depverify-0.2.0.tar.gz.

File metadata

  • Download URL: depverify-0.2.0.tar.gz
  • Upload date:
  • Size: 63.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for depverify-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ba73e789b276e0e9482ee4235fe7f97114d7c73b96b1afe351ad0dec00c4d0d4
MD5 9ae9dacfe9c235dcf8ce7ce47aec693a
BLAKE2b-256 4a2fe7f55afdd0f01cd8f9c7e876add91aebf31463485ef9f0d97487080d336d

See more details on using hashes here.

Provenance

The following attestation bundles were made for depverify-0.2.0.tar.gz:

Publisher: publish.yml on AeroScissors/Depverify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file depverify-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: depverify-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 47.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for depverify-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 10a6de3d0f1ce557d5401fdc4453963c06c3db20f2cb3742a8aed63ca6ce3575
MD5 91d5f54b457f6b66dff64480d9f3cd15
BLAKE2b-256 59886f618fd5aeff09008866da50358389dc104d6c02b166275d84883aee1eb4

See more details on using hashes here.

Provenance

The following attestation bundles were made for depverify-0.2.0-py3-none-any.whl:

Publisher: publish.yml on AeroScissors/Depverify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page