no-defaults
A fast, standalone Python linter that forbids defaults in function signatures and dataclasses. It is implemented in Rust and parses Python with Ruff's parser.
from dataclasses import dataclass, field
def connect(timeout=30): # NOD001
pass
@dataclass
class Job:
retries: int = 3 # NOD001
tags: list[str] = field(default_factory=list) # NOD001
Installation
uv tool install no-defaults
Usage
no-defaults .
no-defaults --fix .
no-defaults --diff .
no-defaults --private-only src tests
no-defaults --output-format json .
no-defaults --output-format github .
no-defaults --show-settings src/package/api.py
Exit status is 0 when clean, 1 when violations are found, and 2 for an operational error.
Directories are walked in parallel, respecting .gitignore and standard hidden-file filters.
Diagnostics use Ruff's concise format and include a summary:
src/example.py:4:17: NOD001 parameter `timeout` of function `connect` has a default
Found 1 error.
Pass --fix to remove defaults automatically. Function parameters and ordinary dataclass assignments become required. For field(...), only the positional default, default=, or default_factory= argument is removed; other metadata is preserved:
retries: int = field(default=3, kw_only=True)
# becomes
retries: int = field(kw_only=True)
After a successful fix, the command exits with status 0 and prints a Ruff-style summary such as Found 2 errors (2 fixed, 0 remaining). Writes use an atomic same-directory replacement. --diff prints a unified diff, writes nothing, and exits with status 1 when changes are available.
--fix does not update call sites
--fix rewrites signatures and nothing else. Removing a default makes that argument required, so every caller that omitted it now raises TypeError at runtime:
def connect(timeout=30): # becomes def connect(timeout):
...
connect() # TypeError: connect() missing 1 required positional argument
The same applies to dataclass fields, which become required at construction. The fixed code still imports and still lints clean, so a warning is printed after fixing and your test suite is what confirms the result.
Rewriting call sites would mean resolving every call to its definition across the whole project, which this per-file design deliberately avoids. It would not be sufficient either: for a function that is part of your public API, the callers that break are in other people's code.
--fix is therefore safest under private_only = true, where the symbols it touches have no callers outside the project.
The default full output includes source excerpts and carets. concise emits one diagnostic per line, json emits a machine-readable array, and github emits workflow commands for GitHub Actions annotations.
The linter detects defaults on positional-only, positional-or-keyword, and keyword-only parameters.
For classes decorated with @dataclass or @dataclasses.dataclass, it detects assigned defaults plus field(default=...) and field(default_factory=...) in the class body.
ClassVar assignments are ignored because they are not dataclass fields, whether the annotation is bare, qualified, or quoted as in x: "ClassVar[int]" = 1. Annotated assignments inside method bodies are ignored because they are locals.
Suppress an individual violation with either a blanket # noqa or the rule-specific # noqa: NOD001 on the line containing the default:
def compatible(timeout=30): # noqa: NOD001
pass
A directive on the line holding def covers every parameter of that signature, so a multi-line signature needs one directive rather than one per parameter:
def compatible( # noqa: NOD001
timeout=30,
retries=3,
):
pass
A directive on the class line does the same for every field of a dataclass:
@dataclass
class Job: # noqa: NOD001
retries: int = 3
tags: list[str] = field(default_factory=list)
Decorators do not move either line, and the scope stops at the signature or the class body: methods, nested functions, and nested dataclasses keep their own violations and need their own directives. A directive placed elsewhere in the signature, such as on the closing parenthesis, still applies only to its own line.
Suppress the rule for an entire file with # ruff: noqa or # ruff: noqa: NOD001.
A directive that names NOD001 without suppressing anything is reported as NOD002 and removed by --fix:
src/example.py:1:21: NOD002 unused `noqa` directive for `NOD001`
Only directives that name the code are checked. A blanket # noqa may exist for another linter, so it is never reported, and a blanket # ruff: noqa or # flake8: noqa silences every rule in the file, including this one. When --fix removes the last code from a directive, it removes the whole comment; otherwise it removes just NOD001 from the list.
Using suppressions alongside Ruff
NOD001 is not a Ruff rule, so Ruff reports every # noqa: NOD001 as RUF102 Invalid rule code.
That diagnostic is fixable, which means ruff check --fix deletes the suppression comment and leaves the violation behind for no-defaults to report.
Register the prefix as an external code so Ruff leaves the suppressions alone:
[tool.ruff]
lint.external = [ "NOD" ]
Configuration
Configuration lives in pyproject.toml:
[tool.no_defaults]
private_only = true
[tool.no_defaults.per_file_enforcement]
"tests/**" = "all"
"src/**" = "private"
Private means a name that starts with one underscore. In private-only mode, the rule applies to private modules and packages, private functions and methods, all members of private classes, and private dataclass fields. For example, all defaults in _module.py and _package/module.py are checked. Dunder names such as __init__.py are not considered private by themselves.
Private modules that are re-exported publicly
Privacy is decided from module and symbol names alone. A function defined in _upload.py counts as private even when the package's __init__.py re-exports it, whether through __all__ or a plain import. Under private_only = true it is still checked, although its defaults are part of the public API, where removing one is a breaking change for callers.
This is deliberate. no-defaults checks each file on its own, which is what lets it resolve configuration per file and stay fast when pre-commit passes only the changed files. It never reads __init__.py to work out which names a private module re-exports, so it cannot tell an internal helper from a re-exported one.
Either exempt the module in configuration:
[tool.no_defaults.per_file_enforcement]
"src/package/_upload.py" = "none"
or suppress the rule on the signatures that are public in practice:
def upload(
*,
strategy: Strategy = Strategy.DIFF, # noqa: NOD001
) -> None:
"""Re-exported from the package root, so the default is public API."""
per_file_enforcement accepts Ruff-style glob patterns relative to the directory containing pyproject.toml. Use "all" to reject every default in matching files, "private" to reject defaults only in private scopes, or "none" to exempt matching files from the rule. "none" also wins over --private-only, so an exempt file stays exempt. Patterns without a slash match file names at any depth. An initial ! negates a pattern. If multiple patterns match, the most specific pattern wins; equally specific patterns are resolved lexicographically so results never depend on TOML table order.
The --private-only CLI flag overrides the configuration for every checked file.
Like Ruff, no-defaults discovers the closest pyproject.toml containing [tool.no_defaults] separately for each file. This supports monorepos with nested configuration; files without a local table continue searching parent directories.
Performance
An optimized 1.0.0 development build checked a pinned Typeshed checkout containing 5,368 Python and stub files (12.5 MiB) in a median 0.29 seconds across five warm runs on an Apple Silicon Mac, or roughly 18,000 files per second. It produced 50,974 diagnostics; an earlier full-output measurement used approximately 41 MiB maximum RSS.
CodSpeed runs parser-and-rule benchmarks for representative modules on every pull request and every push to main, providing stable comparisons against the default-branch baseline. The scheduled Typeshed benchmark remains as a real-project correctness and gross-regression check.
pre-commit
repos:
- repo: https://github.com/adamtheturtle/no-defaults
rev: v1.1.0
hooks:
- id: no-defaults
License
MIT
See CONTRIBUTING.md, SECURITY.md, and CHANGELOG.md.
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 no_defaults-1.1.0.tar.gz.
File metadata
- Download URL: no_defaults-1.1.0.tar.gz
- Upload date:
- Size: 36.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23ab905c4072f5f50f0fc53bdda52e6de474ec008cd0e363926e890fa9aca6d7
|
|
| MD5 |
e230cdaed276fc6d2bbce81eea340897
|
|
| BLAKE2b-256 |
2f76f0ce68ec1aa03435607158a77f4325184c06cfb783a9a55cf921992e42ba
|
Provenance
The following attestation bundles were made for no_defaults-1.1.0.tar.gz:
Publisher:
release.yml on adamtheturtle/no-defaults
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
no_defaults-1.1.0.tar.gz -
Subject digest:
23ab905c4072f5f50f0fc53bdda52e6de474ec008cd0e363926e890fa9aca6d7 - Sigstore transparency entry: 2356920064
- Sigstore integration time:
-
Permalink:
adamtheturtle/no-defaults@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/adamtheturtle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Trigger Event:
push
-
Statement type:
File details
Details for the file no_defaults-1.1.0-py3-none-win_amd64.whl.
File metadata
- Download URL: no_defaults-1.1.0-py3-none-win_amd64.whl
- Upload date:
- Size: 1.9 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 |
01ad0f42b6291b9659dd7d4b66618815c45ff0d254dbeb2b7d82b913de25ba5e
|
|
| MD5 |
4b7d39277e4e014da55a1ca1fba9e0e5
|
|
| BLAKE2b-256 |
ed80a52ae7dcccc1670fe4fcb200c697350475ad8c5cf91ca1bdef8e7c3c20cd
|
Provenance
The following attestation bundles were made for no_defaults-1.1.0-py3-none-win_amd64.whl:
Publisher:
release.yml on adamtheturtle/no-defaults
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
no_defaults-1.1.0-py3-none-win_amd64.whl -
Subject digest:
01ad0f42b6291b9659dd7d4b66618815c45ff0d254dbeb2b7d82b913de25ba5e - Sigstore transparency entry: 2356920278
- Sigstore integration time:
-
Permalink:
adamtheturtle/no-defaults@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/adamtheturtle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Trigger Event:
push
-
Statement type:
File details
Details for the file no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c65fc11b3dcab89ce6a3b707cc80d1b9151a65b5dde10facc96b9ed6cf0e890d
|
|
| MD5 |
752399898080b4f33503531f56f5c871
|
|
| BLAKE2b-256 |
4384d0b7b68622cb222748a482e08515bf89bca39677dee7591515503af818b3
|
Provenance
The following attestation bundles were made for no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on adamtheturtle/no-defaults
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl -
Subject digest:
c65fc11b3dcab89ce6a3b707cc80d1b9151a65b5dde10facc96b9ed6cf0e890d - Sigstore transparency entry: 2356920433
- Sigstore integration time:
-
Permalink:
adamtheturtle/no-defaults@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/adamtheturtle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Trigger Event:
push
-
Statement type:
File details
Details for the file no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.0 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 |
510a888480e95116e519d99ff6a8288003e589a7d91e3c8186bdd4c5c56c2de2
|
|
| MD5 |
53c11ded4bbe8e01a82164f842a7567f
|
|
| BLAKE2b-256 |
b6503f38bde7585072cf13d3823b169d932961ea044bc04b46a46047db0c765f
|
Provenance
The following attestation bundles were made for no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on adamtheturtle/no-defaults
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
510a888480e95116e519d99ff6a8288003e589a7d91e3c8186bdd4c5c56c2de2 - Sigstore transparency entry: 2356920526
- Sigstore integration time:
-
Permalink:
adamtheturtle/no-defaults@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/adamtheturtle
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0f0f7213b5391e04ab160e4b54938df5f0932238 -
Trigger Event:
push
-
Statement type: