Skip to main content

CI PyPI

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

no_defaults-1.1.0.tar.gz (36.0 kB view details)

Uploaded Source

Built Distributions

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

no_defaults-1.1.0-py3-none-win_amd64.whl (1.9 MB view details)

Uploaded Python 3Windows x86-64

no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded Python 3manylinux: glibc 2.34+ x86-64

no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl (2.0 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

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

Hashes for no_defaults-1.1.0.tar.gz
Algorithm Hash digest
SHA256 23ab905c4072f5f50f0fc53bdda52e6de474ec008cd0e363926e890fa9aca6d7
MD5 e230cdaed276fc6d2bbce81eea340897
BLAKE2b-256 2f76f0ce68ec1aa03435607158a77f4325184c06cfb783a9a55cf921992e42ba

See more details on using hashes here.

Provenance

The following attestation bundles were made for no_defaults-1.1.0.tar.gz:

Publisher: release.yml on adamtheturtle/no-defaults

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

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

Hashes for no_defaults-1.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 01ad0f42b6291b9659dd7d4b66618815c45ff0d254dbeb2b7d82b913de25ba5e
MD5 4b7d39277e4e014da55a1ca1fba9e0e5
BLAKE2b-256 ed80a52ae7dcccc1670fe4fcb200c697350475ad8c5cf91ca1bdef8e7c3c20cd

See more details on using hashes here.

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

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

File details

Details for the file no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for no_defaults-1.1.0-py3-none-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 c65fc11b3dcab89ce6a3b707cc80d1b9151a65b5dde10facc96b9ed6cf0e890d
MD5 752399898080b4f33503531f56f5c871
BLAKE2b-256 4384d0b7b68622cb222748a482e08515bf89bca39677dee7591515503af818b3

See more details on using hashes here.

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

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

File details

Details for the file no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for no_defaults-1.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 510a888480e95116e519d99ff6a8288003e589a7d91e3c8186bdd4c5c56c2de2
MD5 53c11ded4bbe8e01a82164f842a7567f
BLAKE2b-256 b6503f38bde7585072cf13d3823b169d932961ea044bc04b46a46047db0c765f

See more details on using hashes here.

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

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