pip-audit is the PyPA-maintained scanner that checks your Python dependencies against the Python Packaging Advisory Database and OSV.dev, and a single command — pip-audit -r requirements.txt — will tell you which of your pinned packages carry known CVEs. What it will not tell you is whether a package is malicious, whether your transitive dependencies have drifted since the last lock, or whether the vulnerable code path is ever executed. This guide covers how to run pip-audit properly, how to gate CI on it without the team learning to ignore the job, and the gaps you still have to close some other way.
Install it isolated from the project
pip-audit has its own dependency tree — pip-api, cachecontrol, cyclonedx-python-lib, and friends. If you install it into the same virtualenv you are auditing, you can silently upgrade or downgrade packages in the environment you are trying to measure. Keep it out of the project:
pipx install pip-audit
pip-audit --version # pip-audit 2.7.2 at time of writing
In CI, installing it into the job's throwaway environment with python -m pip install pip-audit==2.7.2 is fine, because nothing persists. Pin the tool version either way. An unpinned scanner that updates itself mid-week is a great way to get a red pipeline that nobody can reproduce locally.
Know which of the two scan modes you are running
Run bare, pip-audit audits whatever is importable in the current environment. Run with -r, it resolves the requirements file in a temporary isolated environment and audits the result:
# Audit the active virtualenv
pip-audit
# Audit a requirements file (resolves in a temp env)
pip-audit -r requirements.txt
# Skip resolution entirely if the file is fully pinned
pip-audit -r requirements.txt --no-deps
The distinction matters. The environment scan reflects what actually runs; the requirements scan reflects what you declared. If those two disagree — and in older projects they usually do — that disagreement is itself a finding.
Output rows carry PYSEC identifiers with CVE and GHSA aliases. Add --desc for advisory text, and -f json or -f cyclonedx-json when a machine is the consumer:
pip-audit -r requirements.txt --desc -f json -o audit.json
Pin everything, or the results are fiction
Auditing a requirements file full of requests>=2.28 tells you about the version the resolver picked today, not the one running in production since March. Compile a full lock first:
pip-compile --generate-hashes -o requirements.txt requirements.in
pip-audit -r requirements.txt --require-hashes
--require-hashes gives you a second property for free: the audit fails if any pinned artifact's hash does not match PyPI, which catches a class of registry tampering that a plain version check never would. If you are on Poetry, poetry export -f requirements.txt --output requirements.txt gets you the same fully-pinned input.
Gate CI without becoming ignorable
A minimal GitHub Actions step:
- name: Audit Python dependencies
run: |
python -m pip install pip-audit==2.7.2
pip-audit -r requirements.txt --strict --desc
pip-audit exits non-zero when it finds anything, so this blocks the merge by default. Two flags keep it honest. --strict fails the run if any dependency could not be audited (private packages, yanked versions) instead of silently skipping it. And --ignore-vuln PYSEC-2023-192 handles accepted risk — but leave a comment with an expiry date next to every ignore, or your allowlist becomes a graveyard.
One pattern that works: block PRs only on newly introduced advisories, and handle the pre-existing backlog in a separate scheduled job. A gate that fails on day one for 40 inherited findings gets deleted by day five.
The --fix flag: useful, occasionally reckless
pip-audit -r requirements.txt --fix --dry-run
--fix rewrites specifiers in place to the nearest fixed release. It checks that the new set resolves; it does not check that your code still works, and a jump from Django 3.2 to 4.2 will resolve just fine while breaking half your middleware. Treat --fix as a pull-request generator, never as a deploy step. Run it with --dry-run first, always behind your test suite.
What pip-audit misses
This is the part vendors gloss over. Four gaps, in rough order of how much they should worry you:
| Gap | Why pip-audit misses it | What covers it |
|---|---|---|
| Malicious packages | Typosquats live for days before any advisory exists; pip-audit only matches published advisories | Registry-level analysis, install-script review |
| Bundled native code | Pillow ships its own libwebp; the CVE-2023-4863 exposure lived in a .so, not in Python package metadata, until advisories caught up | Container scanning, binary analysis |
| System packages | Debian's openssl and zlib never appear in pip list | OS-level scanners against the image |
| Reachability | No call graph — an advisory in a module you never import scores the same as one in your request path | Reachability-aware SCA |
The last one is where teams drown. A mid-size Django monolith routinely surfaces 60-plus advisories, of which a handful are actually reachable. Reachability-aware SCA exists precisely to make that cut for you, and it is the main axis on which commercial tools differ — the Snyk comparison goes through how Safeguard and others handle it. If you live in Rust as well, the same known-vuln-versus-policy split shows up in cargo-audit vs cargo-deny.
None of this makes pip-audit optional. It is free, fast, PyPA-maintained, and catches the embarrassing stuff. It is a floor, not a ceiling.
Frequently asked questions
Does pip-audit work with Poetry or Pipenv projects?
Yes, indirectly. Export a pinned requirements file first (poetry export -f requirements.txt or pipenv requirements), then audit that. Alternatively, install the project into a virtualenv and run bare pip-audit against the environment.
How is pip-audit different from Safety?
Safety queries its own commercial database and its free tier has narrowed over the years; pip-audit is maintained under the PyPA umbrella and reads the open PyPI Advisory Database via OSV. For most teams the coverage is comparable and pip-audit's licensing is simpler.
Can pip-audit generate an SBOM?
It can emit CycloneDX with -f cyclonedx-json, which is handy for handing results to other tooling. For a production SBOM pipeline with component provenance you will want a dedicated generator rather than a scanner side-effect.
Should a pip-audit failure block merges?
Block on newly introduced advisories at high or critical severity; report everything else. A gate the team can pass by fixing their change stays enabled. A gate that punishes them for two years of inherited debt gets muted within a sprint.