actually
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 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:
Note: Rule codes, names, and groups are NOT stable while
actuallyis pre-1.0 — the taxonomy is still being discovered, so any of them may change with breaking changes at will (ADR 11). Pin a version; do not depend on a specific code or name holding across releases.
actually-chains
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTH001 | multi-line-chain | unstable | partial | a chain of two or more invocations not one call per line under a # well-actually: multi-line anchor |
actually-completion-clauses
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTE001 | no-try-else | stable | partial | else on try — dedent the continuation after the except clauses |
| ACTE002 | no-for-else | stable | no | else on for — the loop-completion clause overloads else |
| ACTE003 | no-while-else | stable | no | else on while — the loop-completion clause overloads else |
actually-if-conditions
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTI001 | no-if-else | stable | no | else on an if — flatten to guard clauses with early exits |
| ACTI002 | no-elif | stable | no | elif — flatten to guard clauses with early exits |
| ACTI003 | prefer-match | unstable | no | two or more consecutive conditional returns comparing one shared subject, closed by a terminal return or raise — dispatch written as control flow |
actually-literals
| 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 |
actually-returns
| 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-ternaries
| Code | Rule | Status | Auto-fix | What it enforces |
|---|---|---|---|---|
| ACTT001 | ternary-not-nested | stable | no | a ternary inside another ternary's arm (elif in expression form) |
| ACTT002 | ternary-not-empty | unstable | no | a degenerate ternary arm (None, "", empty container) — conditional inclusion in disguise |
Standing on Ruff and Ty
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 check under the
recommended configuration is a no-op. ruff format is not guaranteed one: an inserted
trailing comma is exactly the magic trailing comma ruff format expands, so a literal
actually fixed without exploding still reformats. Run actually format before
ruff format and let ruff own the final layout. well-actually never runs ruff or ty
itself — it is its own tool with neither as a dependency; pair them in your own pipeline,
in that order. This repo gates itself on both toolchains plus its own linter on every
commit, which is the guarantee exercised live.
Usage
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
- rewrites chains of two or more invocations to one call per line, anchored with
# well-actually: multi-lineon the base-receiver line soruff formatcannot re-join them, parenthesizing a short chain's base receiver (ADR 9), and strips the anchor when its chain shrinks below two invocations — chains carrying foreign comments or multiline arguments are reported for human layout instead --only-autofixablemakes it best effort: every available fix is applied, the remaining violations are still reported, and the exit code stays 0
Configuration
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.
check --help and format --help are rendered against that same selection: the help names
exactly the rules the invocation will enforce — split for format by what it can rewrite —
never overselling nor underselling the changeset, whatever order the selection flags and
--help are typed in (ADR 8).
Entries are rule codes (ACTT002), group prefixes (ACTI), 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 = ["ACTT002"]
or a group off with one member kept:
exclude = ["ACTL"]
include = ["__ALL__", "ACTL001"]
Hard errors instead of silent tolerance: an unknown selector, any selector appearing more
than once across the two 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.
CI Reports
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.json
GitHub 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.
Example
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)
Development
uv sync
mise install
hk install
uv run pytest
README.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.
tests/valid-code-checks/allowed/ holds the valid-case corpora,
one directory per rule selector (ACTH001/, ACTI/, __ALL__/ — any selector
ADR 6 registers): real Python files the
checker MUST stay silent on and format MUST leave byte-identical under exactly that selection.
A per-rule directory pins its rule's allowed shapes atomically, against that rule alone; __ALL__/ pins the
composed behaviour of the whole rule set in its fixed fixer order
(ADR 10). The test module beside the
corpora globs the directories, so pinning a new allowed shape is adding a file — no test wiring.
Mutate only via mise run format-valid-cases.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 well_actually-0.8.0.tar.gz.
File metadata
- Download URL: well_actually-0.8.0.tar.gz
- Upload date:
- Size: 30.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6dff62391e35d45500fea466a57ddc8533e8be7c7455d915b270766931b1bc0e
|
|
| MD5 |
8714c6fd203b206c2143126e7798a94b
|
|
| BLAKE2b-256 |
2485ef1e76dd7746374ee15b6ce037dcce8fe26d28d4ceab975c13c85376d005
|
File details
Details for the file well_actually-0.8.0-py3-none-any.whl.
File metadata
- Download URL: well_actually-0.8.0-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b12ce5784c5e5e37944badd7479104a4c4100fa18fc2ada1b49f33458ec8ce4
|
|
| MD5 |
f65f24ac45a06ae2a896adbe902f4121
|
|
| BLAKE2b-256 |
2009286f28190b49c5ea7024ad168917d5077b106bf3e884e910e7c277582063
|