Skip to main content

Python Static Code Analysis in 2026: Ruff, Bandit, Pylint and Mypy for Safer Code

Updated in 2026: this article was originally written in 2020 and focused mainly on Prospector and Bandit. The updated version keeps that original idea, but adds a more modern Python static analysis workflow using Ruff, Bandit, Mypy and Pylint.

Python scripts are dangerous when they become invisible.

Not dangerous in a movie-hacker way. Dangerous in the boring real-world way.

A small script starts as a quick helper. Then it ends up running from cron. Then it touches backups, logs, Docker containers, firewall data, API keys, file permissions, or some random directory on a Linux server. Months later, nobody remembers exactly what it does, but everyone hopes it keeps working.

That is where Python static code analysis is useful.

Static analysis checks code before you run it. It can catch common bugs, insecure patterns, style problems, type mistakes, unused imports, dangerous function calls and other issues that are easy to miss when a script is small and “just works”.

In the original version of this post, I used two tools:

  • Prospector for general Python code analysis;
  • Bandit for Python security checks.

Today, I would still keep Bandit, but I would usually start with a more modern stack:

  • Ruff for fast linting and formatting;
  • Bandit for security-focused checks;
  • Mypy for static type checking;
  • Pylint when I want deeper and stricter analysis;
  • Prospector as an optional wrapper or legacy choice.

This is especially useful if you write Python for Linux administration, backup scripts, log parsing, homelab automation, honeypot experiments or small security tools.

Related posts:

Quick answer: which Python static analysis tools should you use?

If I were starting a small Python project today, I would use this setup:

ruff check .
ruff format --check .
bandit -r .
mypy .


For most small scripts and homelab automation projects, that gives a good balance:

  • Ruff catches many common linting and style problems very quickly;
  • Bandit looks for common Python security issues;
  • Mypy catches type-related problems before runtime;
  • Pylint can be added when you want stricter feedback.
For most small Python scripts today, my default setup is Ruff + Bandit + Mypy. Add Pylint when you want stricter analysis, and keep Prospector if you already use it.

Here is the simple comparison:

Tool Best for Use it when
Ruff Fast linting and formatting You want quick feedback and automatic fixes
Bandit Security checks You want to catch risky Python patterns
Mypy Static type checking You use type hints or want fewer runtime surprises
Pylint Deeper static analysis You want stricter warnings and refactoring hints
Prospector Combined analysis wrapper You already use it or want one wrapper around several tools

Why static code analysis matters

Python is easy to write, which is one of the reasons I like it.

But that also means it is easy to create scripts that are:

  • messy;
  • fragile;
  • hard to review;
  • full of unused imports;
  • too permissive with exceptions;
  • unsafe with subprocess calls;
  • careless with secrets;
  • broken only in paths you forgot to test.

Static analysis does not make code perfect. It does not replace testing. It does not understand your full intent.

But it does catch many boring problems early.

Static analysis is not magic. It is a cheap way to make mistakes more visible.

For scripts running on a Linux home server, that matters. A bad script can delete files, expose credentials, break backups, fill disks, restart containers, or silently fail for months.

That is why I like combining code-level checks with system-level hardening. Tools like Lynis help audit the Linux system. Python static analysis helps audit the scripts you run on that system.

What changed since the original 2020 post?

The original version of this post used:

  • Prospector as the general static analysis wrapper;
  • Bandit as the Python security scanner.

That still makes sense as a historical setup, especially if you already have Prospector configured. But for new projects, I usually prefer a clearer split:

  • Ruff for linting and formatting;
  • Bandit for security checks;
  • Mypy for types;
  • Pylint only when I want stricter analysis.

Why this change?

Because modern Python tooling has become faster and more focused. Ruff is extremely fast and covers many rules that previously required multiple tools. Bandit still has a clear security role. Mypy adds a layer that linting does not cover. Pylint remains useful, but I would not always start there because it can be noisy for small scripts.

Modern quick start

Start with a virtual environment:

python3 -m venv .venv
source .venv/bin/activate

python -m pip install --upgrade pip
python -m pip install ruff bandit mypy pylint

Then run the tools:

ruff check .
ruff format --check .
bandit -r .
mypy .
pylint your_package_or_script.py

For a single script:

ruff check script.py
bandit script.py
mypy script.py
pylint script.py

