A misplaced == instead of is, a hardcoded database password, an API endpoint that accepts arbitrary user input without validation — these are the bugs that bring Python applications down. Python static code analysis catches them before the code ever runs. This guide covers every major tool in the 2026 Python ecosystem — from the blazing-fast Ruff linter to mypy's strict type checking and Bandit's security scanning — and explains how to layer them into a workflow that keeps your codebase clean without slowing your team down.

What Is Python Static Code Analysis?

Python Static Analysis Pipeline Linter (Ruff) Style · Unused imports · PEP 8 Type Checker (mypy) Type errors Annotation gaps Security Scan (Bandit) SQLi · Secrets Weak crypto Diff Review (Diff Checker) Visual change review Fast local Type safety SAST gate Human gate 1 2 3 4 Run all stages on every pull request; stages 1 & 4 run locally on every commit
The four-stage Python static analysis pipeline — linting, type checking, security scanning, and visual diff review — each catching distinct issue categories.

Python static code analysis is the automated inspection of Python source files without executing them — a discipline known broadly as static program analysis. A static analyzer parses your .py files into an abstract syntax tree (AST), applies a configurable rule set to that tree, and reports findings — style violations, type mismatches, unused variables, security vulnerabilities — directly back to the developer in the editor or CI pipeline.

The term "static analysis" is an umbrella that covers several distinct categories, each solved by different tools:

  • Linting — style enforcement, PEP 8 compliance, undefined names, unreachable code, and unused imports. Handled by tools like Ruff, Pylint, and Flake8.
  • Type checking — verifying that function arguments, return values, and variable assignments conform to declared type annotations (PEP 484). Handled by mypy, Pyright, and Pyre — separately from linters.
  • Security scanning (SAST) — detecting hardcoded credentials, weak cryptography, SQL injection risks, and OWASP Top 10 patterns. Handled by Bandit and Semgrep. For a broader look at security-focused tools, see our guide to SAST tools.
  • Formatting — deterministic code style that eliminates style debates. Previously Black; now largely subsumed by ruff format.

Because none of these tools execute your code, they are fast, safe to run on partially written files, and trivial to integrate into pre-commit hooks and CI/CD pipelines. The counterpart discipline — running code to observe runtime behaviour — is covered in our guide to dynamic analysis tools.

Why Python Static Analysis Matters in 2026

Python's flexibility has always been a double-edged sword. Dynamic typing, late binding, and duck typing make the language wonderfully expressive — but also let entire classes of bugs hide in plain sight until runtime. Python code analysis has matured significantly over the past three years, and three trends make 2026 the best time to invest in a proper static analysis stack:

  1. Speed is no longer an excuse. The old complaint — "linters are too slow to run on every save" — is obsolete. Ruff, written in Rust, scans a 50,000-line codebase in approximately 0.2 seconds, compared to roughly 20 seconds for Flake8 on the same corpus. That is a 100x improvement that makes local pre-commit checks practically instant.
  2. Type adoption is at an inflection point. According to the Python Developers Survey, a growing share of Python projects now include type annotations, and tools like Pyright provide near-instant feedback inside editors like VS Code. Teams that invest in type coverage today catch entire categories of bugs that linters cannot see — null pointer equivalents, wrong argument orders, missing return paths.
  3. Security shift-left is now expected. Shipping a Python service without running Bandit or Semgrep pre-commit is increasingly viewed as negligent. Both tools are fast enough to run locally, and both integrate cleanly into GitHub Actions, GitLab CI, and pre-commit hooks.

The cumulative effect: a modern Python project running Ruff, mypy, and Bandit catches style violations, type errors, and security issues in seconds — three problem categories that previously required three separate tool stacks. Our broader comparison of static code analysis tools across languages shows Python now has one of the most mature ecosystems outside of Java and C#.

The 8 Best Python Static Analysis Tools

