Gruff
Gruff is an opinionated, deterministic maintainability linter for Python. It complements Ruff with project policies that make agent-assisted code easier to understand and review; it does not infer who or what wrote the code.
Installation
Requires Python 3.10 or later.
pip install gruff
Or with uv:
uv tool install gruff
Verify it works:
gruff --version
[!TIP] To try the latest development version (the head of
mainon GitHub) before it is published:uv tool install git+https://github.com/wkentaro/gruff
Quick start
Enable every Gruff rule in pyproject.toml:
[tool.gruff.lint]
select = ["GR"]
Then check the current directory:
gruff check .
All rules are opt-in. Use an exact code such as GR001 to adopt rules individually; GR enables every Gruff rule. A check with no enabled rules succeeds but warns that it performed no policy analysis.
Rules at a glance
The first release tests four theses: inputs are easier to trace when definitions declare how callers pass them, private behavior is easier to review when callers supply every value, package initializer manifests are easier to review when every public import path defines __all__, and constants are easier to review when uppercase names and Final annotations always appear together.
| Code | Rule | Policy |
|---|---|---|
| GR001 | explicit-input-conventions |
Every fixed input has an explicit calling convention. |
| GR002 | required-private-inputs |
Callers supply every fixed input to private callables. |
| GR003 | package-dunder-all |
Every public package import path defines __all__. |
| GR004 | final-constants |
Uppercase names and Final annotations appear together. |
Configuration and CLI
Gruff reads configuration only from pyproject.toml:
[tool.gruff]
output-format = "full"
[tool.gruff.lint]
select = ["GR001", "GR002", "GR003", "GR004"]
ignore = []
per-file-ignores = { "callbacks.py" = ["GR001"] }
output-format accepts full, concise, json, or github. Rule selectors accept an exact code, the GR prefix, or ALL; the more specific selector wins when select and ignore overlap, and ignore wins ties.
Command-line options override configuration:
gruff check .
gruff check --select GR001,GR002 .
gruff check --ignore GR004 .
gruff check --output-format github .
gruff check --config path/to/pyproject.toml .
gruff check --isolated --select GR .
Pass files or directories as paths. Directory discovery checks .py, .pyi, and .pyw files and respects Git ignore files. Run gruff check --help for the complete command reference.
Lint findings, including invalid Python syntax, exit with status 1. Configuration, I/O, and internal failures exit with status 2. Gruff does not rewrite source code in the first release.
Rule reference
explicit-input-conventions (GR001)
Flags each fixed caller-supplied input to a module-level function or method that is positional-or-keyword, whatever the definition is named. Positional-only (/) and keyword-only (*) inputs declare an explicit calling convention and are accepted; implicit method receivers and variadic parameters are excluded.
Before → after:
-def _resize_image(data: bytes, width: int) -> bytes:
+def _resize_image(data: bytes, /, *, width: int) -> bytes:
return resize(data, width=width)
def make_thumbnail(data: bytes, /) -> bytes:
return _resize_image(data, width=512)
GR002 stays private-only while GR001 covers every callable: a default on a public callable is the contract external callers depend on, while declaring a calling convention costs the same everywhere. Overrides of external base classes and framework hooks suppress with # noqa: GR001.
required-private-inputs (GR002)
Flags each fixed caller-supplied input to a private module-level function or method that has a default; implicit method receivers and variadic parameters are excluded.
Before → after:
-def _resize_image(*, data: bytes, width: int = 512) -> bytes:
+def _resize_image(*, data: bytes, width: int) -> bytes:
return resize(data, width=width)
def make_thumbnail(data: bytes, /) -> bytes:
- return _resize_image(data=data)
+ return _resize_image(data=data, width=512)
Choose the input shape before suppressing the rule. If callers never vary a value, remove the input and keep the value inside the private definition instead of making every caller repeat it. If callers vary the value, keep the input required and have callers supply it explicitly. Reserve a default and GR002 suppression for meaningful semantic policy that would otherwise be duplicated across callers.
package-dunder-all (GR003)
Flags a package initializer when a successfully completing import path leaves a public binding without __all__. The rule covers __init__.py and __init__.pyi, including bindings in module-level control flow, and reports at most one finding per file. Empty, private-only, type-checking-only, and statically false paths do not require a manifest.
Before → after:
from .client import Client
from .errors import GruffError
+__all__ = ["Client", "GruffError"]
final-constants (GR004)
Flags simple-name assignments when an uppercase name and a Final annotation do not appear together. The rule applies in module, class, and function scopes, including nested control flow. Enum members, type aliases, chained and unpacking assignments, augmented assignments, loop and context-manager targets, attributes, subscripts, and imports are excluded.
Before → after:
from typing import Final
-THUMBNAIL_WIDTH = 512
-image_format: Final = "png"
+THUMBNAIL_WIDTH: Final = 512
+IMAGE_FORMAT: Final = "png"
Final prevents type checkers from accepting rebinding; it does not make mutable contents immutable. For example, a Final[list[str]] still permits append. Use an immutable value when the contents must not change.
Exceptions
Use a positional-only marker when an external contract intentionally accepts positional calls. Suppress GR001 only when the contract must accept both positional and keyword calls. Suppress GR002 only when a default centralizes meaningful semantic policy that callers would otherwise duplicate. Suppress GR004 when a binding intentionally follows an external convention:
def _format_cost(value: float, /) -> str:
return f"${value:.2f}"
def _format_cost_compat(value: float) -> str: # noqa: GR001 -- contract accepts both call styles
return f"${value:.2f}"
def _fetch(*, url: str, timeout: float = 30.0) -> bytes: # noqa: GR002 -- service timeout policy
return fetch(url, timeout=timeout)
EXTERNAL_NAME = 1 # noqa: GR004 -- public protocol spelling
For a dynamic package manifest, suppress GR003 on the reported public binding and state why deterministic source analysis does not apply:
public = load_exports() # noqa: GR003 -- exec() defines __all__ below
Prefer an inline suppression because it keeps the exception next to its reason. For files made entirely of protocol implementations, use a per-file ignore instead.
Recommended Ruff pairing
Gruff does not duplicate checks that Ruff already provides. These Ruff rules extend the same theses to code Gruff does not cover:
[tool.ruff.lint]
extend-select = ["ARG", "FBT", "B006", "B008", "PLR2004", "RUF012", "RUF022"]
F401 and F822 are in Ruff's default rule set; the pairing below assumes they stay enabled.
Callable inputs (GR001, GR002)
ARG flags unused function and method arguments, a shape neither GR001 nor GR002 inspects:
-def _resize_image(*, data: bytes, width: int, legacy: bool) -> bytes:
+def _resize_image(*, data: bytes, width: int) -> bytes:
return resize(data, width=width)
GR001 makes every definition declare each input as positional-only or keyword-only; FBT001 and FBT002 go further for booleans, which stay ambiguous at a call site even when GR001 accepts them as positional-only:
-def resize_image(data: bytes, keep_aspect: bool) -> bytes:
+def resize_image(data: bytes, /, *, keep_aspect: bool) -> bytes:
return resize(data, keep_aspect=keep_aspect)
GR002 removes defaults from private callables; B006 and B008 catch shared mutable defaults and import-time call defaults on the public callables that keep theirs:
-def make_thumbnails(data: bytes, /, *, widths: list[int] = []) -> list[bytes]:
+def make_thumbnails(data: bytes, /, *, widths: list[int] | None = None) -> list[bytes]:
-def fetch_image(*, client: Client = Client()) -> bytes:
+def fetch_image(*, client: Client | None = None) -> bytes:
Package manifests (GR003)
GR003 only requires the manifest to exist. Once it does, F401 flags re-exports missing from it:
from .client import Client
from .errors import GruffError
-__all__ = ["Client"]
+__all__ = ["Client", "GruffError"]
F822 finds names in the manifest that are not defined:
-__all__ = ["Client", "GruffErorr"]
+__all__ = ["Client", "GruffError"]
RUF022 sorts static manifests:
-__all__ = ["GruffError", "Client"]
+__all__ = ["Client", "GruffError"]
Constants (GR004)
PLR2004 turns magic values into named constants, which GR004 then requires to be uppercase and Final:
+MAX_WIDTH: Final = 4096
+
def validate_width(width: int, /) -> None:
- if width > 4096:
+ if width > MAX_WIDTH:
raise ValueError(width)
RUF012 applies the same annotation discipline to mutable class attributes, which GR004 excludes:
class ThumbnailWriter:
- formats = ["png", "jpg"]
+ formats: ClassVar[list[str]] = ["png", "jpg"]
Distribution
Gruff releases use PyPI wheels for Linux x86_64 and aarch64, macOS x86_64 and arm64, and Windows x86_64. Gruff is not published to crates.io.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
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 gruff-0.0.3-py3-none-win_amd64.whl.
File metadata
- Download URL: gruff-0.0.3-py3-none-win_amd64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f9aa93765fbfcb010a42ba6fda777060dd7404414fea37a9e4dd4857bd3c771
|
|
| MD5 |
77b71c53e0c8245c00265915e61a013d
|
|
| BLAKE2b-256 |
20c7225e5868e2362d69eb6aa94ffcc74df340b711782f9e4d3e4b8db9ab4dea
|
Provenance
The following attestation bundles were made for gruff-0.0.3-py3-none-win_amd64.whl:
Publisher:
release.yml on wkentaro/gruff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gruff-0.0.3-py3-none-win_amd64.whl -
Subject digest:
3f9aa93765fbfcb010a42ba6fda777060dd7404414fea37a9e4dd4857bd3c771 - Sigstore transparency entry: 2623975881
- Sigstore integration time:
-
Permalink:
wkentaro/gruff@4c05d0d79891d036591770b1be78632dded86a43 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/wkentaro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c05d0d79891d036591770b1be78632dded86a43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gruff-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: gruff-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.0 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b63fa84689237767000160246d4bec3d699cd15e307c599aa913fefdcd3d802a
|
|
| MD5 |
028d4ce19d0c109ef4d8b59fbb3b1dcc
|
|
| BLAKE2b-256 |
02304dbdc38907bedd6431132f1d1ae0ff270a7961ed7d01fe24184e99ad3ebe
|
Provenance
The following attestation bundles were made for gruff-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on wkentaro/gruff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gruff-0.0.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
b63fa84689237767000160246d4bec3d699cd15e307c599aa913fefdcd3d802a - Sigstore transparency entry: 2623975795
- Sigstore integration time:
-
Permalink:
wkentaro/gruff@4c05d0d79891d036591770b1be78632dded86a43 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/wkentaro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c05d0d79891d036591770b1be78632dded86a43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gruff-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: gruff-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ec3b65da5a427cb241d75eb2830903aa58dde52acfb71854c7767228c86a313
|
|
| MD5 |
f8df5e22b718b7f76eab2b4b4ce81521
|
|
| BLAKE2b-256 |
5bd7965cf7534ed12fcac92cc836e8f70eff8d9623a6070e9ee72bf7cefb35e0
|
Provenance
The following attestation bundles were made for gruff-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on wkentaro/gruff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gruff-0.0.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
3ec3b65da5a427cb241d75eb2830903aa58dde52acfb71854c7767228c86a313 - Sigstore transparency entry: 2623975964
- Sigstore integration time:
-
Permalink:
wkentaro/gruff@4c05d0d79891d036591770b1be78632dded86a43 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/wkentaro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c05d0d79891d036591770b1be78632dded86a43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gruff-0.0.3-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: gruff-0.0.3-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4350d2723d5af437175d91c561273dd9b6c7417e4be992152c64aff6b4d646f5
|
|
| MD5 |
9101ccb7fa38e7f78f8da55e2941be4d
|
|
| BLAKE2b-256 |
f33bfd91d67738194be091c56cf7326a162a2d6c7d54162c0099cdb7a41040ac
|
Provenance
The following attestation bundles were made for gruff-0.0.3-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on wkentaro/gruff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gruff-0.0.3-py3-none-macosx_11_0_arm64.whl -
Subject digest:
4350d2723d5af437175d91c561273dd9b6c7417e4be992152c64aff6b4d646f5 - Sigstore transparency entry: 2623975837
- Sigstore integration time:
-
Permalink:
wkentaro/gruff@4c05d0d79891d036591770b1be78632dded86a43 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/wkentaro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c05d0d79891d036591770b1be78632dded86a43 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gruff-0.0.3-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: gruff-0.0.3-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1f77b0de73006feed4a27646da417d18c0f8fb43bfe79bf4ac271d50ccc8d22
|
|
| MD5 |
2845154c1ddba7a676e4e6f734d44041
|
|
| BLAKE2b-256 |
f2af141e6409792df4d639d3432b40cb15bfa2e06e7f0ab73484f89f17306845
|
Provenance
The following attestation bundles were made for gruff-0.0.3-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on wkentaro/gruff
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gruff-0.0.3-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
d1f77b0de73006feed4a27646da417d18c0f8fb43bfe79bf4ac271d50ccc8d22 - Sigstore transparency entry: 2623975733
- Sigstore integration time:
-
Permalink:
wkentaro/gruff@4c05d0d79891d036591770b1be78632dded86a43 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/wkentaro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4c05d0d79891d036591770b1be78632dded86a43 -
Trigger Event:
push
-
Statement type: