Engineering safety lint rules and pre-commit integration for modern Python codebases
Project description
SafeLint
SafeLint is a configurable static analysis tool that enforces safety-critical coding practices inspired by Gerard J. Holzmann's "Power of Ten" rules at NASA/JPL.
Originally designed for mission-critical systems, these principles apply to any modern Python codebase - and are especially valuable when code is written fast, reviewed quickly, or generated by AI.
SafeLint integrates with pre-commit and CI pipelines to prevent unsafe code from entering your codebase.
Why SafeLint?
Fast-moving codebases - whether written by humans under pressure or generated by AI tools - tend to drift toward the same failure patterns:
- Unbounded loops
- Silent error handling
- Hidden side effects
- Poor resource management
SafeLint catches these early, automatically, regardless of who wrote the code.
Philosophy
"When it really counts, it may be worth going the extra mile and living within stricter limits than may be desirable."
- Gerard J. Holzmann, NASA/JPL
Power of Ten - adapted for Python
In 1987, Holzmann wrote ten rules for spacecraft software at NASA/JPL. Nearly four decades later, the same failure patterns appear in every Python codebase. SafeLint is those ten rules, adapted for Python and automated.
| # | Holzmann's Rule | SafeLint Rule | Code |
|---|---|---|---|
| 1 | No complex control flow - no goto, no deep recursion |
nesting_depth, complexity |
SAFE102, SAFE104 |
| 2 | All loops must have a fixed upper bound | unbounded_loops |
SAFE501 |
| 3 | No dynamic memory allocation after startup | - | (not applicable to Python) |
| 4 | Functions must fit on one printed page | function_length |
SAFE101 |
| 5 | Use at least two assertions per function | missing_assertions |
SAFE601 |
| 6 | Declare variables at the smallest scope | - | (Python handles this) |
| 7 | Check the return value of every non-void function | return_value_ignored, bare_except, empty_except |
SAFE802, SAFE201, SAFE202 |
| 8 | Limit preprocessor use | - | (not applicable to Python) |
| 9 | Restrict pointer use - no chained indirection | null_dereference |
SAFE803 |
| 10 | Compile with all warnings; use static analysis | SafeLint itself | - |
Original paper: spinroot.com/gerard/pdf/P10.pdf
Installation
pip install safelint
To also support YAML config files (.safelint.yaml):
pip install "safelint[yaml]"
Usage
Check modified files (default — only files changed since last commit):
safelint check src/
Check all files (full scan, e.g. in CI):
safelint check src/ --all-files
Check specific files (pre-commit style):
safelint src/mymodule.py src/utils.py
Fail on warnings too (useful in CI):
safelint check src/ --all-files --fail-on=warning
Run in CI mode (warnings become blocking):
safelint check src/ --all-files --mode=ci
Ignore specific rules for one run:
safelint check src/ --ignore SAFE203 --ignore side_effects
Pre-commit integration
Add this to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/shelkesays/safelint
rev: v1.0.0 # replace with the latest release tag
hooks:
- id: safelint
args: [--fail-on=error] # use --fail-on=warning for stricter CI
files: ^src/
Then install the hooks:
pre-commit install
SafeLint will now run on every git commit and block the commit if it finds errors.
What it checks
| Code | Rule | What it flags |
|---|---|---|
| SAFE101 | function_length |
Functions longer than 60 lines |
| SAFE102 | nesting_depth |
Control flow nested more than 2 levels deep |
| SAFE103 | max_arguments |
Functions with more than 7 parameters |
| SAFE104 | complexity |
Functions with high cyclomatic complexity |
| SAFE201 | bare_except |
except: with no exception type |
| SAFE202 | empty_except |
except blocks that do nothing (pass) |
| SAFE203 | logging_on_error |
Except blocks that swallow errors silently |
| SAFE301 | global_state |
Use of the global keyword inside functions |
| SAFE302 | global_mutation |
Writing to global variables inside functions |
| SAFE303 | side_effects_hidden |
Pure-looking functions that secretly do I/O |
| SAFE304 | side_effects |
Functions that call print, open, etc. without signalling intent |
| SAFE401 | resource_lifecycle |
Files or connections opened outside a with block |
| SAFE501 | unbounded_loops |
while True loops with no break |
Dataflow rules (opt-in, disabled by default):
| Code | Rule | What it flags |
|---|---|---|
| SAFE801 | tainted_sink |
User input flowing into eval, exec, subprocess, etc. without sanitization |
| SAFE802 | return_value_ignored |
Discarding the return value of calls like subprocess.run or file.write |
| SAFE803 | null_dereference |
Chaining methods directly on calls that can return None, e.g. d.get("key").strip() |
For opt-in rules (SAFE601, SAFE701, SAFE702) and full configuration options for every rule, see CONFIGURATION.md.
Suppressing violations inline
Add a # nosafe comment to suppress a violation on a specific line without changing global config.
Suppress all violations on a line:
result = eval(user_input) # nosafe
Suppress a specific rule by code:
while True: # nosafe: SAFE501
...
Suppress by rule name:
while True: # nosafe: unbounded_loops
...
Suppress multiple rules at once:
def get_data(conn, q, p1, p2, p3, p4, p5, p6): # nosafe: SAFE101, SAFE103
...
When at least one violation is suppressed, the CLI summary reports the count so suppressions remain visible and auditable. Use # nosafe sparingly — it's for line-level exceptions only. For broader suppression use the config-level options:
# pyproject.toml
[tool.safelint]
ignore = ["SAFE203", "side_effects"] # suppress project-wide
[tool.safelint.per_file_ignores]
"tests/**" = ["SAFE101", "SAFE103"] # suppress only for matching files
See CONFIGURATION.md — Inline suppression, CONFIGURATION.md — Global ignore list, and CONFIGURATION.md — Per-file ignore list for full reference.
Configuration
SafeLint is configured via [tool.safelint] in your pyproject.toml, or a .safelint.yaml file. See CONFIGURATION.md for all options, defaults, and examples.
Ready-to-copy samples:
- examples/sample.pyproject.toml — TOML format (recommended)
- examples/sample.safelint.yaml — YAML format (legacy)
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run the linter on itself
safelint check src/
Releasing to PyPI (Trusted Publishing)
This project publishes to PyPI via GitHub Actions using PyPI Trusted Publishing (OIDC). Do not use local uv publish username/password auth.
One-time setup:
- In PyPI, open your project → Manage → Publishing → Add a trusted publisher.
- Use:
- Owner:
shelkesays - Repository:
safelint - Workflow:
publish.yml - Environment:
pypi
- Owner:
- In GitHub, create an environment named
pypiin Settings → Environments.
Release flow:
# 1) bump version in pyproject.toml
# 2) commit and push
git tag vX.Y.Z
git push origin vX.Y.Z
Pushing the version tag triggers .github/workflows/publish.yml, which builds and publishes to PyPI.
Contributing
See CONTRIBUTING.md for guidelines on bug reports, adding new rules, and opening pull requests.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file safelint-1.3.2.tar.gz.
File metadata
- Download URL: safelint-1.3.2.tar.gz
- Upload date:
- Size: 49.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3a7a028babd8de90141ea617313170f78d384fc31805542569f4264f7f7c7ed
|
|
| MD5 |
e1fe04cee49f51249c15d44647239774
|
|
| BLAKE2b-256 |
0fbd4e1d174314a79fa0d4375229d3bb224af068bcc234b12ea9ce396c3b9cb4
|
Provenance
The following attestation bundles were made for safelint-1.3.2.tar.gz:
Publisher:
publish.yml on shelkesays/safelint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
safelint-1.3.2.tar.gz -
Subject digest:
b3a7a028babd8de90141ea617313170f78d384fc31805542569f4264f7f7c7ed - Sigstore transparency entry: 1369709108
- Sigstore integration time:
-
Permalink:
shelkesays/safelint@bf8d4d4c79b9cf0a02f0a90dea31431d573de7af -
Branch / Tag:
refs/tags/v1.3.2 - Owner: https://github.com/shelkesays
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bf8d4d4c79b9cf0a02f0a90dea31431d573de7af -
Trigger Event:
push
-
Statement type:
File details
Details for the file safelint-1.3.2-py3-none-any.whl.
File metadata
- Download URL: safelint-1.3.2-py3-none-any.whl
- Upload date:
- Size: 39.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2a9c9f521eae3301c595c903ab5aa3b80914a20c51920566dae23d056e91db9
|
|
| MD5 |
6e267754a6841d62d31e4a8a7efb0603
|
|
| BLAKE2b-256 |
ec438620d9d5498547e22b68e7e80f7f4f0fbd11d15a18f28bbef9ad104f6e05
|
Provenance
The following attestation bundles were made for safelint-1.3.2-py3-none-any.whl:
Publisher:
publish.yml on shelkesays/safelint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
safelint-1.3.2-py3-none-any.whl -
Subject digest:
b2a9c9f521eae3301c595c903ab5aa3b80914a20c51920566dae23d056e91db9 - Sigstore transparency entry: 1369709316
- Sigstore integration time:
-
Permalink:
shelkesays/safelint@bf8d4d4c79b9cf0a02f0a90dea31431d573de7af -
Branch / Tag:
refs/tags/v1.3.2 - Owner: https://github.com/shelkesays
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bf8d4d4c79b9cf0a02f0a90dea31431d573de7af -
Trigger Event:
push
-
Statement type: