Skip to content

Suppression mechanisms#

SafeLint offers four ways to silence a rule, ranging from one-line surgical to project-wide. Pick the narrowest scope that matches your intent. Broader scopes are easier to forget about.

Mechanism Scope Where it lives Best for
# nosafe One line The file (trailing comment) "This specific line is intentional"
# safelint: ignore One file The file (own-line comment) "This whole file is intentional", generated code, fixtures, vendor stuff
[tool.safelint.per_file_ignores] A glob pattern safelint.toml / pyproject.toml Project policy: "files under tests/ never need their own tests"
ignore = [...] The whole project safelint.toml / pyproject.toml "We've decided this rule doesn't fit our codebase"

The narrower-scope forms compose with the broader ones: toml-level ignores still apply on top of file-level directives, and inline # nosafe always wins for the specific line it's on. The two TOML-level mechanisms are documented on the Configuration file page; this page covers the in-source forms (# nosafe and # safelint: ignore) plus the run-summary diagnostics that surface what got suppressed.

Inline # nosafe#

Add a # nosafe comment to the end of any line to suppress violations on that specific line. This is the escape hatch for the rare case where a violation is a deliberate, justified choice.

Suppressed violations do not appear in output and do not count toward the blocking total, but a per-code breakdown of what was suppressed is always reported at the end of the run so suppressions remain auditable.

Syntax#

Comment Effect
# nosafe Suppress all violations on this line
# nosafe: SAFE101 Suppress only the rule with code SAFE101
# nosafe: function_length Suppress only the rule named function_length
# nosafe: SAFE101, SAFE103 Suppress multiple rules (comma-separated codes or names)

Both rule codes (e.g. SAFE101) and rule names (e.g. function_length) are accepted and can be mixed in the same comment.

Examples#

Suppress all violations on a line. Use when a line genuinely triggers multiple unrelated rules and fixing each would make the code worse:

result = eval(user_input)  # nosafe

Suppress a single code. Preferred; makes the intent explicit and leaves other rules active:

while True:  # nosafe: SAFE501
    item = queue.get()     # blocking poll, bounded by the caller's timeout
    if item is None:
        break

Suppress by rule name. Identical behaviour to suppressing by code; use whichever is more readable in context:

while True:  # nosafe: unbounded_loops
    ...

Suppress multiple rules. Keep the list short; a long list is a signal the code needs refactoring:

def get_data(conn, query, p1, p2, p3, p4, p5, p6):  # nosafe: SAFE101, SAFE103
    ...

When to use # nosafe#

Use # nosafe when:

  • A violation is correct by design and fixing it would make the code worse (e.g. a deliberate while True polling loop with an external timeout).
  • A third-party integration forces a pattern safelint flags (e.g. a framework-required function signature with many parameters).
  • You are mid-refactor and need to commit a transitional state without breaking CI.

Prefer config changes (adjusting thresholds or disabling rules) over # nosafe when the exception applies to the entire project. For a single file or whole-file scope, see the file-level directive below.

File-level # safelint: ignore#

For cases where a whole file is intentionally violating (auto-generated code, code that has to look the way it does to satisfy a vendor protocol, a fixture file, and similar), put a directive in the file itself rather than in toml:

# safelint: ignore: SAFE101, SAFE304
# Auto-generated by openapi-codegen 4.2.1: do not edit by hand.

import requests   # ... 800 lines of generated I/O ...

Forms accepted:

Directive Effect
# safelint: ignore Suppress every rule for this file
# safelint: ignore: SAFE101 Suppress only the rule with code SAFE101 for this file
# safelint: ignore: function_length Suppress by rule name (equivalent to the code form)
# safelint: ignore: SAFE101, SAFE304 Multiple codes / names, comma-separated

Placement: the directive must be a comment alone on its line (no preceding code). Trailing comments after code are scope-local and use # nosafe instead, putting a # safelint: ignore after code on the same line is silently skipped, which prevents accidental file-wide scope creep from what was meant as a per-line suppression.

String-literal safety: the parser uses Tree-sitter, so a literal # safelint: ignore inside a docstring or string is correctly ignored.

Typo guard: unknown codes / rule names trigger a safelint: warning: line on stderr, same as the toml-config typo guard. The run continues; the bogus entry is just noted.

Suppressed violations stay auditable: they go into LintResult.suppressed and surface in the per-code breakdown the CLI prints at the end of the run, so file-level suppressions are visible without grepping the codebase.

End-of-run summary#

When suppressions are active, the summary surfaces a per-code breakdown so it is clear which rules were silenced. Codes are ordered by descending count, ties broken alphabetically.

When there are still active violations, the breakdown rides on the post-summary line. The text varies depending on whether any rule emitted advisory suggestions for those violations (added in 1.8.0):

Found 2 errors, 1 warning. [--fail-on=error].
No suggestions available (safelint does not auto-fix). (2 SAFE501, 1 SAFE304 suppressed)

If at least one violation has a suggestion, the line reads N advisory suggestion(s) available, view via --format json or --format sarif (safelint does not auto-apply fixes). followed by the same suppression breakdown. Suggestions are emitted in both JSON output and the SARIF fixes[] block (advisory by spec). SafeLint never auto-applies, the line is informational.

When the run is otherwise clean (no active violations), only the all-clear line is printed, the suggestion / no-suggestion line is omitted:

All checks passed. (2 SAFE501, 1 SAFE304 suppressed)

A fully clean run with no suppressions prints a single line in bold green to match ruff / ty:

All checks passed.

Diagnostic output#

Lint violations and the run summary are written to stdout in the ruff/ty multi-line format. Issues that aren't lint violations, typos in your ignore list, malformed TOML, files skipped because they exceed max_file_size_bytes, etc., are written to stderr as single-line safelint: warning: / safelint: error: messages so they stay out of the violation stream and are captured separately by pre-commit, CI, and editor integrations.

Examples:

safelint: warning: unknown entries in ignore list (typo or stale rule?): SAFFE101
safelint: error: failed to parse /path/to/pyproject.toml: Expected '=' after a key in a key/value pair (at line 5, column 12), skipping file
safelint: warning: skipping /path/to/huge_generated.py (12,345,678 bytes exceeds max_file_size_bytes=5,242,880)

Diagnostics fall into two timing buckets:

  • Config-load / engine-construction time: typos in ignore / per_file_ignores, malformed TOML, the max_file_size_bytes = 0 fallback warning. These are emitted once, up front, before any file is linted. None of them fails the run on their own; safelint falls back to defaults and continues.
  • Per-file runtime, max_file_size_bytes skip warnings fire from SafetyEngine.check_file() as the engine walks the file list, so they are interleaved with lint output (one warning per oversize file, just before that file's spot in the run). The skipped file produces no violations and does not affect exit status.