A naming convention linter for Python projects
Project description
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.
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 |
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
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
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 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.
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 python_naming_linter-0.4.0.tar.gz.
File metadata
- Download URL: python_naming_linter-0.4.0.tar.gz
- Upload date:
- Size: 69.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da192310661f64f89de369b0a6ba942093ae00b8c1336b5d488e6ee62e277e52
|
|
| MD5 |
a48d349ec16a28af1b55978d3c55f873
|
|
| BLAKE2b-256 |
3f7dbaa39c2079cddc3055ff7bcc22f15aca7039457b42938ba34a8ebf8e3ec3
|
Provenance
The following attestation bundles were made for python_naming_linter-0.4.0.tar.gz:
Publisher:
publish.yaml on heumsi/python-naming-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_naming_linter-0.4.0.tar.gz -
Subject digest:
da192310661f64f89de369b0a6ba942093ae00b8c1336b5d488e6ee62e277e52 - Sigstore transparency entry: 1203568489
- Sigstore integration time:
-
Permalink:
heumsi/python-naming-linter@abe4e3991881674d4a013b9e92f9a801839a9a79 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@abe4e3991881674d4a013b9e92f9a801839a9a79 -
Trigger Event:
push
-
Statement type:
File details
Details for the file python_naming_linter-0.4.0-py3-none-any.whl.
File metadata
- Download URL: python_naming_linter-0.4.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c03bfcc266cf1244a56f4b946dbaae0f12deea8b2c4103c52dab19cf5a31a0ae
|
|
| MD5 |
5a5d60f9807cbf88ee636bde9893f506
|
|
| BLAKE2b-256 |
776ccd3337f9405556f0bede3d9cb9cc04635f55f50c4dc4b2a4c38ec7b3e1db
|
Provenance
The following attestation bundles were made for python_naming_linter-0.4.0-py3-none-any.whl:
Publisher:
publish.yaml on heumsi/python-naming-linter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_naming_linter-0.4.0-py3-none-any.whl -
Subject digest:
c03bfcc266cf1244a56f4b946dbaae0f12deea8b2c4103c52dab19cf5a31a0ae - Sigstore transparency entry: 1203568492
- Sigstore integration time:
-
Permalink:
heumsi/python-naming-linter@abe4e3991881674d4a013b9e92f9a801839a9a79 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/heumsi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@abe4e3991881674d4a013b9e92f9a801839a9a79 -
Trigger Event:
push
-
Statement type: