Skip to main content

Engineering safety lint rules and pre-commit integration for modern Python codebases

Project description

SafeLint

CI PyPI Python

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

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 a per-code breakdown (e.g. (2 SAFE501, 1 SAFE304 suppressed)) 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 standalone safelint.toml file at your project root. When both exist in the same directory, safelint.toml wins — its values override anything in [tool.safelint] — matching ruff's ruff.toml / pyproject.toml precedence. See CONFIGURATION.md for all options, defaults, and examples.

Ready-to-copy samples:


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:

  1. In PyPI, open your project → ManagePublishingAdd a trusted publisher.
  2. Use:
    • Owner: shelkesays
    • Repository: safelint
    • Workflow: publish.yml
    • Environment: pypi
  3. In GitHub, create an environment named pypi in 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

safelint-1.4.1.tar.gz (66.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

safelint-1.4.1-py3-none-any.whl (50.8 kB view details)

Uploaded Python 3

File details

Details for the file safelint-1.4.1.tar.gz.

File metadata

  • Download URL: safelint-1.4.1.tar.gz
  • Upload date:
  • Size: 66.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for safelint-1.4.1.tar.gz
Algorithm Hash digest
SHA256 7612d545ee8b8d0a169d289e702ec8263690382cc5854fe7c94fe293a253bdf3
MD5 be91fa723eb9784132e165069a08004f
BLAKE2b-256 e9161fde146b695b0bfc6bc8450add80d29803ff69e36deae8f92f56549dc347

See more details on using hashes here.

Provenance

The following attestation bundles were made for safelint-1.4.1.tar.gz:

Publisher: publish.yml on shelkesays/safelint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file safelint-1.4.1-py3-none-any.whl.

File metadata

  • Download URL: safelint-1.4.1-py3-none-any.whl
  • Upload date:
  • Size: 50.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for safelint-1.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 817497f13da304fcbaef88b48d53b74f77410a886abc77b65e822c77a66f7391
MD5 8971eb3c498221ad2af5de77045b18e6
BLAKE2b-256 d32bb631a17ecd2f6597a878b79c9601f59635a67ca1f309932d94d90a1fc8ba

See more details on using hashes here.

Provenance

The following attestation bundles were made for safelint-1.4.1-py3-none-any.whl:

Publisher: publish.yml on shelkesays/safelint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page