Python#
SafeLint analyses Python source for the Holzmann "Power of Ten" safety rules, function length, nesting depth, cyclomatic complexity, error-handling discipline, hidden side effects, dataflow taint, and other classes of bug that style linters like ruff don't catch. Python is SafeLint's original target language and stays the most fully covered.
File extensions#
.py, .pyw. Both are picked up by safelint check (directory mode, --all-files mode, and the pre-commit hook). Notebook formats (.ipynb) are not yet registered.
Quick start#
pip install 'safelint[python]' # or: uv add 'safelint[python]'
safelint check src/ # lint a directory (git-modified files by default)
safelint check --all-files . # lint everything
safelint check --format json src/ # machine-readable for editors / CI
v2.0.0 ships every language grammar as an opt-in extra, the [python] extra installs tree-sitter-python alongside the engine. Plain pip install safelint installs only the engine and emits an install hint on first run.
Rules that fire on Python#
All 19 user-facing rules apply to Python: the 16 cross-language rules (Python / JS / TS / Java) plus SAFE302 global_mutation (Python / JS / TS only, not ported to Java yet) plus the 2 Python-only rules (SAFE201 bare_except, SAFE301 global_state). The 1 JavaScript-family-only rule (SAFE305 wide_scope_declaration) and the 4 Java + Spring Boot only rules (SAFE901-904) are skipped automatically by the engine's per-language dispatch.
| Code | Rule | Notes for Python |
|---|---|---|
| SAFE101 | function_length |
Default cap 60 lines; count_mode supports lines / logical_lines / statements (Python-only mode). |
| SAFE102 | nesting_depth |
Counts if / for / while / with / try / match blocks. Default max 2. |
| SAFE103 | max_arguments |
Counts positional, keyword, *args, **kwargs separately. Excludes self / cls. Default cap 7. |
| SAFE104 | complexity |
Cyclomatic complexity, every if / elif / for / while / except / case / ternary / and / or adds one. Default cap 10. |
| SAFE201 | bare_except |
Python-only. Fires on except: with no exception type, catches KeyboardInterrupt and SystemExit. |
| SAFE202 | empty_except |
Fires on except: pass, except: ..., except: 0, except: "TODO". |
| SAFE203 | logging_on_error |
Requires a call to logger.{debug,info,warning,error,exception,critical} (or bare raise) in every except handler. |
| SAFE301 | global_state |
Python-only. Fires on the global keyword. With strict = true, fires on every declaration; default is "declaration + write". |
| SAFE302 | global_mutation |
Function-body writes that follow a global declaration. Reading a global doesn't fire. |
| SAFE303 | side_effects_hidden |
Functions named with a pure-prefix (calculate_, get_, is_, …) that secretly call open() / print() / input(). |
| SAFE304 | side_effects |
Any function calling an I/O primitive whose name doesn't signal I/O (no log_ / write_ / read_ / etc. infix). |
| SAFE401 | resource_lifecycle |
Tracked acquirer calls (open, connect, Lock, Pool, …) must be inside a with statement. |
| SAFE501 | unbounded_loops |
while True: with no break. Also fires on while <non-comparison>:, a heuristic that stays Python-only. |
| SAFE601 | missing_assertions |
Functions with zero assert statements. Disabled by default. |
| SAFE701 | test_existence |
Every source file should have a matching test_<stem>.py under test_dirs. Disabled by default. |
| SAFE702 | test_coupling |
If you change src/foo.py, you must also change tests/test_foo.py in the same commit. Disabled by default. |
| SAFE801 | tainted_sink |
Function parameters / input() flowing into eval / exec / subprocess / cursor.execute. Disabled by default. |
| SAFE802 | return_value_ignored |
Bare calls to subprocess.run, f.write, socket.send, os.rename, etc., return value carries success/failure. Disabled by default. |
| SAFE803 | null_dereference |
config.get("k").strip(), dereferencing a call that can return None. Disabled by default. |
The 1 rule not registered for Python: SAFE305 wide_scope_declaration, JavaScript-only; Python has no var / let / const distinction.
Configuration#
SafeLint reads its config from [tool.safelint] in pyproject.toml, or from a standalone safelint.toml at the project root. Standalone wins when both are present.
pyproject.toml:
[tool.safelint]
mode = "ci" # "local" (fail-on=error) or "ci" (fail-on=warning)
ignore = ["SAFE701"] # rules suppressed project-wide
[tool.safelint.per_file_ignores]
"tests/**" = ["SAFE101", "SAFE601"] # tests routinely have longer functions
"migrations/**" = ["*"] # ignore everything under migrations/
[tool.safelint.rules.function_length]
max_lines = 80 # raise the default cap
count_mode = "logical_lines"
[tool.safelint.rules.tainted_sink]
enabled = true # opt into the dataflow rules
Standalone safelint.toml:
# Same content but drop the [tool.safelint] prefix
mode = "ci"
ignore = ["SAFE701"]
[per_file_ignores]
"tests/**" = ["SAFE101", "SAFE601"]
[rules.function_length]
max_lines = 80
See Configuration file for the full list of top-level keys and Rules reference for every per-rule option.
Installing the Python extra#
v2.0.0 ships every language grammar, Python included, as an opt-in extra so projects only install what they actually lint:
pip install 'safelint[python]' # Python-only project
pip install 'safelint[python,javascript]' # Python + JS monorepo
pip install 'safelint[all]' # kitchen-sink
pip install safelint (no extras) installs only the engine. safelint will emit safelint: warning: skipping .py files, install with: pip install 'safelint[python]' on first run when it finds Python files but the grammar isn't installed. Heads-up for CI: in a Python-only project that pattern means every candidate file gets skipped, which fires the silent-failure guard and exits with code 2 plus the install hint embedded in the error, so CI / pre-commit can't accidentally report green on an un-linted run.
Pre-commit integration#
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shelkesays/safelint
rev: v2.1.0rc1 # pin to a release (use a recent tag; v2.1.0rc1 also unlocks the Java extra if you later add .java files)
hooks:
- id: safelint
# Every safelint hook needs an extra in v2.0.0+, including Python-only projects.
additional_dependencies: ['safelint[python]']
# Optional: scope to a directory
files: ^src/
The published hook spec sets types_or: [python, javascript, ts, tsx] and no files: filter, add your own files: / exclude: keys to scope it. Mixed-language projects compose extras: additional_dependencies: ['safelint[python,javascript]'] (or [all]).
Python-specific config keys#
Most rule options work uniformly across languages, but a few are Python-only:
[tool.safelint.rules.function_length],count_mode = "statements"(counts AST statement nodes) is Python-only. JavaScript files uselines(default) orlogical_lines.[tool.safelint.rules.global_mutation],strict = true(fire on everyglobaldeclaration regardless of write) is Python-only.[tool.safelint.rules.side_effects_hidden],pure_prefixesdefaults match Pythonsnake_case(calculate_,get_,is_,has_,find_). For mixed-language repos the same list applies to both, the substring check is case-insensitive.[tool.safelint.rules.resource_lifecycle],tracked_functions,extend_tracked_functions, andcleanup_patternsare Python-only keys. The JavaScript equivalent istracked_functions_javascript.[tool.safelint.rules.tainted_sink],sinks,sanitizers,sourcesdefault to Python's threat surface (eval,exec,subprocess, …). The_javascript-suffixed equivalents are independent lists.
Contributing#
Want to refine a rule's Python behaviour, add a Python-specific config option, or fix a Python parser edge case? See Adding a language for the architecture overview, or open an issue / PR against the main repo.