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:
- Linux Home Server Security Checklist
- Lynis Hardening Checklist
- Docker Security for Homelab Beginners
- Backing Up Docker Containers on a Home Server
- OpenCanary Honeypot Deployment on Linux
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.
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:
- harden the Linux server;
- restrict network access with UFW;
- protect SSH with Fail2ban;
- audit the system with Lynis;
- avoid exposing random Docker containers;
- check your Python scripts before they become invisible infrastructure.
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
Pathfor 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=Trueunless 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:
- Linux Home Server Security Checklist
- Lynis Hardening Checklist
- Docker Security for Homelab Beginners
- Backing Up Docker Containers on a Home Server
- OpenCanary Honeypot Deployment on Linux
Comments
Post a Comment