oaklint
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.
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:columnlines - 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 sameoak docspointer.
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:
config::Config::discoverresolves settings.discoveryfinds the.pyfiles and drops excluded ones.linter::check_sourceparses each file and runs the rules.- The violations are filtered by
select,ignore, andper-file-ignores. diagnostics::Violationreports them, andlinter::apply_fixesrewrites 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
Built Distributions
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 oaklint-0.1.5.tar.gz.
File metadata
- Download URL: oaklint-0.1.5.tar.gz
- Upload date:
- Size: 88.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a99e7566db7729ca18492ddbdddf258f0dcc18fadde2b54b08f416959469fc0
|
|
| MD5 |
7f6dc6ecd57d4fb093a560b079f4afcb
|
|
| BLAKE2b-256 |
f0deafc80dc6dae72958438275418553c757f1eb1e0db804c1c7dbe6cdea5315
|
Provenance
The following attestation bundles were made for oaklint-0.1.5.tar.gz:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5.tar.gz -
Subject digest:
2a99e7566db7729ca18492ddbdddf258f0dcc18fadde2b54b08f416959469fc0 - Sigstore transparency entry: 2338220330
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-win_amd64.whl.
File metadata
- Download URL: oaklint-0.1.5-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94083ba75b89379efef1068e8cd4bda7627d030315eaa52f873a8ff66879d989
|
|
| MD5 |
85931c9073460939ddf4742f4c40fd54
|
|
| BLAKE2b-256 |
f0f8e22cec5391514873fbb0e182521a2e790c3cdbefc9bd8876e03fefeeea0c
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-win_amd64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-win_amd64.whl -
Subject digest:
94083ba75b89379efef1068e8cd4bda7627d030315eaa52f873a8ff66879d989 - Sigstore transparency entry: 2338220359
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
596e2412447004710f337554c34c6ac4784091664e59ed4e2189b5669990c365
|
|
| MD5 |
2da4334d659d4e743ef41d5dba58e3b4
|
|
| BLAKE2b-256 |
f606931f58cefebe64f81b1db7f8a42cf731c50200e8425b037b497d57c8e90d
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-musllinux_1_2_x86_64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-musllinux_1_2_x86_64.whl -
Subject digest:
596e2412447004710f337554c34c6ac4784091664e59ed4e2189b5669990c365 - Sigstore transparency entry: 2338220336
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a93e681ff315d9b03b5427ee52f189e19b34a125edbeb1c6fe472a6e126f67d3
|
|
| MD5 |
c1482fe12ab1279b11b63e9fd6269faa
|
|
| BLAKE2b-256 |
ffd40320f8132b7eff1b4ab8d952f09c2f5dd4776753b42ceaff1ee84df3f23f
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-musllinux_1_2_aarch64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-musllinux_1_2_aarch64.whl -
Subject digest:
a93e681ff315d9b03b5427ee52f189e19b34a125edbeb1c6fe472a6e126f67d3 - Sigstore transparency entry: 2338220381
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78d0d82e0306d23d5c94b079cfe698b56a12df2c03c0dff6bfb48d42d5d59568
|
|
| MD5 |
a7cc395178a7aeec167227fb2343178f
|
|
| BLAKE2b-256 |
9717925ad67c80a3355a57efabdba6632a9b64aa0ff2817895da23507533d48c
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
78d0d82e0306d23d5c94b079cfe698b56a12df2c03c0dff6bfb48d42d5d59568 - Sigstore transparency entry: 2338220351
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 1.7 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
426408dcb3c5a66de6ff1cca196c1ddae5c69af5977acd729c5472e254da05e2
|
|
| MD5 |
cf34e50576ce11480d9484242bcfa417
|
|
| BLAKE2b-256 |
b33df8dc4aaaa4477d3ba2a675f467a61814465860cf39e8d1114961bca070db
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
426408dcb3c5a66de6ff1cca196c1ddae5c69af5977acd729c5472e254da05e2 - Sigstore transparency entry: 2338220366
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.7 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a2fecf632dec83a81730163fe9da88b91331f4aac7a1cbab1d40586f18d728f
|
|
| MD5 |
228038e063c70d34d17e3f8479724fc5
|
|
| BLAKE2b-256 |
12e99fc291fb41ae00ff57b9da480425bf27349a1b0c012393ac81704a9d80d2
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-macosx_11_0_arm64.whl -
Subject digest:
3a2fecf632dec83a81730163fe9da88b91331f4aac7a1cbab1d40586f18d728f - Sigstore transparency entry: 2338220371
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file oaklint-0.1.5-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: oaklint-0.1.5-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f99e53e0b8dfb57962803d752fd06a1a32f1b0a9c6d6a0254235d0a5584ce988
|
|
| MD5 |
e41e326b396e4c6eaf8835c0555911bf
|
|
| BLAKE2b-256 |
99f55170db5f1634974bc5b31fb3c558004ed066fb29e3bc7b4f41d39a5f9c50
|
Provenance
The following attestation bundles were made for oaklint-0.1.5-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on omaralikhn/oaklint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oaklint-0.1.5-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
f99e53e0b8dfb57962803d752fd06a1a32f1b0a9c6d6a0254235d0a5584ce988 - Sigstore transparency entry: 2338220346
- Sigstore integration time:
-
Permalink:
omaralikhn/oaklint@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Branch / Tag:
refs/tags/v0.1.5 - Owner: https://github.com/omaralikhn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5760c49cf520ff93cf365af4fac47520d8576fd4 -
Trigger Event:
push
-
Statement type: