Skip to main content

oaklint

CI codecov PyPI Python License

oak is an opinionated, agent-first Python linter - a cross between Black and Ruff that enforces a curated set of rules targeting common sources of technical debt. These are the patterns that accumulate quietly, from minor style drift up to real structural problems, and they show up most in agent-written code and junior-developer code. oak is built to be both a linter and a learning tool: every rule explains why it exists, so the code gets fixed and the author learns the reasoning behind the fix. Built in Rust on the rustpython-ruff_python_parser crate, so it parses exactly what a modern Python toolchain does while staying a small standalone binary.

oak runs alongside ruff, black, and whatever else is already in your toolchain rather than replacing any of them. It stays fully compatible and layers its curated rules on top, so you keep your existing formatter and linter and add oak for the checks they do not cover.

Some of these rules are hot takes - deliberately more opinionated than a general-purpose linter would risk. In practice I have found they are what keeps medium-to-large teams and their codebases maintainable as they grow.

The set grows by one rule: if a standard can be systematically deduced from the code - checked mechanically rather than by judgment - it gets added here. Anything that needs human taste to adjudicate stays out.

Installation

oak ships as a prebuilt wheel on PyPI, so it installs with no Rust toolchain:

uv tool install oaklint       # install the oak command globally
uvx oaklint path/to/file.py   # or run it without installing
pip install oaklint           # or with pip

The distribution is named oaklint; the installed command is oak.

Android (Termux) is covered too: an android_21_arm64_v8a wheel is published for CPython that reports the android platform, so uv tool install oaklint there installs the prebuilt binary instead of compiling from source.

Usage

oak path/to/file.py src/            # report violations, exit 1 if any
oak --fix src/                      # rewrite files to resolve fixable violations
oak docs OAK005,OAK012              # print the full reasoning for one or more rules

Rule selection can be set inline without a config file. Each flag takes a comma-separated list and is repeatable:

oak --select OAK005,OAK012 src/     # lint only these, replacing any configured select
oak --select ALL src/               # run every rule
oak --extend-select OAK014 src/     # add a code on top of the active set (config select or default-on)
oak --ignore OAK002 src/            # drop a code, appended to any configured ignore
oak --exclude 'vendor/**' src/      # skip matching paths, appended to any configured exclude

The flags overlay the discovered config: --select replaces its select, --extend-select adds on top, and --ignore/--exclude append to their lists.

Every rule ships with a full documentation page that an agent or a human can read on demand. Running oak docs <codes> prints the complete rationale, Good/Bad examples, and the recommended fix for exactly those rules - so the reader learns why the rule exists and how to apply the change, without leaving the terminal or hunting through the repo.

Output

A run is structured to be read by an agent under a token budget, not to repeat itself once per line:

Run `oak docs OAK007,OAK014,OAK015` for full rule reasoning, or fetch each individually.

  Code    Count  Rule
  OAK007     10  A public function or class is defined below a private function in the same scope.
  OAK014      3  A function returns a fixed-shape dict literal instead of a named type.
  OAK015      5  A function or method name does not lead with an action verb.
  Total      18

OAK007 - Public function or class must be placed above private functions
  tests/conftest.py:106:5: `build_client`
  tests/conftest.py:126:5: `Ledger`

OAK014 - Function returning a record must use a class, not a dict
  tests/conftest.py:69:9

OAK015 - Function or method name must lead with an action verb
  tests/conftest.py:85:5: `gateway`
  tests/conftest.py:89:5: `ledger`

Found 18 violations

The shape is deliberately agent-friendly and context-length-aware:

  • The docs command leads. One line points at the full reasoning for every rule the run hit, and says each can be fetched on its own - the agent pulls the deep explanation only for the rules it decides to act on, instead of paying for it up front.
  • The summary table amortizes the explanation. Each rule's definition and its violation count appear exactly once, so an agent can triage which rules matter before reading a single location.
  • Locations are grouped, not annotated. The per-rule message is stated once as a section header, then followed by bare path:line:column lines - each suffixed with the specific identifier at fault (a function name, import alias, or offending token) when the rule has one. A file that trips one rule a hundred times costs a hundred short lines, not a hundred repetitions of the same sentence and the same oak docs pointer.

This keeps a large run's output roughly proportional to the number of distinct rules plus the number of locations, rather than to the product of the two - so a sweep over a whole codebase stays inside an agent's context window.

Rules

