Skip to main content

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 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:

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-conditionals

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 — flatten to guard clauses with early exits
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
ACTC005 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

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/else completion clause into straight-line code when every except body 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-line on the base-receiver line so ruff format cannot 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-autofixable makes 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 (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, 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/, ACTC/, __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

well_actually-0.7.0.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

well_actually-0.7.0-py3-none-any.whl (34.9 kB view details)

Uploaded Python 3

File details

Details for the file well_actually-0.7.0.tar.gz.

File metadata

  • Download URL: well_actually-0.7.0.tar.gz
  • Upload date:
  • Size: 28.3 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

Hashes for well_actually-0.7.0.tar.gz
Algorithm Hash digest
SHA256 9148edf02df8ed96b4ada92b59ed3ed650d97722e3a00d3686ada8ff22f14121
MD5 f08c5e2df930d87f416a40cd56aea50b
BLAKE2b-256 2c7a3f6f407460a22535ce6a81e12cca1e3f717d9d60ae1712bf0a2d2b1a6dc8

See more details on using hashes here.

File details

Details for the file well_actually-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: well_actually-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 34.9 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

Hashes for well_actually-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8fad0f23eeecd72021585e3aee84641fb45fd953f793b8476f61d4486b7415c5
MD5 0b869fcfebcd82361db70f76002c2db3
BLAKE2b-256 38a7081e5391d1c0f8581e5351538c9f215ad8b8d98dcdedee7f5ffc62cbfe79

See more details on using hashes here.

Release history Release notifications | RSS feed

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.3

2 files

0.2.2

2 files

0.1.0

2 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