Well, actually, your code should read like this.
actually is a highly opinionated Python linter and formatter built on
ast-grep. It enforces a guard-clause style through rules with
ruff-style stable codes, grouped by language construct
(ADR 1). Every rule is checkable;
the auto-fix column marks what format can rewrite. Each rule links to its documentation page
with rationale and a banned/wanted example pair — generated from
rules.toml and validated by the linter itself
(ADR 2). actually rules --list
prints the same catalog in the terminal, docs links included:
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTC001 | no-else | stable | partial | else on if, and the completion clauses on for, while, try |
| ACTC002 | no-elif | stable | no | elif — use guard clauses or a dispatch table |
| ACTC003 | ternary-not-nested | stable | no | a ternary inside another ternary's arm (elif in expression form) |
| ACTC004 | ternary-not-empty | unstable | no | a degenerate ternary arm (None, "", empty container) — conditional inclusion in disguise |
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTL001 | trailing-comma | unstable | yes | a dict/list/set literal whose last element lacks a trailing comma |
| ACTL002 | one-element-per-line | unstable | partial | a dict/list/set literal with elements sharing a line with a bracket or each other |
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTR001 | blank-before-return | stable | yes | a return stacked directly under other statements in its block |
| ACTR002 | blank-after-return | stable | yes | code directly under a return line |
actually deliberately covers only what ruff and
ty cannot express — they do most of the lifting; adopt
them first. Our recommended configurations are this repo's own ruff.toml and
ty.toml. actually's opinionation starts where those stop, and it MANDATES
compatibility of its own output: no actually rule demands, and no actually format fix
produces, code those rule sets reject — after actually format, a ruff format under the
recommended configuration is a no-op. well-actually never runs ruff or ty itself — it is
its own tool with neither as a dependency; pair them in your own pipeline. This repo gates
itself on both toolchains plus its own linter on every commit, which is the guarantee
exercised live.
uvx well-actually@latest check .
uvx well-actually@latest format .The @latest matters: a bare uvx well-actually reuses a cached tool environment and can
silently run an outdated version; @latest re-resolves against the index every time.
Installed (uv tool install well-actually), the short command is available too:
actually check .
actually format .check reports violations and exits non-zero when it finds any. Both commands lint .py
files only; directory scans skip environment, cache, and VCS directories (.venv, venv,
.git, __pycache__, node_modules, and friends) and respect .gitignore files — nested
ones and negations included, matched via pathspec
(black's approach), so no git installation is required. When a .git directory is found
above the scanned path, .gitignore files up to that repo root apply as well. Global
excludes (core.excludesFile, .git/info/exclude) are not consulted. A .py file passed
explicitly is always linted.
format rewrites files in place, then reports what it could not fix:
- inserts the missing blank lines around
return - dedents a
try/except/elsecompletion clause into straight-line code when everyexceptbody already exits (return,raise,continue,break) — when one falls through, the rewrite would change behaviour, so it is reported for human refactoring instead - rewrites dict/list/set literals to one element per line with a trailing comma — literals carrying comments or multiline elements are reported for human formatting instead
--only-autofixablemakes it best effort: every available fix is applied, the remaining violations are still reported, and the exit code stays 0
Select the rule subset in a well-actually.toml (sourced from the current working directory
only — never a parent) or with repeatable --include / --exclude options, which override
the file's corresponding list
(ADR 5). Every invocation
declares its selection on stderr — Found well-actually.toml. Running with: … or
No well-actually.toml found, running with default '__ALL__' — so the active subset is
never a matter of guessing.
Entries are rule codes (ACTC004), group prefixes (ACTC), or __ALL__ — the special
all-encompassing group (ADR 6). The
longest match per rule wins; ties go to exclude. include defaults to __ALL__, so
exclude-only configs just work:
exclude = ["ACTL"]Any subset is expressible — one rule only:
exclude = ["__ALL__"]
include = ["ACTC004"]or a group off with one member kept:
exclude = ["ACTL"]
include = ["__ALL__", "ACTL001"]Hard errors instead of silent tolerance: an unknown selector, __ALL__ appearing more than
once across both lists, exclude = ["__ALL__"] without any include entry, and a selection
that enables no rules. format obeys the selection — a disabled rule neither reports nor
fixes.
check and format emit machine-readable reports via --output-format
(text/gitlab/github/sarif) and --output-file
(ADR 7). GitLab code quality:
actually:
script:
- uvx well-actually@latest check --output-format=gitlab --output-file=gl-code-quality-report.json .
artifacts:
when: always
reports:
codequality: gl-code-quality-report.jsonGitHub inline annotations need no upload — --output-format=github prints workflow commands;
--output-format=sarif produces SARIF 2.1.0 for GitHub code scanning or any SARIF consumer.
def describe_config(path):
try:
config = parse_json_file(path)
except ParseError:
return "invalid config"
else:
return describe(config)actually format rewrites this to:
def describe_config(path):
try:
config = parse_json_file(path)
except ParseError:
return "invalid config"
return describe(config)uv sync
mise install
hk install
uv run pytestREADME.md and rules/*.md are generated from README.template.md and
src/actually/rules.toml by scripts/generate_docs.py; an hk pre-commit hook regenerates
and stages them. Edit the sources, never the outputs.