Code Rule
OAK001 Missing blank line after an indented block (if/for/while/with/try/match) or before a continuation (elif/else/except/finally).
OAK002 Missing blank line before a return.
OAK003 Comment must be a sentence-case NOTE/TODO/XXX ending with a period.
OAK004 Continuation line must align under the comment's first word.
OAK005 Import must not use an alias.
OAK006 Only functions may be private; classes and module- or class-level names must be public.
OAK007 Private functions must be placed below all public functions and classes in a scope.
OAK008 Name must not be a single character.
OAK009 Test must assert observable behavior, not mock calls.
OAK010 Mock library must not be used, prefer an in-process fake.
OAK011 pytest.raises must not use match=; assert the full error message.
OAK012 Function returning multiple values must use a class, not a tuple.
OAK013 Empty string must not stand for an absent value; use None.
OAK014 Function returning a record must use a class, not a dict.
OAK015 Function or method name must lead with an action verb.
OAK016 Test must not contain conditional logic (if/elif/else).

OAK001 and OAK002 are fixable with --fix, which inserts the missing blank line. OAK003 through OAK016 are report-only. Each rule has a page in docs/rules/ with its rationale and a good/bad example.

Configuration

oak reads settings from the first of .oak.toml, oak.toml, or [tool.oak] in pyproject.toml found by walking up from the current directory. A pyproject.toml without a [tool.oak] table is skipped and the search continues upward.

[tool.oak]
select = ["OAK001"]              # when set, only these codes lint (prefixes like "OAK" and "ALL" work)
ignore = ["OAK002"]              # removed from the active set after select
exclude = ["tests/**", "vendor"] # globs skipped entirely
action-verbs = ["yeet", "reconcile"] # extra leading verbs OAK015 accepts

[tool.oak.per-file-ignores]
"tests/**" = ["OAK002"]          # codes silenced only for matching files

In a standalone oak.toml the same keys are written at the top level (no [tool.oak] header). Unknown keys are a hard error and keys are kebab-case.

Inline suppression

A comment can silence a violation in place, following ruff's # noqa model:

import numpy as np  # noak                 # silences every oak rule on this line
import numpy as np  # noak: OAK005         # silences only OAK005 on this line
import numpy as np  # noak: OAK005,OAK008  # silences a comma-separated list

A # oak: noqa comment silences a whole file, with the same optional code list:

# oak: noqa               # silences every oak rule in this file
# oak: noqa: OAK005,OAK008  # silences only these codes in this file

