To check Python code for security issues you need two passes: a static analysis pass over the source you wrote, and a dependency pass over the packages you imported. Most teams do the first and skip the second, which is backwards — the majority of exploitable defects in a modern Python service live in transitive dependencies, not in the application code. A complete Python code check covers both.
This guide walks through the tools, the exact commands, and how to wire them into CI so the check runs on every pull request instead of once a quarter.
Start with static analysis of your own code
The de facto standard for scanning first-party Python source is bandit, maintained under the PyCQA umbrella. It walks your abstract syntax tree looking for known-dangerous patterns: use of eval, subprocess calls with shell=True, hardcoded passwords, weak hashing, insecure deserialization with pickle, and more.
pip install bandit
bandit -r ./src -f screen
The -r flag recurses through a directory. Each finding gets a severity and a confidence rating. A typical high-severity, high-confidence hit looks like this:
import subprocess
# bandit B602: subprocess call with shell=True is dangerous
subprocess.call("ping " + user_input, shell=True)
That pattern is a command injection waiting to happen. The fix is to pass an argument list and drop the shell:
subprocess.run(["ping", user_input], check=True)
Bandit will produce false positives — it flags patterns, not proven exploits. Triage them with inline # nosec comments only after you have confirmed the specific line is safe, and add a short justification so the next reviewer understands the exception.
Add type and lint checks that catch security-adjacent bugs
ruff and mypy are not security scanners, but they catch the sloppy code that turns into vulnerabilities. Ruff can enforce a large subset of bandit's rules through its S (flake8-bandit) ruleset, which means you can consolidate linting and basic security checks into one fast pass:
pip install ruff
ruff check --select S ./src
Ruff is written in Rust and runs in milliseconds on most repositories, so there is no excuse to leave it out of the pre-commit hook.
Check the dependencies, not just your code
This is the step teams forget. When you pip install requests, you also install urllib3, certifi, charset-normalizer, and idna. A known vulnerability in any of those is your vulnerability. To do a real Python code check you have to enumerate every installed package and match it against a vulnerability database.
pip-audit, maintained by the PyPA, queries the Python Packaging Advisory Database:
pip install pip-audit
pip-audit -r requirements.txt
Output lists each affected package, the vulnerability ID, and the version that fixes it:
Name Version ID Fix Versions
------- ------- ------------------ ------------
jinja2 3.1.2 GHSA-h5c8-rqwp-cp95 3.1.3
For projects using Poetry or PDM, point the audit at the resolved lockfile so you are scanning exactly what ships, not the loose ranges in your manifest. This transitive angle is where an SCA tool such as Safeguard adds value — it can flag a vulnerable package pulled in three levels deep that never appears in your requirements.txt at all. If you want a broader treatment of the discipline, the software composition analysis product page covers the mechanics.
Do not commit secrets, and check that you did not
Credential leaks are the most common and most damaging finding in real audits. A single AWS key in git history can end a company. Add a secret scan to your Python code check:
pip install detect-secrets
detect-secrets scan > .secrets.baseline
Commit the baseline, then run detect-secrets audit .secrets.baseline to walk through each candidate and mark true or false. In CI, fail the build if new secrets appear that are not in the baseline.
Wire it all into CI
A code check that only runs on your laptop protects nothing. Here is a minimal GitHub Actions job that runs every check on each pull request:
name: python-security
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install bandit pip-audit ruff
- run: ruff check --select S ./src
- run: bandit -r ./src -ll
- run: pip-audit -r requirements.txt
The -ll flag on bandit reports only medium-and-above severity so the build does not fail on noise. Tune the threshold as your codebase matures — start permissive, tighten over time.
Read the results like an engineer, not a checklist
The point of the check is not a green build; it is understanding your risk. When pip-audit flags a transitive dependency, ask three questions before you scramble to patch: does my code reach the vulnerable function, is the vulnerable code path exposed to untrusted input, and is a fixed version available without a breaking major bump. A CVSS 9.8 in a code path you never call is lower real-world risk than a CVSS 6.5 in your request-handling hot path. Reachability context turns a raw list into a prioritized queue.
If you want to build the muscle for this kind of triage, the Safeguard Academy has walkthroughs on reading advisories and scoping impact.
FAQ
What is the best free tool to check Python code for security issues?
There is no single tool. Use bandit (or ruff --select S) for your own source, pip-audit for dependency vulnerabilities, and detect-secrets for leaked credentials. Together they cover the three main classes of Python security defect and all are free and open source.
How often should I run a Python code check?
On every pull request, automatically, in CI. Static analysis is cheap enough to run on each commit. Dependency scans should also run on a schedule (nightly or weekly) because new vulnerabilities are disclosed against packages you already have installed, even when your code has not changed.
Does bandit find vulnerabilities in third-party packages?
No. Bandit analyzes the source files you point it at. It does not understand your dependency tree or match installed package versions against advisories. That is what pip-audit and SCA tools do. You need both a source scanner and a dependency scanner for full coverage.
Can I fail my build automatically on security findings?
Yes, and you should — but tune the severity threshold. Start by failing only on high-severity, high-confidence static findings and any fixable dependency vulnerability, then tighten the gate as your team clears the initial backlog. Failing on every low-confidence finding on day one just teaches people to ignore the check.