Linter Speed on 50,000 Lines of Python (seconds, lower is better) Ruff Pylint Flake8 0 5 10 15 20 0.2s ✓ Blazing fast 8s 20s 20 seconds 8 seconds Seconds to analyse 50,000 lines of Python code Ruff is 100x faster than Flake8
Ruff (0.2 s) versus Pylint (8 s) versus Flake8 (20 s) on a 50,000-line Python codebase — Ruff is roughly 100x faster than Flake8.

1. Ruff — The Modern Default

Ruff is the defining tool of the 2026 Python linting landscape. Written in Rust by Astral (the team behind uv), it implements over 900 lint rules drawn from Flake8, isort, pydocstyle, pyupgrade, and more — while running 10 to 100 times faster than any Python-native linter. On a 50,000-line codebase, Ruff completes in approximately 0.2 seconds; Flake8 takes roughly 20 seconds on the same corpus.

Ruff is also a formatter (ruff format), making it a direct replacement for Black. For new Python projects, Ruff is the unambiguous starting point for python static analysis. The official Ruff documentation includes a complete rule reference and migration guides from Flake8, isort, and Black.

# ruff.toml
line-length = 88
target-version = "py312"

[lint]
select = ["E", "F", "I", "UP", "S"]
ignore = ["S101"]

[format]
quote-style = "double"

2. Pylint — Deep Semantic Analysis

Pylint is Python's most thorough semantic linter. Unlike Ruff's rule-based approach, Pylint builds a full call graph and performs control-flow analysis, catching subtle bugs that simpler tools miss — circular imports, incorrect method signatures, attribute access on possibly-None values. The trade-off is speed: Pylint takes roughly 8–12 seconds on a 50,000-line codebase, making it better suited to CI pipelines than developer save loops.

Pylint scores code from 0 to 10 and produces a diff against the previous score, which makes it useful for tracking quality trends across releases. Many teams run Ruff locally and add Pylint only in CI for comprehensive gate checks.

3. Flake8 — The Legacy Standard

Flake8 is a thin wrapper around Pyflakes (undefined names, unused imports), pycodestyle (PEP 8), and McCabe (cyclomatic complexity). It dominated Python linting for years and has a vast plugin ecosystem. However, Ruff now reimplements the vast majority of Flake8 rules with dramatically better performance. For existing projects with a large Flake8 plugin investment, migration is still worthwhile; for new projects, start with Ruff.

4. mypy — The Static Type Checker

mypy is the original and most widely used Python type checker. It reads PEP 484 annotations and reports type errors without running the code. mypy's strict mode (--strict) enforces full annotation coverage and disallows implicit Any — a meaningful bar for production services.

Critically, mypy is not a linter replacement. It runs separately, requires stub files for untyped third-party libraries, and is significantly slower than Ruff on large codebases. The correct mental model: Ruff catches style and simple bugs; mypy catches type-level bugs that linters cannot see.

# Install and run mypy in strict mode
pip install mypy
mypy --strict src/

5. Pyright — Microsoft's Type Checker

Pyright is Microsoft's TypeScript-inspired Python type checker, written in TypeScript and powering the Pylance VS Code extension. It is faster than mypy in most benchmarks and provides better incremental checking — meaning it re-checks only changed files. Pyright's strict mode is roughly equivalent to mypy's; the main reason to choose one over the other is editor integration (Pylance for VS Code users) and CI performance requirements. Both tools are complementary to python code review tools like Ruff.

6. Bandit — Security Vulnerability Scanner

Bandit is a purpose-built security scanner maintained by PyCQA (Python Code Quality Authority). It traverses the AST and applies 47 built-in security checks across 7 categories — hardcoded passwords, use of insecure hash functions (md5, sha1), SQL string concatenation, subprocess calls with shell=True, XML parsing vulnerabilities, and more. Bandit processes roughly 5,000 lines per second, making it fast enough for pre-commit use.

# Run Bandit on the src directory, show only medium+ severity
pip install bandit
bandit -r src/ -ll

7. Semgrep — Multi-Pattern SAST Engine

Semgrep is a multi-language static analysis framework with over 1,000 community-maintained rules for Python. Unlike Bandit's AST-only approach, Semgrep supports taint tracking (following user input through your codebase) and cross-file dataflow analysis, making it better at detecting complex injection patterns. Semgrep is slower than Bandit on large codebases, but its custom rule language lets security teams write precise, project-specific checks.

For a detailed comparison of SAST tools including Semgrep, see the SAST tools guide.

8. Pyre / Pysa — Facebook's Type and Security Layer

Pyre is Meta's incremental type checker for Python, designed for extremely large monorepos where mypy and Pyright struggle with cold-start times. Pysa (Python Static Analyzer) is built on top of Pyre and performs inter-procedural taint analysis — tracking data from sources (HTTP request parameters) to sinks (database queries, file writes) across module boundaries. Pysa is overkill for most teams but worth evaluating when Semgrep's taint rules reach their limits.

Types of Issues Caught by Python Static Analysis Style • PEP 8 formatting violations • Line length > 88 chars • Missing docstrings • Inconsistent quotes Tool: Ruff, Flake8 Bugs • Undefined variable references • Unreachable code paths • Unused imports & variables • Circular import chains Tool: Pylint, Ruff Security • SQL injection patterns • Hardcoded secrets / passwords • Weak crypto (md5, sha1) Tool: Bandit, Semgrep Types • Argument type mismatches • Missing return type paths • None dereference risks Tool: mypy, Pyright All four categories are distinct — each requires a dedicated tool class to catch reliably
The four distinct categories of issues caught by Python static analysis: style violations, bugs, security vulnerabilities, and type errors — each requiring dedicated tooling.

Comparison Table: Top Python Code Review Tools at a Glance

Tool Type Speed Best For Free?
Ruff Linter + Formatter ~0.2s / 50k LOC Daily dev loop, drop-in Flake8 replacement Yes (MIT)
Pylint Semantic Linter ~8–12s / 50k LOC Deep CI gate, quality scoring Yes (GPL-2.0)
Flake8 Linter (wrapper) ~20s / 50k LOC Legacy projects with plugin ecosystem Yes (MIT)
mypy Type Checker Moderate (incremental) Type safety enforcement, strict mode Yes (MIT)
Pyright Type Checker Fast (incremental) VS Code / Pylance users, large repos Yes (MIT)
Bandit Security Scanner ~5k LOC/sec Local security checks, pre-commit Yes (Apache-2.0)
Semgrep SAST Engine Slower (cross-file) Taint tracking, custom security rules Community: Yes

Linters vs. Type Checkers vs. Security Scanners

One of the most common sources of confusion in python static analysis is treating linters, type checkers, and security scanners as interchangeable. They are not — each category examines different properties of the code and produces different categories of findings.

Linters (Ruff, Flake8, Pylint) operate primarily at the syntactic and stylistic level. They enforce PEP 8 formatting, flag unused imports, detect undefined variables, and measure cyclomatic complexity. They are fast because they do not need to resolve types or trace data flow. A linter will catch import os with no usage, but it will not catch passing a str where an int is expected.

Type checkers (mypy, Pyright, Pyre) resolve your type annotations and verify that the program is internally consistent from a type perspective. They catch entire categories of bugs — None dereference, wrong argument count, incompatible return types — that linters are blind to. Type checkers require type annotations to work effectively; on an unannotated codebase, their value is limited.

Security scanners (Bandit, Semgrep) look for dangerous patterns: hardcoded secrets, use of deprecated cryptographic primitives, HTTP calls without TLS verification, subprocess invocations with shell=True. Security findings are distinct from linting or type errors — a perfectly typed, perfectly formatted function can still contain a SQL injection vulnerability. Security scanning is the only category that specifically addresses these concerns, which is why treating it as optional is increasingly risky.

The conclusion: all three categories are necessary for a production-grade python code analysis stack. They complement rather than replace each other, and modern workflows run all three.

Setting Up a Modern Python Static Analysis Workflow

Python Static Analysis: Pre-commit + CI Workflow Local (pre-commit) Ruff Lint + Format (~0.2s) Runs on: git commit Target: <5 second total Fast feedback loop push PR (GitHub Actions) Ruff check + format mypy --strict Bandit -r src/ -ll Full gate — all must pass merge Merge to main All checks passed Safe to merge Local layer: fast iteration · CI layer: authoritative gate · Merge: green-checkmark confirmed
Two-layer Python static analysis workflow: fast local pre-commit hooks (Ruff only) and a comprehensive CI gate (Ruff + mypy + Bandit) that must pass before merge.

A well-structured 2026 static analysis Python workflow has two layers: a fast local layer (pre-commit hooks) and a comprehensive CI layer (GitHub Actions or equivalent). The local layer runs on every commit and must finish in under five seconds. The CI layer runs deeper checks that are too slow for local use.

Step 1: Install the Tools

pip install ruff mypy bandit pre-commit

Step 2: Configure pre-commit Hooks

The .pre-commit-config.yaml below configures Ruff, mypy, and Bandit to run automatically on every git commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        args: [--strict]
        additional_dependencies: [types-requests]

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.9
    hooks:
      - id: bandit
        args: [-ll, -r]
        files: ^src/

Step 3: Add a GitHub Actions CI Pipeline

Pre-commit hooks can be bypassed with --no-verify. A GitHub Actions workflow provides an authoritative second gate:

# .github/workflows/lint.yml
name: Python Static Analysis

on: [push, pull_request]

jobs:
  analyse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install ruff mypy bandit

      - name: Ruff lint
        run: ruff check src/

      - name: Ruff format check
        run: ruff format --check src/

      - name: mypy type check
        run: mypy --strict src/

      - name: Bandit security scan
        run: bandit -r src/ -ll

Optional: Add Pylint in CI for Deep Semantic Checks

Pylint's comprehensive semantic analysis is too slow for local use but valuable as a CI-only gate. Add it as a separate, non-blocking job initially to gather baseline scores before enforcing a minimum threshold. This hybrid approach — Ruff locally, Pylint in CI — gives the best of both worlds without slowing developer loops.

For developers comparing code changes across editors, the VS Code compare files guide covers the built-in diff tooling that pairs well with these static analysis workflows.

Why Visual Diff Review Belongs Before Static Analysis

Visual Diff Review: Before vs. After main.py (before) 1 import hashlib 2 import os 3 4 password = "s3cr3t123" 5 6 def hash_pw(pw): 7 return hashlib.md5(pw).hexdigest() 8 9 def connect(host, query): 10 sql = "SELECT * WHERE id=" + query Bandit: 3 issues detected B105 hardcoded_password · B324 md5 · B608 SQL main.py (after) 1 import hashlib 2 import os 3 4 + password = os.environ["DB_PASS"] 5 6 def hash_pw(pw: bytes) -> str: 7 + return hashlib.sha256(pw).hexdigest() 8 9 def connect(host, query: str): 10 + sql = "SELECT * WHERE id=?" Bandit: 0 issues Env var · SHA-256 · Parameterized query DIFF view Red lines removed · Green lines added · Review diff before running Bandit to separate new issues from pre-existing debt
Split-pane diff review in Diff Checker: three deleted lines (hardcoded password, md5, SQL concatenation) replaced by secure equivalents — each Bandit finding maps directly to a visible changed line.

Every Python team has experienced this: a pull request triggers 47 static analysis warnings. Some are genuine new issues introduced by the change; others are pre-existing debt the tool noticed while scanning files the developer happened to touch. Without knowing exactly what changed, it is nearly impossible to separate signal from noise.