A line directive is anchored to the line the violation is reported on, and the keyword reads case-insensitively (# NOAK). A bare directive with no codes blankets its scope; naming codes narrows it to exactly those.

Default rule set

With no select key, oak runs the default-on set: OAK001, OAK002, OAK004, OAK008, OAK012, and OAK013. The remaining rules stay off until you name them in select.

Setting select replaces the default set rather than adding to it, so list every code you want to run, including the default-on ones you want to keep:

[tool.oak]
# NOTE: The default rules plus the record-dict rule.
select = [
    "OAK001",  # Blank line after a block or before a continuation.
    "OAK002",  # Blank line before a return.
    "OAK004",  # Continuation lines align under the comment's first word.
    "OAK008",  # No single-character names.
    "OAK012",  # No tuple returns; use a named type.
    "OAK013",  # No empty string for an absent value; use None.
    "OAK014",  # No record dict returns; use a named type.
]

select = ["ALL"] runs every rule. OAK015 fires against a maintained verb allowlist, so expect to tune it before turning it on broadly.

Development

make check     # fmt --check + clippy -D warnings + tests (the CI gate)
make format    # cargo fmt
make coverage  # per-file source coverage report

make coverage uses Rust's built-in -C instrument-coverage and the system llvm-cov/llvm-profdata, so it needs neither cargo-llvm-cov nor a rustup component. Point it at other target directories or llvm binaries with CARGO_TARGET_DIR, LLVM_COV, and LLVM_PROFDATA, and pass extra flags straight through (make coverage -- --show-missing-lines).

Rules live in src/rules/, one module per code, named oak0NN_<domain>_<thing>.rs. Helpers shared between two codes of the same family sit in src/rules/util/.

A run flows through five stages:

  1. config::Config::discover resolves settings.
  2. discovery finds the .py files and drops excluded ones.
  3. linter::check_source parses each file and runs the rules.
  4. The violations are filtered by select, ignore, and per-file-ignores.
  5. diagnostics::Violation reports them, and linter::apply_fixes rewrites files under --fix.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oaklint-0.1.6.tar.gz (88.7 kB view details)

Uploaded Source

Built Distributions

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

oaklint-0.1.6-py3-none-win_amd64.whl (1.8 MB view details)

Uploaded Python 3Windows x86-64

oaklint-0.1.6-py3-none-musllinux_1_2_x86_64.whl (1.9 MB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

oaklint-0.1.6-py3-none-musllinux_1_2_aarch64.whl (1.8 MB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

oaklint-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

oaklint-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.7 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

oaklint-0.1.6-py3-none-macosx_11_0_arm64.whl (1.7 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

oaklint-0.1.6-py3-none-macosx_10_12_x86_64.whl (1.8 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

oaklint-0.1.6-py3-none-android_21_arm64_v8a.whl (1.7 MB view details)

Uploaded Android API level 21+ ARM64 v8aPython 3

File details

Details for the file oaklint-0.1.6.tar.gz.

File metadata

  • Download URL: oaklint-0.1.6.tar.gz
  • Upload date:
  • Size: 88.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for oaklint-0.1.6.tar.gz
Algorithm Hash digest
SHA256 1593c06a7d831fce1fcc520f82921500ddfa55b179da841062312c9f26b82503
MD5 436d9485c7fd353a6c2aa55265e12e1b
BLAKE2b-256 ea44dcdee090b3e9f6bb84a71df238e9d99bb4c51b19d5c5d297b6d6832e403e

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6.tar.gz:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-win_amd64.whl.

File metadata

  • Download URL: oaklint-0.1.6-py3-none-win_amd64.whl
  • Upload date:
  • Size: 1.8 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 oaklint-0.1.6-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 bffaa8aeb4b6363158c94908453e50ef21e16bda70a74a4d48aac40d50c20809
MD5 9bc8547b82fc1fbe1650b860f2cac704
BLAKE2b-256 aba68a236f97643103de65280c5e52e225ec9e115308351fe5f6f064701a3ad7

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-win_amd64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 d6bb222e0ac78dc9f325df066227a2efa471dc58e26891457c1b4b82789c1cd4
MD5 96e6e33ae1eb6e20d95e48b24ebe7948
BLAKE2b-256 c9caa7f73f0593cfa35adcb9ac3185388af9d6f88227e890410579d5116759be

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-musllinux_1_2_x86_64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 2b021dbd5121d99f577036838db393bd04d2ca4202b95201691049cdd02664c8
MD5 b66d380981ddbcd663b79da5f667054b
BLAKE2b-256 9df06ebf44cd4cd4665053d406d61f90ba3c3ca58d3c8a455dd131ef77ee0e5c

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-musllinux_1_2_aarch64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a10ad5ff62dd1544ae483409b862724411487f18505d6c8df5300605e44af07d
MD5 1fcfa496e24324423c4e00ecb0f1c47e
BLAKE2b-256 cd0a967172295f3995238526e4aa02f15284dd9337ed15c8c8fd32e7e9fa0bb6

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 b43ca359a02c4f7041493e69c345fa2e522f6aff9223a335e53e498518dd4fd2
MD5 0f36627c40bf983821c40eca7eb88735
BLAKE2b-256 0b68d01915577872e3516d6a305e3669d9d10c06e4948f4f57a3a12a4bbc2f33

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 3cf605883867a7f2f9c698c036d2ad5750b23d36986bc36a41a6b8e3fa86128e
MD5 3501cad3b8fb25f7f6240aa8cce226e1
BLAKE2b-256 1a3ee9a3094f22d5f8c07b144ecc5454c2fcf3b8acb7d60385c4b260a7bf62a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 1854b582e5305b94f07a9c197c9c0765287d882c11a668d7bbc0fe332adcd2a6
MD5 c00a1a86c7e576fea91a99f46564d3de
BLAKE2b-256 b9fffc138093b7be8425c5f33eb2614d1ae96346a532c1d010afda5241087b66

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on omaralikhn/oaklint

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

File details

Details for the file oaklint-0.1.6-py3-none-android_21_arm64_v8a.whl.

File metadata

File hashes

Hashes for oaklint-0.1.6-py3-none-android_21_arm64_v8a.whl
Algorithm Hash digest
SHA256 b6bf11ae9301e4aca8c69a8d6a9a2a8419197b1f917a9b89389a064b3ee31d69
MD5 bc97e71f26f3f7f760d6299ed3b1abee
BLAKE2b-256 e81eb8ca5031fa0ce7960113b92a9a44ceca1441b18701f70ca7ba2e4c3b5d87

See more details on using hashes here.

Provenance

The following attestation bundles were made for oaklint-0.1.6-py3-none-android_21_arm64_v8a.whl:

Publisher: release.yml on omaralikhn/oaklint

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page