If you want Ruff to fix safe issues automatically:

ruff check . --fix
ruff format .

My usual starting point is:

ruff check .
bandit -r .

Then I add Mypy and Pylint when the project deserves it.

Example bad Python script

Let’s use a tiny example with several issues.

import os
import subprocess
import requests

password = "admin123"

def backup(path):
    cmd = "tar -czf backup.tar.gz " + path
    subprocess.call(cmd, shell=True)

def get_status(url):
    response = requests.get(url, verify=False)
    return response.text

backup("/srv/docker")
print(get_status("https://example.com"))

This script has several problems:

  • hardcoded password;
  • unsafe shell usage;
  • string concatenation inside a shell command;
  • TLS certificate verification disabled;
  • unused import;
  • no timeout on the HTTP request;
  • no error handling;
  • no type hints.

It is a small script, but it already shows why static analysis is useful.

Ruff: fast linting and formatting

Ruff is usually my first tool now. It is fast, practical and works well for small scripts and larger projects.

Run it:

ruff check .

Run it on one file:

ruff check script.py

Ask Ruff to fix what it safely can:

ruff check . --fix

Check formatting:

ruff format --check .

Format the code:

ruff format .

Ruff is good for catching things like:

  • unused imports;
  • undefined names;
  • style issues;
  • common bug patterns;
  • import sorting;
  • some modernization suggestions;
  • many issues that previously required several tools.

For small Linux administration scripts, Ruff is a very low-friction improvement.

Bandit: Python security checks

Bandit is still one of the most useful Python security analysis tools. It scans Python code for common security issues.

Install it:

python -m pip install bandit

Run it recursively:

bandit -r .

Run it on one script:

bandit script.py

Generate a report:

bandit -r . -f txt -o bandit-report.txt

Bandit can flag risky patterns such as:

  • hardcoded passwords;
  • insecure subprocess usage;
  • unsafe temporary files;
  • weak cryptography choices;
  • use of eval;
  • disabled TLS verification;
  • dangerous YAML loading patterns.

Bandit is especially useful if you write Python scripts for automation, backups, log parsing or small security experiments.


Bandit python check output

For example, if you are writing scripts around Docker backups, combine code checks with an actual backup process:

Related: Backing Up Docker Containers on a Home Server

Mypy: static type checking

Mypy checks Python type hints before runtime.

Install it:

python -m pip install mypy

Run it:

mypy .

Or on one file:

mypy script.py

Example typed function:

from pathlib import Path

def backup_path(path: Path) -> Path:
    return path.resolve()

Mypy becomes more useful when your scripts grow beyond “quick one-off thing”.

It helps catch problems like:

  • passing strings where paths are expected;
  • returning the wrong type;
  • forgetting that a value may be None;
  • mixing lists, dictionaries and optional values incorrectly;
  • breaking function contracts during refactoring.

You do not need to type everything on day one. You can add type hints gradually.

Pylint: stricter and deeper analysis

Pylint is older, stricter and often more verbose. That can be good or annoying depending on the project.

Install it:

python -m pip install pylint

Run it:

pylint script.py

Or on a package:

pylint your_package/

Pylint can help with:

  • code smells;
  • unused variables;
  • bad naming;
  • too many branches;
  • duplicate code patterns;
  • possible bugs;
  • refactoring hints;
  • style consistency.

For small homelab scripts, I do not always run Pylint first. It can produce a lot of noise. But when a Python script becomes important, Pylint is worth trying.

Prospector: where the original tool still fits

The first version of this post used Prospector.

Prospector is a wrapper around multiple Python analysis tools. It can be useful if you want one command to run several checks together.

Install it:

python -m pip install prospector

Run it:

prospector

Or on a specific path:

prospector my_project/

I would still consider Prospector if:

  • an existing project already uses it;
  • you want a single wrapper command;
  • you like its combined reporting;
  • you are maintaining an older workflow.

But for a new small project today, I prefer starting with the clearer tool split:

  • Ruff for linting and formatting;
  • Bandit for security;
  • Mypy for types;
  • Pylint for deeper analysis.


Suggested pyproject.toml

For a small project or script folder, I would start with a simple pyproject.toml.

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "B", "I", "UP", "SIM"]
ignore = []

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true

