Safeguard
DevSecOps

Python Code Analysis: Tools and Techniques for Secure Code

How static and dynamic Python code analysis catches security bugs before they ship, from Bandit and Semgrep to dependency scanning and taint tracking.

Priya Mehta
DevSecOps Engineer
5 min read

Python code analysis is the practice of inspecting Python source, without or with execution, to find security defects, and for most teams the highest-value starting point is static analysis with Bandit and Semgrep wired into CI. Python's readability hides real hazards: dynamic typing lets type-confusion bugs through, eval and pickle are footguns, and the dependency graph pulls in code you never read. Analysis tooling exists to surface those problems mechanically instead of hoping a reviewer notices.

Static versus dynamic analysis

Static analysis reads the code without running it. It is fast, runs on every commit, and finds patterns like hardcoded secrets, use of dangerous functions, and injection-prone string building. Its weakness is false positives and an inability to reason about values only known at runtime.

Dynamic analysis observes the program while it executes, typically against a running service. It finds issues that only appear with real data flowing through, at the cost of needing a deployed target and test coverage that actually exercises the risky paths.

A mature Python security setup uses both, but static analysis is where you start because the barrier to entry is a single CI step.

Bandit: the Python-specific baseline

Bandit is the de facto static analyzer for Python security. It ships a set of plugins that flag well-known dangerous patterns, and it understands Python's AST rather than matching raw text. Install and run it against a package:

pip install bandit
bandit -r ./src -ll

The -ll flag limits output to medium and high severity findings, which keeps the initial signal manageable. Bandit catches things like subprocess calls with shell=True, use of yaml.load without a safe loader, weak hash algorithms, and assert statements used for security checks (which vanish under python -O).

Tune it with a .bandit config or inline # nosec comments, but require a justification comment next to every # nosec so suppressions get reviewed rather than sprinkled to silence noise.

Semgrep: rules you can read and write

Semgrep sits alongside Bandit and is more flexible because rules are pattern templates that look like the code they match. A rule to catch eval on request data reads almost like the vulnerable code itself. The public registry has a solid Python security ruleset:

pip install semgrep
semgrep --config "p/python" ./src

Semgrep's strength is that your security team can encode organization-specific patterns, for example flagging a deprecated internal crypto helper, without learning a plugin API. Its taint-mode rules track data from a source (say, a Flask request argument) to a sink (a database call or os.system) and only alert when untrusted data reaches somewhere dangerous, which cuts false positives sharply.

Type checking as a security control

mypy and pyright are usually filed under code quality, but type checking is a real security layer in Python. Type confusion, where a function assumes a str and gets a dict, causes crashes and occasionally exploitable behavior. Running a type checker in strict mode on the modules that handle external input catches a class of bugs that Bandit and Semgrep do not look for:

pip install mypy
mypy --strict ./src/api

Don't forget the dependencies

Your own code is a fraction of what ships. The rest is third-party packages, and that is where a large share of real-world Python vulnerabilities live. Software composition analysis scans your requirements.txt, poetry.lock, or Pipfile.lock against vulnerability databases and tells you which installed versions carry known CVEs, including transitive dependencies you never chose directly. An SCA tool such as Safeguard can flag a vulnerable transitive package that no amount of source-level Python code analysis would ever reveal, because the flaw is not in your code at all.

Pin versions in a lockfile so the scan reflects exactly what installs, and fail the build on new high-severity findings rather than filing a ticket nobody reads.

Wiring it into CI

The pattern that sticks is a fast pre-commit hook plus a stricter CI gate:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.9
    hooks:
      - id: bandit
        args: ["-ll", "-r", "src"]

Pre-commit gives developers feedback in seconds. The CI gate runs Semgrep and the dependency scan and blocks merges on regressions. Keep the failing threshold honest, if every build is red, people stop reading the output, and the whole exercise becomes theater. The Safeguard Academy has a walkthrough of tuning these gates so they stay credible.

FAQ

What is the best free tool for Python code analysis?

Bandit is the strongest free, Python-specific security analyzer and the best first tool to adopt. Pair it with Semgrep's free tier for taint tracking and custom rules, and a dependency scanner for third-party CVEs.

Does static analysis replace code review?

No. Static analysis catches mechanical, pattern-based defects consistently, which frees human reviewers to focus on logic, authorization, and design flaws that tools cannot judge. The two are complementary.

How do I reduce false positives in Python code analysis?

Use taint-mode rules that only fire when untrusted data reaches a dangerous sink, tune severity thresholds, and require a justification comment on every suppression so the ignore list stays reviewed rather than silently growing.

Do I need dynamic analysis for a Python API?

It is valuable but secondary. Start with static analysis and dependency scanning, then add dynamic scanning against a deployed instance to catch issues that only appear with live data and configuration.

Never miss an update

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