Skip to main content

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 main on 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: private 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 keyword-only-private-inputs Every fixed private 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

keyword-only-private-inputs (GR001)

Flags each fixed caller-supplied input to a private module-level function or method that is positional-or-keyword. 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)

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)

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"

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 other rules on definitions that intentionally provide a convenience default or follow 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 _render(*, value: float, unit: str = ""):  # noqa: GR002 -- optional suffix
    return f"{value}{unit}"


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, including arguments on private definitions:

-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 private definitions declare each input as positional-only or keyword-only; FBT001 and FBT002 extend the keyword-only convention to boolean inputs on public callables:

-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

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.

gruff-0.0.2-py3-none-win_amd64.whl (1.9 MB view details)

Uploaded Python 3Windows x86-64

gruff-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

gruff-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

gruff-0.0.2-py3-none-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

gruff-0.0.2-py3-none-macosx_10_12_x86_64.whl (1.9 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file gruff-0.0.2-py3-none-win_amd64.whl.

File metadata

  • Download URL: gruff-0.0.2-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

Hashes for gruff-0.0.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 1d23abe143bfe34d7abc1bc03474c993a8399d21fa6ec8466f3a33338f20c02f
MD5 04db98493f54f7b14cba2f806cee1641
BLAKE2b-256 c9f5d895b4382d53df21df5db47ce8c53c37343db3330e4d201a6331d95bf1f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.2-py3-none-win_amd64.whl:

Publisher: release.yml on wkentaro/gruff

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

File details

Details for the file gruff-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for gruff-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 49f951eb682692b17874ac6e959bfb3b1f430db1597f1d6ca010040b68de2c4b
MD5 b6ff01624827924b26bc4eda161ec183
BLAKE2b-256 d4b835145af7c38db8ca278131d986140d3208da8c5cc27188ce24aebd1e2ff0

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on wkentaro/gruff

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

File details

Details for the file gruff-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for gruff-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 e53a93002d3dbd045ad6f12a7793f20d6d1cccd3af8b9f18e548b78ef1b5f0c4
MD5 f3d583828f809b2f2d7ef4e678f119bc
BLAKE2b-256 fb30562d994e5c9a5672b6683f233be90fbdc6fa4e9d6ef3d79c7bf45ce4f9ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on wkentaro/gruff

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

File details

Details for the file gruff-0.0.2-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: gruff-0.0.2-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

Hashes for gruff-0.0.2-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 718be5dc5e1b5c62bacd114b1039756562848e36598f0da434f7fc1088699899
MD5 49ea3fb3cd7fee3b97ffdf90bf46450c
BLAKE2b-256 2d61513fa8ae4fc3d9a090c5b76213f60be27688e04a3dbee76228ccb9a14713

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.2-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on wkentaro/gruff

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

File details

Details for the file gruff-0.0.2-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for gruff-0.0.2-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 ca1babb36a3eff1e6f991d5a4d69dab942effcacbf7dbe8843973a8cbc57b9d7
MD5 89dc344463aa2ff6bd9c32d3010203e8
BLAKE2b-256 947570f1c59df07d3205c88db37e329d01f499056e6963619265fbe2676a1078

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.2-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on wkentaro/gruff

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

Release history Release notifications | RSS feed

0.0.7

5 files

0.0.6

5 files

0.0.5

5 files

0.0.4

5 files

0.0.3

5 files

This release

0.0.2 This release

5 files

0.0.1

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