[tool.bandit]
exclude_dirs = ["tests", ".venv"]

This is not a perfect universal config. It is a sane starting point.

Adjust it depending on the project, Python version and how strict you want to be.

Simple pre-commit workflow

If you want these checks to run before every commit, use pre-commit.

Install it:

python -m pip install pre-commit

Create .pre-commit-config.yaml:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff
        args: ["--fix"]
      - id: ruff-format

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.10
    hooks:
      - id: bandit
        args: ["-r", "."]

Install the hooks:

pre-commit install

Run manually:

pre-commit run --all-files

For a serious project, this is better than relying on memory.

Example GitHub Actions workflow

If the code is hosted on GitHub, you can run checks automatically on push.

Create:

.github/workflows/python-static-analysis.yml

Example workflow:

name: Python static analysis

on:
  push:
  pull_request:

jobs:
  static-analysis:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - name: Install tools
        run: |
          python -m pip install --upgrade pip
          python -m pip install ruff bandit mypy

      - name: Ruff check
        run: ruff check .

      - name: Ruff format check
        run: ruff format --check .

      - name: Bandit security scan
        run: bandit -r .

      - name: Mypy type check
        run: mypy .

This makes the workflow repeatable and prevents “I forgot to run the checks” problems.

How this connects to Linux and homelab security

This blog has a lot of Linux home server and homelab security content now, so it is worth connecting Python static analysis to that world.

Many home servers have small scripts for:

  • checking disk space;
  • parsing logs;
  • backing up files;
  • rotating old archives;
  • querying APIs;
  • checking Docker containers;
  • sending notifications;
  • processing honeypot logs;
  • running from cron or systemd timers.

Those scripts may be small, but they often touch important things.

A broken backup script is a security problem.

A script that logs secrets is a security problem.

A script that uses unsafe shell commands with user-controlled input is a security problem.

A script that silently fails for six months is also a security problem.

That is why static analysis belongs next to the rest of the home server checklist:

What I would use today

For a quick script:

ruff check script.py
bandit script.py

For a small project:

ruff check .
ruff format --check .
bandit -r .
mypy .

For a stricter project:

ruff check .
ruff format --check .
bandit -r .
mypy .
pylint your_package/

For an older setup already using Prospector:

prospector
bandit -r .

That last one is close to the original version of this article.

Common mistakes

Only using one tool

One tool rarely covers everything. Ruff, Bandit and Mypy each solve different problems.

Ignoring Bandit warnings without thinking

Some warnings may be false positives, but they still deserve review. Do not blindly silence security warnings.

Running checks once and forgetting them

Static analysis is most useful when it becomes part of your normal workflow.

Making the setup too strict too soon

If the tool output is overwhelming, people stop using it. Start with a useful baseline and increase strictness later.

Assuming static analysis replaces testing

It does not. You still need tests, logs, reviews and common sense.

Good workflow for small scripts

For a folder full of Linux administration scripts, I like this simple routine:

cd ~/scripts

ruff check .
bandit -r .
mypy .

Before copying a script to a server:

ruff check backup_script.py
bandit backup_script.py
python -m py_compile backup_script.py

The last command checks that the file at least compiles:

python -m py_compile script.py

It is simple, but useful.

Example improved script

Here is a safer version of the earlier example.

from pathlib import Path
import subprocess

import requests


def create_backup(source: Path, output: Path) -> None:
    source = source.resolve()
    output = output.resolve()

    subprocess.run(
        ["tar", "-czf", str(output), str(source)],
        check=True,
    )


def get_status(url: str) -> str:
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.text


if __name__ == "__main__":
    create_backup(Path("/srv/docker"), Path("backup.tar.gz"))
    print(get_status("https://example.com"))

This is not perfect, but it is better:

  • no hardcoded password;
  • no shell=True;
  • uses a list for subprocess arguments;
  • uses check=True;
  • adds timeout to the HTTP request;
  • uses type hints;
  • uses Path for paths.

Static analysis tools will not make every design decision for you, but they push you toward better habits.

Security notes for Python scripts

When writing scripts for servers, I try to avoid:

  • shell=True unless there is a very good reason;
  • hardcoded passwords or tokens;
  • printing secrets to logs;
  • disabling TLS verification;
  • unsafe file permissions;
  • unvalidated paths;
  • broad exception handling like except Exception: pass;
  • running scripts as root when not needed.

Bandit helps catch some of these. Ruff and Pylint catch others. Mypy catches a different class of problems.

No single tool is enough.

Final recommended setup

For my own small Python scripts today, I would start with:

python -m pip install ruff bandit mypy

ruff check .
ruff format --check .
bandit -r .
mypy .

Then, if the project becomes bigger or more important:

python -m pip install pylint
pylint your_package/

And if I want checks to happen automatically:

python -m pip install pre-commit
pre-commit install
pre-commit run --all-files

That is enough for most small projects and homelab scripts.

Final thoughts

The original version of this post used Prospector and Bandit, and that was a reasonable starting point at the time.

Today, I would still keep the same idea but update the tools:

  • Ruff for fast linting and formatting;
  • Bandit for security checks;
  • Mypy for type checking;
  • Pylint for stricter analysis when needed;
  • Prospector if you want a combined wrapper or already use it.

Python static code analysis is not only for big projects.

It is useful for the small scripts that quietly become important: backup helpers, log parsers, Docker maintenance scripts, monitoring checks and security experiments.

Run the checks before the script becomes invisible infrastructure.

Future you will appreciate it.


Recommended next reading:


Written by MsR

MsR is a Linux homelab and cybersecurity enthusiast who documents practical experiments with home servers, Docker, firewalls, backups, Lynis, Fail2ban, honeypots and old hardware. The guides on IT Random Stuff are based on hands-on testing, real configurations and lessons learned from running Linux systems at home.

 


Comments

Popular posts from this blog

Honeypot deployment on Linux - OpenCanary

What’s a honeypot what what its purpose ? It’s basically a computer or Virtual Machine emulating some services (ex: ssh, ftp, telnet, netbios, https, samba server etc) and accepting, logging and sending warnings of all incoming connections. You can use it as intrusion detection or early warning system but it also might go a little further and allow one to get inside the intruders ”head” since you get to log every interaction. How and where should it be placed? Let’s start with “where”. I usually place them in specific areas to get an idea how/or if the network is tested from outside or inside. So I have about three major areas; behind firewalls, in “sensible zones” where only pre-defined machines should have access and in the “public zone” such as administrative/general network. Placing a honeypot behind firewalls/”sensible zones” will ensure that the firewall is doing its and if you get a hit that means you have a missconfigurations or a serious intrusion. Honeypots placed...

Lenovo ThinkPad X250 on Linux: Tweaks, Undervolting, Battery Life and 2026 Update

I wanted a cheap, small, serviceable Linux laptop. Something light enough to carry, easy enough to repair, and inexpensive enough that upgrades would still make sense. The Lenovo ThinkPad X250 was a good candidate because it has a 12.5-inch form factor, a proper ThinkPad keyboard, SSD upgrade options, replaceable parts, Ethernet, docking support and generally good Linux compatibility. I found one on eBay for around 130€ : an Intel Core i5-5300U model with 8GB RAM , a 128GB SSD , two batteries and an HD screen with a small bruise. The plan was simple: clean it, repaste it, upgrade the SSD, install Linux Mint, undervolt it and see how useful it could still be. This post started as my original 2019 notes about tweaking the Lenovo X250 in Linux. I have now updated it with a 2026 perspective, cleaner instructions, better internal links and a more realistic look at whether this old ThinkPad is still worth using. Related posts: Linux Home Server Security Checklist Docker Secu...

Strong, Unique Passwords Without Losing Your Mind

Last updated: May 27, 2026 Password Security in 2026: Password Managers, Passkeys & 2FA for Real People Password Security in 2026: Password Managers, Passkeys & 2FA That Actually Work Most people do not have a weak-password problem. They have a reused-password problem. You can invent the cleverest password in the world, but if you use it on twenty websites and one of them gets breached, you suddenly have twenty compromised accounts. That is how most real-world account takeovers happen in 2026. Not elite hackers brute-forcing your login from a dark room somewhere. Just automated credential stuffing using databases leaked years ago from services you forgot existed. One old forum breach becomes access to your email, cloud storage, streaming services, VPN account, and eventually your homelab dashboard because the same password got reused everywhere. This guide explains how to handle passwords properly today: without paranoia, without enterprise co...