If you're adding static analysis to a Python project, you've probably narrowed it down to three tools: Bandit for security, Vulture for dead code, or something that does both. Here's how they actually compare.

What Each Tool Does

Before comparing, it's important to understand that these tools solve different problems:

BanditVultureSkylos
Primary focusSecurity issue candidatesDead code detectionSecurity and dead-code candidates + quality metrics
Detection methodAST pattern matchingAST name resolutionAST + local taint propagation + framework-aware liveness
LanguagesPython onlyPython onlyPython (deepest), TypeScript/JavaScript and Java: dead code, security and quality. PHP, Rust, Dart: dead code and security. Go: dead code and security need the separate skylos-go engine (in the Docker image and GitHub Action, not the PyPI package). C# partial. Kotlin: dead code only. C++: unused file-local functions only. Shell: security only.
CI/CD integrationOfficial Action, pre-commit, SARIFDocumented Action and pre-commitGenerated workflow (skylos cicd init)
PR feedbackAction output or SARIF in Code ScanningAction outputGitHub annotations and inline review comments
Framework handlingFramework-specific security pluginsConfigurable decorator ignores and whitelistsBuilt-in liveness models for Django, FastAPI, Flask, Pydantic, pytest, and others

The short version: Bandit reports Python security issue candidates. Vulture reports dead-code candidates. Skylos covers both categories and adds code-quality metrics, secrets detection, and CI/CD integration.


Detection Coverage

Security Scanning

Bandit covers common Python security issue types:

  • Hardcoded passwords, SQL injection (pattern-based), shell injection
  • Weak crypto (MD5, SHA1), unsafe YAML, pickle deserialization
  • No taint analysis — can't trace data flow from user input to sink

Skylos covers common Python security issue types and includes dataflow-oriented checks:

  • Taint analysis: Propagates taint through local assignments and expressions and conservatively treats ordinary function parameters as untrusted when checking most dangerous sinks (outbound HTTP URLs are the exception)
  • SSRF detection (SKY-D216): Flags potentially tainted URLs passed to HTTP clients
  • ORM injection (SKY-D217): Catches sqlalchemy.text(), pandas.read_sql(), Django .raw() with interpolated strings
  • XSS patterns (SKY-D226-D228): mark_safe(), template |safe filter, HTML string building
  • JWT vulnerabilities (SKY-D232): algorithms=['none'], verify=False
  • MCP server security (SKY-D240-D244): Tool description poisoning, unauthenticated transport
  • AI-defect checks: Evidence-backed phantom calls, dependency hallucinations, test gaps, and change risks under --ai-defects
  • Other risky patterns: Hardcoded credentials, disabled security controls, and insecure randomness

Vulture does not scan for security issues.

Dead Code Detection

Vulture is the established tool here. It finds:

  • Unused functions, classes, imports, variables
  • Unreachable code after return/break/continue
  • Confidence scoring (60-100%)
  • Whitelist support

