A naming convention linter for Python projects. Define custom naming rules and enforce them with a single CLI command.
- 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.
pip install python-naming-linterOr with uv:
uv add python-naming-linterCreate .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 checkOutput:
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).
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.*.domainThis 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.
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.*.domainA file custom.py containing class CustomObject is a violation — it should be custom_object.py.
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: "**"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_] }| 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 |
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 |
| 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 |
case |
Name must follow a casing convention | snake_case, PascalCase, UPPER_CASE |
Control which files are scanned:
include:
- src
exclude:
- src/generated/**
rules:
- name: ...- No
includeorexclude— All.pyfiles under the project root are scanned includeonly — Only files matching the given paths are scannedexcludeonly — All files except those matching the given paths are scanned- Both —
includeis applied first, thenexcludefilters within that result
* 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, ...{name} captures a single level (like *) and allows back-referencing:
apply:
- name: domain-isolation
rules: [attribute-matches-type]
modules: contexts.{context}.domainYou 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 = "**"Suppress violations on specific lines using # pnl: ignore comments:
x: int = 1 # pnl: ignoreTo suppress only specific rules, specify rule names:
x: int = 1 # pnl: ignore=attribute-matches-typeMultiple rules can be listed with commas:
x: int = 1 # pnl: ignore=attribute-matches-type,constant-upper-case# 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.yamlExit codes:
0— No violations1— Violations found2— 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.
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-linterTo pass custom options:
- repo: https://github.com/heumsi/python-naming-linter
rev: ''
hooks:
- id: python-naming-linter
args: [--config, custom-config.yaml]MIT