Visual diff review — comparing the old and new version of a file side by side before running python static analysis tools — solves this problem at the source. When a reviewer can see precisely which lines changed, they can immediately map each static analyzer warning to a specific new line or confirm it is pre-existing debt. This keeps code review focused, reduces back-and-forth, and prevents genuine issues from being dismissed as "linter noise."

Diff Checker is a free browser extension that brings Monaco-based diff review — the same editor that powers VS Code — directly to your browser without any signup or data upload. It supports split view and unified view modes, handles DOCX, XLSX, and PPTX files alongside code, auto-detects programming languages, and keeps up to 50 recent comparisons in IndexedDB locally. For JSON configuration files (a common source of subtle Python environment bugs), it can also sort keys and normalize whitespace before diffing — which is particularly useful when comparing two JSON configuration objects.

The recommended workflow is:

  1. Open Diff Checker and paste the before/after versions of the file (or drag-and-drop).
  2. Review the highlighted changes to understand the logical intent of the diff.
  3. Run Ruff, mypy, and Bandit.
  4. Map each warning back to the changed lines you already reviewed.
  5. Dismiss pre-existing issues; fix the issues introduced by the current change.

This diff-first approach is especially effective for security reviews. When a developer can see exactly which new lines were added, Bandit's findings become immediately actionable — there is no need to audit the entire file to understand which patterns are new. If the AI summary feature is available (requires an OpenAI API key), it can also generate a plain-language description of the diff, which helps non-Python reviewers participate in security sign-offs without becoming experts in AST analysis.

Frequently Asked Questions

What is the difference between Pylint, Flake8, and Ruff?

All three are Python static analysis tools focused on linting, but they differ significantly in approach and speed. Flake8 is a thin wrapper around three older tools (Pyflakes, pycodestyle, McCabe) with a plugin ecosystem — it held the market for years but is slow. Pylint is a deep semantic analyzer that builds a full call graph; it catches subtle logic bugs but takes 8–12 seconds on a typical codebase. Ruff reimplements over 900 rules from both tools in Rust, running in 0.2 seconds, and also replaces Black as a formatter. For new projects in 2026, Ruff is the default choice; Pylint is worth adding to CI pipelines for comprehensive gate checks.

Do I need mypy if I already use Ruff?

Yes. Ruff and mypy operate on fundamentally different properties of the code. Ruff catches style violations, unused imports, and simple bug patterns without understanding types. mypy (or Pyright) verifies that function arguments, return values, and variable assignments are type-consistent across the entire codebase. A codebase can pass all Ruff checks and still contain dozens of type errors that will crash at runtime. The two tools are complementary, not redundant.

Should I use Bandit or Semgrep for Python security scanning?

Both — starting with Bandit. Bandit is fast (5,000 LOC/sec), has 47 focused checks covering the most common Python security mistakes, and requires no configuration to provide immediate value. Semgrep adds taint tracking, custom rule support, and cross-file dataflow analysis, but is slower and requires more setup. The recommended approach: run Bandit pre-commit for speed, add Semgrep in CI for depth. For teams managing database configurations, see also our guide to comparing JSON objects online when auditing configuration drift.

How do I integrate Python static analysis into GitHub Actions?

Create a .github/workflows/lint.yml file that triggers on push and pull_request, sets up Python 3.12, installs ruff, mypy, and bandit, then runs ruff check, ruff format --check, mypy --strict, and bandit -r src/ -ll as separate steps. Each step acts as a gate so the workflow fails on any violation. The complete YAML configuration is shown in the workflow setup section above — copy it directly into your repository to enable comprehensive static analysis Python checks on every pull request.

Are Python static analysis tools free?

Yes. All major python static analysis tools — Ruff (MIT), Pylint (GPL-2.0), Flake8 (MIT), mypy (MIT), Pyright (MIT), and Bandit (Apache-2.0) — are free and open-source. Semgrep offers a free Community edition alongside paid tiers with extra rules and dashboards. For most teams, the free tier covers every linting, type-checking, and security-scanning need without any license cost, making robust Python static code analysis accessible to projects of any size.