Skylos covers the same ground with additions:

  • Framework-aware dead code (won't flag @app.route, pytest.fixture, @admin.register, Pydantic model_validator)
  • Transitive dead code propagation (if A calls B and A is dead, B might be too)
  • Cleanup: preview removals with skylos clean . --dry-run and apply them with --apply (Python imports and functions), or review interactively with skylos . -i

Bandit does not detect dead code.

Code Quality

Neither provides the broader complexity and architecture metrics below.

Skylos includes:

  • Cyclomatic complexity (SKY-Q301)
  • Deep nesting (SKY-Q302)
  • Argument count / function length (SKY-C303, SKY-C304)
  • God class detection (SKY-Q501)
  • Coupling and cohesion metrics (SKY-Q701, Q702)
  • Async blocking calls (SKY-Q401)
  • Advisory architecture metrics (distance from main sequence, SKY-Q802/Q803; not part of the gate by default)

How the findings differ

This is where the differences matter most in practice.

Bandit reports shell use and adjusts severity

Bandit's B602 plugin reports shell=True and adjusts severity based on whether the command appears static or computed:

# Bandit B602: LOW severity for a simple static command
subprocess.call("ls -la", shell=True)  # B602: subprocess_popen_with_shell_equals_true

# Bandit B602: HIGH severity for a computed command
cmd = request.args.get("cmd")
subprocess.call(cmd, shell=True)

That is useful broad coverage. Bandit does not trace the request value to the subprocess sink as a separate source-to-sink command-injection result.

Vulture provides configuration for indirect uses

Framework callbacks are a common challenge for dead-code analysis:

@app.route("/api/users")  # Flask route — called by HTTP, not Python
def get_users():
    return jsonify(users)

@pytest.fixture  # Called by pytest, not your code
def db_connection():
    return create_connection()

Vulture documents --ignore-decorators "@app.route" for Flask and also supports ignore lists and generated whitelists. Projects still need to configure indirect-use patterns that their framework or plugin system introduces.

Skylos's Approach

Skylos uses framework-aware visitors that understand Django, Flask, FastAPI, Pydantic, and pytest patterns. Its Python security rules also propagate taint through local assignments and expressions. For most sinks other than outbound HTTP requests, they conservatively treat ordinary function parameters as untrusted, so review the evidence rather than assuming every result proves an end-to-end request flow:

# Skylos reports shell use for both forms (SKY-D209)
subprocess.call("ls -la", shell=True)  # SKY-D209

# A directly recognized request value adds a command-injection result
cmd = request.args.get("cmd")
subprocess.call(cmd, shell=True)  # SKY-D209 + SKY-D212

This direct example was reproduced with Skylos 4.47.1: the static command produced SKY-D209, while the request value assigned in the same function produced both SKY-D209 and CRITICAL SKY-D212. This is not a claim of interprocedural call-chain tracing.


Speed

All three tools are usually fast enough for local and CI use on small-to-medium projects. Exact timing depends on file count, rule configuration, import graph shape, framework usage, hardware, and which analyzers are enabled.

The practical rule: run the tool on your repository and measure it in your CI environment. If the scan is meant to block PRs, benchmark the exact command you plan to enforce rather than relying on generic timing tables.


CI/CD Integration

Bandit

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
  with:
    python-version: "3.12"
- run: python -m pip install bandit==1.9.4
- run: bandit -r src/

This pins the current scanner and preserves Bandit's normal nonzero finding exit. Bandit also supports severity and confidence thresholds, baselines, and SARIF. Its official Action uploads SARIF, but its current v1 runtime installs an older Python-3.8-compatible Bandit and its wrapper does not fail the step on Bandit's finding exit code; account for that if you use the Action instead.

Vulture

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: gtkacz/vulture-action@1.0.0
  with:
    src: "src/"
    args: "--min-confidence 80"

Vulture also documents pre-commit integration. Its nonzero finding exit can fail the Action step; it does not generate Skylos's separate configurable gate and PR-review workflow.

Skylos

# Generates a pull-request gate; --no-upload skips the Skylos Cloud job
skylos cicd init --no-upload

It generates a GitHub Actions workflow with:

  • Security + quality scanning on changed lines
  • GitHub annotations and PR review comments
  • Quality gate (configurable thresholds)
  • A "Skylos Done" job that runs your tests, when a pytest project is found

skylos cicd init writes a GitHub Actions pull-request gate that needs no Skylos account or API key. By default it also adds a "Skylos Cloud upload" job for pushes to your default branch, which fails until the Skylos GitHub App is installed on the repository. Without Cloud, run skylos cicd init --no-upload.


When to Use What

Use Bandit if:

  • You only need security scanning
  • You want a configurable Python security plugin system
  • You already have a dead code tool
  • You want severity/confidence thresholds, baselines, or SARIF output

Use Vulture if:

  • You only need dead code detection
  • You value confidence scoring
  • You're comfortable configuring indirect framework uses through decorator ignores, name ignores, or whitelists

Use Skylos if:

  • You want security + dead code + quality in one tool
  • You use Django, Flask, FastAPI, or Pydantic (framework awareness matters)
  • You want CI/CD integration with PR comments out of the box
  • You want local taint evidence alongside pattern findings
  • You want to detect AI-generated code problems

Use Bandit + Vulture if:

  • You want established tools with large communities
  • You're comfortable managing two tools and their configurations
  • You don't need quality metrics or PR comments

Quick Start Comparison

Bandit

pip install bandit
bandit -r src/

Vulture

pip install vulture
vulture src/

Skylos

pip install skylos
skylos . --danger --quality --format rich

Final Thoughts

There's no single "best" tool. Bandit focuses on Python security plugins. Vulture focuses on dead-code candidates and confidence. Skylos combines security, dead code, quality, and generated CI workflows; teams should compare the actual findings on their own repository.

Pick the scope you need, run it on a representative repository, and measure which findings your team would actually act on before making the job blocking.

Facts were rechecked on October 9, 2026 against the Bandit documentation, Bandit B602 behavior, Bandit's GitHub Action, Vulture documentation, and Skylos 4.47.1.



All tools mentioned are open source. Bandit | Vulture | Skylos | Skylos Docs | Install