Open Source Security

How to Verify a PyPI Package Before Install

A practical pre-install verification workflow for PyPI packages covering sigstore attestations, maintainer checks, and sdist auditing.

Shadab Khan
Security Engineer
5 min read

Typosquatting, compromised maintainer accounts, and malicious post-install hooks are common on PyPI, and pip install runs arbitrary Python code from the package's setup hooks the moment you run it. That means verification has to happen before the install, not after. This tutorial walks through a pragmatic pre-install checklist you can run in under 15 minutes for any unfamiliar package: verify the package exists and matches what you think it does, check its sigstore attestation if available, download the sdist and inspect it, and scan it with pip-audit. You will finish with a reusable shell function you can drop into your dotfiles.

Prerequisites: Python 3.10+, pip 23.2+, pip-audit 2.6+, sigstore 2.1+, and jq. Time to complete: 15 minutes the first run, under 5 minutes thereafter.

How do I confirm the package I am installing is the one I intend?

Start with the PyPI JSON API to inspect metadata without downloading the package. This catches typosquats before you ever pull a wheel:

PKG=requests
curl -s "https://pypi.org/pypi/${PKG}/json" | \
  jq '{name, version: .info.version, author: .info.author, \
       home: .info.home_page, project_urls: .info.project_urls, \
       releases: (.releases | length)}'

Three red flags:

  1. A release count of 1 or 2 when the project claims a long history.
  2. home_page or project_urls pointing to a repo that does not exist or has a different name.
  3. An author field that does not match what the GitHub repo shows.

Compare package name against the top PyPI list. Typosquats frequently target top-100 packages with single-letter swaps, like requsts or python-requsts. A quick check:

pip install pypi-command-line
pypi search "${PKG}" --top 5

If the package you want does not show up on top of a direct search, be suspicious.

How do I verify a sigstore attestation on the package?

PyPI began supporting PEP 740 attestations in late 2024, and many major packages now publish sigstore bundles. The sigstore CLI verifies them against a publisher identity:

pip download --no-deps requests==2.31.0 -d /tmp/verify
sigstore verify identity \
  --cert-identity-regex "^https://github.com/psf/requests/.+$" \
  --cert-oidc-issuer https://token.actions.githubusercontent.com \
  /tmp/verify/requests-2.31.0-py3-none-any.whl

A clean run prints OK: requests-2.31.0-py3-none-any.whl. If the package does not publish attestations, the verify step fails with "no matching bundles found," which is a cue to be more thorough with the manual checks below rather than an automatic reject.

For packages without attestations, fall back to comparing the sdist's declared Home-page against the actual published artifacts on GitHub. Attestations are the strongest signal, but they are not yet universal.

How do I inspect the sdist for suspicious code?

Download the source distribution and extract it before pip ever runs setup.py:

pip download --no-binary=:all: --no-deps "${PKG}==2.31.0" -d /tmp/verify
tar -xzf /tmp/verify/${PKG}-2.31.0.tar.gz -C /tmp/verify
cd /tmp/verify/${PKG}-2.31.0

Scan for common malicious patterns:

grep -RniE "eval\(|exec\(|base64\.b64decode|urllib\.request\.urlopen|subprocess\.Popen|os\.system" \
  --include="*.py" .
grep -RniE "setup_requires|post_install|build_py" setup.py setup.cfg pyproject.toml

Legitimate packages sometimes hit these matches. The question to ask is whether the matches are in code that runs during install (in setup.py, a setuptools entry point, or pyproject.toml build hooks) or in the library's runtime code. Install-time network calls or subprocess invocations are the main warning sign. A tight heuristic: anything that writes to ~/.ssh, ~/.aws, or env files in a setup hook is hostile.

How do I run a quick vulnerability and license scan?

Use pip-audit against the single package in a clean virtualenv so the scan does not see your global packages:

python -m venv /tmp/audit-venv
source /tmp/audit-venv/bin/activate
pip install "${PKG}==2.31.0"
pip-audit --format json > /tmp/audit.json
jq '.dependencies[] | select(.vulns | length > 0)' /tmp/audit.json
deactivate

This surfaces known CVEs against the specific version and its pulled-in transitives. For license, run:

pip install pip-licenses
source /tmp/audit-venv/bin/activate
pip-licenses --format=json --with-urls | \
  jq '[.[] | select(.License | test("GPL|AGPL|SSPL"; "i"))]'
deactivate

A non-empty result means a copyleft license is in the transitive tree, which may or may not matter depending on how you ship your product.

How do I turn this into a reusable pre-install command?

Wrap the steps into a shell function in your ~/.bashrc or ~/.zshrc:

pypi-verify() {
  local pkg="$1"
  local ver="${2:-}"
  local spec="${pkg}${ver:+==${ver}}"
  echo "==> Metadata"
  curl -s "https://pypi.org/pypi/${pkg}/json" | \
    jq '{author: .info.author, home: .info.home_page, releases: (.releases | length)}'
  echo "==> Sigstore"
  local tmp=$(mktemp -d)
  pip download --no-deps "${spec}" -d "${tmp}" >/dev/null 2>&1
  sigstore verify identity --cert-oidc-issuer https://token.actions.githubusercontent.com \
    --cert-identity-regex ".*" "${tmp}"/*.whl 2>&1 | head -5
  echo "==> pip-audit"
  local venv=$(mktemp -d)
  python -m venv "${venv}" && "${venv}/bin/pip" install "${spec}" >/dev/null 2>&1
  "${venv}/bin/pip-audit" 2>/dev/null | head -20
  rm -rf "${tmp}" "${venv}"
}

Call it as pypi-verify requests 2.31.0 before any first-time install. The function is not a replacement for CI scanning, but it turns a thirty-minute manual audit into a thirty-second shell command.

When should I escalate past this checklist?

Escalate to a deeper review whenever any of the following is true: the package is less than 90 days old, the maintainer account changed within the last 30 days, the repository linked from PyPI has fewer than 10 commits, or the sdist contains compiled binaries you cannot trivially reproduce. A deeper review means sandboxed execution under firejail or a disposable container, full strace of the install phase, and a signed sign-off from a second engineer.

How Safeguard Helps

Safeguard runs all of these checks continuously against every PyPI package your projects declare, and surfaces the results alongside reachability data so you know whether a risky dependency is actually exercised by your code. Griffin AI narrates the verification chain for any package, including attestation status, maintainer reputation, and diff from previous versions. SBOMs are updated on every push, and policy gates block merges for packages that fail attestation, have known malicious-package reports, or introduce unexpected transitive chains. What this tutorial does by hand for one package, Safeguard does automatically for every install candidate across your monorepo.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.