Skip to main content

ruffian

PyPI Wheel Downloads CI License: MIT ruff 0.16.1

A ruffian breaks rules. This tool adds the ones ruff refused.

ruffian is a drop-in superset of ruff. It runs ruff internally, adds its own built-in lint rules, and supports user-defined plugin executables — all producing output that is indistinguishable from ruff's own.

Replace ruff with ruffian in your CI scripts, pre-commit hooks, and editor config. Everything ruff does still works. ruffian adds on top.


Installation

pip install ruffian
# or
uv add --dev ruffian

ruff is a declared dependency and will be installed automatically.


Usage

# Check files (ruff rules + ruffian built-in rules + your plugins)
ruffian check src/

# Format files — pure passthrough to ruff format
ruffian format src/

# Show documentation for a built-in rule
ruffian rule PLC0302

# JSON output (same format as ruff --output-format json)
ruffian check src/ --output-format json

ruffian accepts the same flags as ruff check for the options it passes through. Run ruffian --help for the full list.

Output format note: ruffian's text output matches ruff check --output-format concise --quiet — one path:row:col: CODE [*] message line per violation, no source context or summary footer. Use --output-format json for full detail.


Inline suppression

Add a # ruffian: noqa comment to suppress ruffian violations on a specific line:

some_huge_module_header = True  # ruffian: noqa           # suppress all ruffian rules on this line
some_huge_module_header = True  # ruffian: noqa PLC0302   # suppress a specific rule
some_huge_module_header = True  # ruffian: noqa PLC0302, RFN001  # suppress multiple rules

Note: ruff's own # noqa suppression is handled by ruff before violations reach ruffian. Use # noqa: CODE to suppress ruff violations and # ruffian: noqa CODE to suppress ruffian violations.


Configuration

All ruffian config lives in pyproject.toml under [tool.ruffian]. Ruff's own [tool.ruff] section is untouched and passed directly to ruff.

[tool.ruffian]
select = ["PLC0302"]   # built-in rules to enable (empty = all enabled)
ignore = []

[tool.ruffian.rules.PLC0302]
max-module-lines = 800   # override the default (1000); `max-lines` also accepted for compatibility

# User plugins
[[tool.ruffian.plugins]]
name = "no-todo"
executable = "./scripts/no_todo.py"   # any executable
config = {}                            # passed to the plugin as JSON on stdin

Built-in rules

Code Name Pylint source Default
PLC0302 too-many-module-lines C0302 1000

Rule code prefixes

ruffian follows ruff's own prefix conventions. Since ruffian only implements rules that ruff has not, there is no overlap.

Prefix Meaning
PLC, PLE, PLR, PLW Pylint rules not implemented by ruff — same PL prefix ruff uses, no conflict since we only ship what ruff skipped
RFN Novel rules with no equivalent in any existing linter
RFC Reserved for user plugin rules — plugin authors must use this prefix to avoid collisions with ruffian built-ins

PLC0302 — too-many-module-lines

Reports Python modules that exceed a configurable line count. Encourages splitting large files before they become hard to navigate.

[tool.ruffian.rules.PLC0302]
max-module-lines = 800   # default: 1000; `max-lines` also accepted for v0.1 compatibility

To raise the limit or disable the rule globally:

[tool.ruffian]
ignore = ["PLC0302"]   # disable entirely

To suppress it for a single file, add # ruffian: noqa PLC0302 to line 1 of that file (the rule always fires on line 1):

# ruffian: noqa PLC0302 — this file is intentionally large (generated code)
...

Plugin system

Any executable — Python script, shell script, compiled binary — can act as a ruffian plugin. This is how you add project-specific rules without writing any Rust.

How ruffian calls your plugin

./my_plugin.py file1.py file2.py ...

Files to check are passed as positional arguments. A JSON config blob is written to stdin:

{
  "ruffian_version": "0.1.0",
  "config": { "threshold": 5 }
}

config is whatever you put in [[tool.ruffian.plugins]]config.

What your plugin must write to stdout

A JSON array of violations. Empty array means no violations.

[
  {
    "code": "MY001",
    "message": "Human-readable description",
    "filename": "/abs/path/to/file.py",
    "location": { "row": 42, "column": 0 },
    "end_location": { "row": 42, "column": 10 },
    "url": "https://my-docs.example.com/rules/MY001",
    "fix": null
  }
]

code, message, filename, and location are required. end_location, url, and fix are optional.

Exit codes: exit 0 regardless of violations found — violations are communicated via JSON, not the exit code. Exit non-zero to signal that the plugin itself failed (ruffian will report an error, separate from any lint violations).

Stderr: any output to stderr is forwarded to ruffian's stderr with a [plugin: name] prefix.

Minimal Python plugin

#!/usr/bin/env python3
import json, sys

config_blob = json.loads(sys.stdin.read())
files = sys.argv[1:]
violations = []

for path in files:
    source = open(path).read()
    # ... your logic ...

print(json.dumps(violations))

A working example is in python/example_plugins/example_plugin.py.

Security note

Plugins run as arbitrary executables with the same permissions as your shell. Only register plugins you trust.


Editor integration

ruffian's output format is identical to ruff's, so any editor integration that already works with ruff will work with ruffian without changes. Replace ruff with ruffian in your editor's lint command setting.

For VS Code with the Ruff extension, point it at the ruffian binary:

{
  "ruff.path": ["/path/to/ruffian"]
}

GitHub Actions

- name: Lint with ruffian
  run: |
    pip install ruffian
    ruffian check src/

Or pin the version for reproducible CI:

- name: Lint with ruffian
  run: |
    pip install ruffian==0.1.0
    ruffian check src/

Pre-commit

# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: ruffian
      name: ruffian
      entry: ruffian check
      language: system
      types: [python]

Contributing

Proposing a new built-in rule

ruffian only ships rules that ruff has explicitly declined to implement. Before opening a PR here, please follow this process:

  1. Propose the rule to ruff first. Open a feature request in the ruff issue tracker. Many rules belong there, not here — ruff has a much larger audience and faster release cadence.

  2. Use the plugin system in the meantime. While ruff considers your proposal, implement the rule as a ruffian plugin. This lets you and your team use it immediately with zero Rust code and no waiting on anyone.

  3. If ruff declines, open an issue here first. If ruff closes or explicitly refuses your issue, open a ruffian issue with a link to that discussion. This lets us align on the rule design before any code is written.

  4. Then submit a PR. Built-in ruffian rules are written in Rust — see Adding a built-in rule below. Your PR must reference the ruffian issue and the upstream ruff discussion that refused the rule.

This keeps ruffian's built-in rule set small and intentional: every rule here has a paper trail explaining why ruff said no.

Prerequisites

# 1. Install the task runner (provides the `task` command)
brew install go-task

# 2. Install everything else
task setup

task setup installs the Rust toolchain via rustup, dev tools (cargo-llvm-cov, llvm-tools), and maturin for packaging. After it completes, restart your terminal or run source ~/.cargo/env to pick up the Rust toolchain.


Adding a built-in rule

  1. Create src/rules/<rule_name>.rs and implement the Rule trait:
pub trait Rule: Send + Sync {
    fn code(&self) -> &'static str;
    fn name(&self) -> &'static str;
    fn description(&self) -> &'static str;
    fn check(&self, file: &ParsedFile) -> Vec<Violation>;
}

ParsedFile exposes three fields:

Field Type Notes
path String Path to the file as provided to ruffian on the CLI
source String Raw source text
ast Option<Parsed<ModModule>> Parsed AST from ruff_python_parser; None if the file has syntax errors

For source-level checks (line count, regex, etc.) use file.source. For structural checks use file.ast.as_ref().map(|p| p.syntax()) to get the ModModule and walk the statement list.

  1. Register it in src/rules/mod.rs — one line in all_rules(). No other changes required.

See PLC0302 for a complete example.

Development commands

task build              # debug build
task test               # run all tests
task test:coverage      # run tests with coverage report, fail below 85%
task lint               # cargo clippy -D warnings
task lint:fix           # clippy --fix (--allow-dirty) + cargo fmt
task fmt:check          # check formatting without writing changes (what CI runs)
task install:local      # build and install into the active Python env for manual testing
task version:bump       # bump minor version and push (optionally: task version:bump -- 1.0.0)
task release            # interactive release: checks CI, generates notes, creates GitHub release
task ruff:update        # update ruff dependency pins to latest release

Coding style

  • Functional-style Rust where it doesn't fight the type system
  • No traits or structs for things with only one implementation
  • Files stay under 500 lines — split by responsibility when they grow
  • thiserror for error types; anyhow only at the binary boundary
  • All PRs must pass cargo clippy -- -D warnings and cargo fmt -- --check

License

MIT

Download files

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

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

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

ruffian-0.17.0-py3-none-win_amd64.whl (2.0 MB view details)

Uploaded Python 3Windows x86-64

ruffian-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.3 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

ruffian-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.2 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

ruffian-0.17.0-py3-none-macosx_11_0_arm64.whl (2.1 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

ruffian-0.17.0-py3-none-macosx_10_12_x86_64.whl (2.2 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file ruffian-0.17.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: ruffian-0.17.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 2.0 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ruffian-0.17.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 b942e25587ff0810def800af5d06f0f06a91dff9f2c7aa69d908e25aebaf6493
MD5 9ec3ae62fd666ee22110be0ae69be241
BLAKE2b-256 ad157a2ef4077f6f30c51d182240cc287ae72a98b5890747130d67006ad834c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for ruffian-0.17.0-py3-none-win_amd64.whl:

Publisher: publish.yml on tluolamo/ruffian

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

File details

Details for the file ruffian-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for ruffian-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5d7d0e9c33490b86d32b79d12c9229d8f969340d6f6a0843623f0445f32f8363
MD5 f83592c1f2604f097dc64dabb0230be5
BLAKE2b-256 00e9f87d6f814b9d3455ca5ef90e03045d213d954bf0cd0722320186088820bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for ruffian-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on tluolamo/ruffian

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

File details

Details for the file ruffian-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for ruffian-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b690252b9909a4246926c8e8ec634f37ac6d25ee9d21abb07f6b5618ed62db01
MD5 81f84b5f8a0ebcbc3044dc39ea646a61
BLAKE2b-256 2e749f4aaa0c5d344952916b332bc606a452fc19535a320a98a524fbff25f6c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ruffian-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish.yml on tluolamo/ruffian

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

File details

Details for the file ruffian-0.17.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for ruffian-0.17.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f3ab2a504c4490d5a75d551254a7d202744754a7d49b54894b8abc9518999f8c
MD5 40cfa332bd200ccd24e753213de1baec
BLAKE2b-256 24ca7ddc2a8fbb3c153a2678a461e7de2cd9b5cbead91ed8b4a12a47b7b88cd4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ruffian-0.17.0-py3-none-macosx_11_0_arm64.whl:

Publisher: publish.yml on tluolamo/ruffian

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

File details

Details for the file ruffian-0.17.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for ruffian-0.17.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 902493bcc8f377513586d1e62d138d07960efbd9e506153d70383d9da81753f6
MD5 efb41d19d9967209d4d0de44f93f1467
BLAKE2b-256 7160b1611c4af7d7d4714194b18fa0898dac14d888224cf3071ee4364dc4a6f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for ruffian-0.17.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: publish.yml on tluolamo/ruffian

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

Release history Release notifications | RSS feed

This release

0.17.0 This release

5 files

0.11.0

5 files

0.10.0

5 files

0.9.0

5 files

0.8.0

5 files

0.7.0

5 files

0.6.0

5 files

0.5.0

5 files

0.4.0

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page