Python Dependency Security: Audit, Pin and Verify

How to find vulnerable Python packages and stop surprises: scan with pip-audit, pin with lockfiles, verify with hashes, and automate updates.

Run pip-audit on what you deploy, pin everything with a lockfile, and let a bot open update pull requests. Those three steps catch most of the problems that make the news, such as the Pillow security release in October 2026.

1. Find known vulnerabilities with pip-audit

pip-audit scans Python environments, requirements files and projects for packages with known vulnerabilities. By default it uses the Python Packaging Advisory Database through the PyPI JSON API, and it can also use OSV.

python -m pip install pip-audit

pip-audit                        # the current environment
pip-audit -r requirements.txt    # a requirements file
pip-audit .                      # a local project
pip-audit --locked .             # lockfiles (pyproject.toml and pylock.*.toml)

Useful options from the project’s README:

  • pip-audit --fix upgrades dependencies that have known vulnerabilities. Add --dry-run to see what it would change first.
  • -f json (also columns, cyclonedx-json, cyclonedx-xml and markdown) changes the output format.
  • The exit code is 0 when nothing is found and 1 when something is, so a CI job fails by default.

One warning from the README is worth repeating: auditing a requirements file works much like installing it, so only audit files you would be willing to pip install.

A GitHub Action, pypa/gh-action-pip-audit, and a pre-commit hook are both available for CI.

2. Pin everything with a lockfile

An audit is only useful if it looks at what you ship. Unpinned dependencies resolve to whatever is newest at install time, so the thing you audited on Monday isn’t what the server installs on Friday.

For applications, commit a lockfile that pins every direct and transitive dependency:

  • With uv, uv sync --locked raises an error if the lockfile is out of date instead of updating it, and --frozen uses the lockfile without checking. Use --locked in CI and Docker builds so a stale lockfile fails loudly.
  • With Poetry, commit poetry.lock.
  • With plain pip, pin exact versions in requirements.txt with ==.

See requirements.txt vs pyproject.toml for how these files fit together, and Poetry vs uv vs Hatch for the tools.

Libraries are different: they should declare compatible ranges, not exact pins, so applications can resolve them together.

3. Verify what you download with hashes

Pip’s hash-checking mode verifies each downloaded file against hashes you recorded. It protects against a tampered or substituted package and against network corruption.

FooProject == 1.2 \
  --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \
  --hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
pip install --require-hashes -r requirements.txt

The rules, from pip’s documentation:

  • Adding --hash to any requirement turns the mode on for the whole file.
  • Every requirement and every dependency needs a hash, and each must be pinned with ==, a URL or a path. A partly hashed file is an error.
  • Use sha256 or stronger. The #sha256= fragments in PyPI download links don’t count, because they come from the same remote source.
  • Use pip hash to compute hashes for archives pip didn’t download, such as a source distribution next to a wheel.

Writing these by hand is tedious, so generate them. Lockfile tools such as uv record hashes in the lockfile for you.

4. Automate updates

A pinned dependency is safe only until the next advisory. A bot that opens pull requests keeps the pins moving:

  • GitHub’s Dependabot supports several Python tools, including pip, pipenv, pip-compile and poetry. Check GitHub’s supported ecosystems page for your package manager, as support varies.
  • Run pip-audit in CI as well, so a new advisory against an old pin fails the build even when no update PR exists.

5. Check for malware, not only known bugs

Advisories describe bugs in real packages. They don’t help with a malicious package that was published yesterday. uv has a preview option that scans your lockfile against OSV for known malware when you sync: set audit.malware-check = true, or UV_MALWARE_CHECK=1, and a match stops the sync. It is a preview feature, so check uv’s documentation before relying on it.

Habits matter here too:

  • Read the name twice before adding a dependency, since typo-squatted names exist.
  • Prefer packages with active maintenance and a known source repository.
  • Add new dependencies in their own pull request so someone looks at them.

What to do when an advisory lands

  1. Find out if you are affected: pip show <package> in every environment, including containers and servers.
  2. Upgrade: python -m pip install -U "package>=fixed.version", then update the lockfile.
  3. Rebuild: container images, wheels and deployed servers keep the old version until rebuilt.
  4. Narrow the exposure if you can’t upgrade yet. For the Pillow batch, the common thread was files supplied by an attacker, so limit what untrusted input reaches the library.

Keep your interpreter current too. Python 3.15.2, released on 25 October 2026, included security fixes for ssl, tarfile and http.cookiejar; see the Python version support dates to check yours still gets them.

Frequently Asked Questions

How do I check my Python dependencies for vulnerabilities?

Run pip-audit. Install it with python -m pip install pip-audit, then run pip-audit for the current environment, pip-audit -r requirements.txt for a requirements file, or pip-audit . for a local project. It checks packages against the Python Packaging Advisory Database through the PyPI JSON API by default.

What is pip-audit?

pip-audit is a command-line tool maintained under the Python Packaging Authority that scans Python environments, requirements files and projects for packages with known vulnerabilities. It exits with code 1 when it finds one, which makes it usable as a CI check.

What does pip install —require-hashes do?

It makes pip verify every downloaded package against a hash you recorded in your requirements file, so a tampered or substituted file is rejected. Every requirement must be pinned with == and carry at least one —hash, including all dependencies.

Should I commit a lockfile?

For applications, yes. A lockfile pins every direct and transitive dependency to an exact version, so builds are repeatable and an audit looks at what you actually deploy. In uv, uv sync —locked fails if the lockfile is out of date.

Does Dependabot work with Python?

Yes. GitHub’s Dependabot supports pip, pipenv, pip-compile and poetry among its Python options. Check GitHub’s supported-ecosystems page for your package manager, since support differs by tool and changes over time.

Is pip-audit enough on its own?

No. It only reports known, published vulnerabilities. It will not catch a malicious package that has no advisory yet, an abandoned package, or a typo-squatted name. Pinning, hash checking and reviewing new dependencies cover the gaps.