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.

The first release tests four theses: private inputs are easier to trace when callers name 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.

All rules are opt-in. Teams enable policies one at a time as they decide which opinionated constraints fit their codebase. A check with no enabled rules succeeds but warns that it performed no policy analysis.

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

Rules

keyword-only-private-inputs (GR001)

Flags each fixed caller-supplied input to a private module-level function or method that is positional; 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)
+    return _resize_image(data=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

Suppress a rule on definitions that must follow an external calling convention or intentionally provide a convenience default:

def _format_cost(value: float) -> str:  # noqa: GR001 -- Callable[[float], str]
    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 callers of private callables name every input; FBT001 and FBT002 extend that 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"]

Interface

Gruff will follow Ruff's familiar command and diagnostic conventions:

gruff check .
gruff check --select GR001 .
gruff check --select GR002 .
gruff check --select GR003 .
gruff check --select GR004 .
gruff check --select GR001,GR002,GR003,GR004 .

Lint findings, including invalid Python syntax, will exit with status 1. Configuration, I/O, and internal failures will exit with status 2.

Gruff does not rewrite source code in the first release.

Configuration

Gruff reads configuration only from pyproject.toml:

[tool.gruff.lint]
select = ["GR001", "GR002", "GR003", "GR004"]
ignore = []
per-file-ignores = { "callbacks.py" = ["GR001"] }

Directory discovery checks .py, .pyi, and .pyw files and respects Git ignore files.

Distribution

Public releases will 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.1-py3-none-win_amd64.whl (1.9 MB view details)

Uploaded Python 3Windows x86-64

gruff-0.0.1-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.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

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

Uploaded Python 3macOS 11.0+ ARM64

gruff-0.0.1-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.1-py3-none-win_amd64.whl.

File metadata

  • Download URL: gruff-0.0.1-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.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 c311b6e769cfc0140ba16fa4f81b1f8de5df7e95810310963d660da8d9aaf80f
MD5 c63d69a8caf5bceaa00f7f4345cb8a8d
BLAKE2b-256 5a6187a70f844ecc108672d40925619b934e980422cc54dd97d279cbf3024a54

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.1-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.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for gruff-0.0.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 fae0bda2f2545f4ba70ccd8d639e1ed785f5b0c07ddfd00b6647c789d54b51ea
MD5 27a82e940cc2a4107592a03bfc8fe1a1
BLAKE2b-256 5c4fc2b5201da53d9001e5770a91829ba8ec3d4de13e1e6c3698eb9c4e216900

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.1-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.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for gruff-0.0.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 08d371a6742e29e0960beda2183b4eaf25dd991cebd1c1b736b52fc3b846ee7d
MD5 5148b8d04a940cbdb1ca1d8d9222d3c5
BLAKE2b-256 bb6151e26afcb4b0bfe08f27a38acf50b85856b688b7780c3166cc1098fcfc7d

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.1-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.1-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: gruff-0.0.1-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.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1b6297a9064955c0a7c89ed3f33d9f80cdd76402c496c696246e04c67238436c
MD5 3940ab1d74e64e52c58020595f5c4e83
BLAKE2b-256 172d4763fde8f9ea906c8e2865132a94c88186d85420bffa0b65d4a7f44f385d

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.1-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.1-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for gruff-0.0.1-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 7131801ed8e9ba55419c8ea3177d70fe53831316ca4d1fc074b633f10d20b682
MD5 11b4e477c0eb6a17aa1e9807ce609577
BLAKE2b-256 e4ff511d1514b841a5cc0a016ec3af7cada243225c684f567dcf9d41c91fc029

See more details on using hashes here.

Provenance

The following attestation bundles were made for gruff-0.0.1-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

0.0.2

5 files

This release

0.0.1 This release

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