Skip to main content

python-naming-linter

A naming convention linter for Python projects. Define custom naming rules and enforce them with a single CLI command.

What It Does

  • Define naming rules for variables, functions, classes, modules, and packages
  • Apply rules to specific modules using pattern matching
  • Integrate into CI or pre-commit to keep your naming conventions consistent

For Python developers who want to enforce team-specific naming conventions beyond what PEP 8 and ruff cover.

Installation

pip install python-naming-linter

Or with uv:

uv add python-naming-linter

Quick Start

Create .python-naming-linter.yaml in your project root:

rules:
  - name: bool-method-prefix
    description: Bool-returning functions must start with is_, has_, or should_
    type: function
    filter: { return_type: bool }
    naming: { prefix: [is_, has_, should_] }

  - name: exception-naming
    description: "Exception classes must follow the <Noun><Reason>Error pattern"
    type: class
    filter: { base_class: Exception }
    naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }

apply:
  - name: all
    rules: [bool-method-prefix, exception-naming]
    modules: "**"

Run:

pnl check

Output:

src/domain/service.py:12
    [bool-method-prefix] Bool-returning functions must start with is_, has_, or should_
    validate (expected prefix: is_ | has_ | should_)

src/domain/exceptions.py:8
    [exception-naming] Exception classes must follow the <Noun><Reason>Error pattern
    FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 2 violation(s).

Examples

Variable Naming — Match Type Annotation

Enforce that variable names match their type annotation in snake_case:

rules:
  - name: attribute-matches-type
    description: Attribute names must match their type annotation in snake_case
    type: variable
    filter: { target: attribute }
    naming: { source: type_annotation, transform: snake_case }

apply:
  - name: domain-layer
    rules: [attribute-matches-type]
    modules: contexts.*.domain

This catches repo: SubscriptionRepository (should be subscription_repository).

The {prefix}_{expected} form is also allowed — source_object_context: ObjectContext passes because it ends with _object_context.

Module Naming — Match Class Name

Enforce that module filenames match the primary class they contain:

rules:
  - name: domain-module-naming
    description: Module filename must match the primary class it contains
    type: module
    naming: { source: class_name, transform: snake_case }

apply:
  - name: domain-layer
    rules: [domain-module-naming]
    modules: contexts.*.domain

A file custom.py containing class CustomObject is a violation — it should be custom_object.py.

By default the filename must match the class name exactly (match: exact). Use match: suffix when the class name carries a leading qualifier that the filename drops — for example an outbound adapter whose class is PostgresOrganizationRepository but whose file is organization_repository.py:

rules:
  - name: outbound-module-naming
    description: Outbound adapter filename is the port name; the technology lives in the directory
    type: module
    naming:
      source: class_name
      transform: snake_case
      match: suffix
      strip_prefix: parent_dir

apply:
  - name: outbound-adapters
    rules: [outbound-module-naming]
    modules: contexts.*.adapters.outbound

match: suffix requires the filename to be a trailing token-slice of the class name. strip_prefix: parent_dir additionally requires the dropped leading tokens (the qualifier) to equal the immediate parent directory, so the qualifier is carried by the directory rather than the filename. Thus postgres/organization_repository.py passes, while postgres/postgres_organization_repository.py (qualifier duplicated in the filename) and property_sync_fetcher/google_sheet.py (filename is a prefix, not a suffix) are violations. Compound qualifiers compare with separators dropped, so a bigquery/ directory matches a BigQuery… class prefix.

Combining Rules Per Layer

Apply different rules to different parts of your codebase:

rules:
  - name: attribute-matches-type
    description: Attribute names must match their type annotation in snake_case
    type: variable
    filter: { target: attribute }
    naming: { source: type_annotation, transform: snake_case }

  - name: bool-method-prefix
    description: Bool-returning functions must start with is_, has_, or should_
    type: function
    filter: { return_type: bool }
    naming: { prefix: [is_, has_, should_] }

  - name: exception-naming
    description: "Exception classes must follow the <Noun><Reason>Error pattern"
    type: class
    filter: { base_class: Exception }
    naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }

  - name: domain-module-naming
    description: Module filename must match the primary class it contains
    type: module
    naming: { source: class_name, transform: snake_case }

  - name: constant-upper-case
    description: Module-level constants must be UPPER_CASE
    type: variable
    filter: { target: constant }
    naming: { case: UPPER_CASE }

apply:
  - name: domain-layer
    rules:
      - attribute-matches-type
      - bool-method-prefix
      - domain-module-naming
      - constant-upper-case
    modules: contexts.*.domain

  - name: global-exceptions
    rules: [exception-naming]
    modules: "**"

Configuration

Description

Each rule supports an optional description field. When set, the description is displayed in violation output above the violation detail line, making it easier to understand why the rule exists.

rules:
  - name: bool-method-prefix
    description: Bool-returning functions must start with is_, has_, or should_
    type: function
    filter: { return_type: bool }
    naming: { prefix: [is_, has_, should_] }

Rule Types

Type Target
variable Variable names (attribute, parameter, local_variable, constant)
function Function/method names
class Class names (including exceptions)
module Module (file) names
package Package (directory) names

Filter

Each rule can narrow its scope with type-specific filters:

Type Filter Example Values
variable target attribute, parameter, local_variable, constant
function target method, function
function return_type bool
function decorator staticmethod
class base_class Exception
class decorator dataclass

Naming Constraints

Field Description Example
prefix Name must start with one of the listed prefixes [is_, has_]
suffix Name must end with one of the listed suffixes [Repository, Service]
regex Name must match a regular expression "^[A-Z][a-zA-Z]+Error$"
source + transform Name must be derived from another element source: type_annotation, transform: snake_case
match How a source: class_name module name is compared — exact (default) or suffix match: suffix
strip_prefix With match: suffix, the dropped qualifier must equal the immediate parent directory strip_prefix: parent_dir
case Name must follow a casing convention snake_case, PascalCase, UPPER_CASE

Include / Exclude

Control which files are scanned:

include:
  - src
exclude:
  - src/generated/**

rules:
  - name: ...
  • No include or exclude — All .py files under the project root are scanned
  • include only — Only files matching the given paths are scanned
  • exclude only — All files except those matching the given paths are scanned
  • Both — include is applied first, then exclude filters within that result

Wildcard

* matches a single level in dotted module paths:

modules: contexts.*.domain  # matches contexts.boards.domain, contexts.auth.domain, ...

** matches one or more levels:

modules: contexts.**.domain  # matches contexts.boards.domain, contexts.boards.sub.domain, ...

Named Capture

{name} captures a single level (like *) and allows back-referencing:

apply:
  - name: domain-isolation
    rules: [attribute-matches-type]
    modules: contexts.{context}.domain

pyproject.toml

You can also configure in pyproject.toml:

[[tool.python-naming-linter.rules]]
name = "bool-method-prefix"
description = "Bool-returning functions must start with is_, has_, or should_"
type = "function"

[tool.python-naming-linter.rules.filter]
return_type = "bool"

[tool.python-naming-linter.rules.naming]
prefix = ["is_", "has_", "should_"]

[[tool.python-naming-linter.apply]]
name = "all"
rules = ["bool-method-prefix"]
modules = "**"

Inline Ignore

Suppress violations on specific lines using # pnl: ignore comments:

x: int = 1  # pnl: ignore

To suppress only specific rules, specify rule names:

x: int = 1  # pnl: ignore=attribute-matches-type

Multiple rules can be listed with commas:

x: int = 1  # pnl: ignore=attribute-matches-type,constant-upper-case

CLI

# Check with auto-discovered config (searches upward from cwd)
pnl check

# Specify config file (project root = config file's parent directory)
pnl check --config path/to/config.yaml

Exit codes:

  • 0 — No violations
  • 1 — Violations found
  • 2 — Config file not found

If no --config is given, the tool searches upward from the current directory for .python-naming-linter.yaml or pyproject.toml (with [tool.python-naming-linter]). The config file's parent directory is used as the project root.

Pre-commit

Add to .pre-commit-config.yaml:

- repo: https://github.com/heumsi/python-naming-linter
  rev: ''  # Use the tag you want to point at (e.g., v0.1.0)
  hooks:
    - id: python-naming-linter

To pass custom options:

- repo: https://github.com/heumsi/python-naming-linter
  rev: ''
  hooks:
    - id: python-naming-linter
      args: [--config, custom-config.yaml]

License

MIT

Metadata

Release files for python-naming-linter 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for python-naming-linter 0.5.0
File Size Uploaded
python_naming_linter-0.5.0.tar.gz 73.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for python-naming-linter 0.5.0
File Interpreter ABI Platform
python_naming_linter-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.7 kB

Release files / python_naming_linter-0.5.0.tar.gz

Download URL python_naming_linter-0.5.0.tar.gz
Size 73.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5cda3a344e261764d96e51a2caa2bc9d022605d3d213a66f7aecb749ff2715e1
BLAKE2b-256 checksum
How to use checksums
fbe5fabcd71f19aea3c9be8b1f538378edc86e1528eafef4e8d54982c4af62cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.

Transparency log

Release files / python_naming_linter-0.5.0-py3-none-any.whl

Download URL python_naming_linter-0.5.0-py3-none-any.whl
Size 18.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a177d06061d36720291f32326089880a0541804bda544b43442468de99b448e
BLAKE2b-256 checksum
How to use checksums
4fb6c6728dba3fc08551d576b50b443b0fe6a8b416c3b406e4eb5968582fc37b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release 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