Rules are the core building blocks of pnl. Each rule targets a specific kind of Python name, optionally narrows its scope with filters, and then enforces a naming constraint.
Structure¶
Every rule has three required fields and two optional ones:
rules:
- name: my-rule # Unique identifier for this rule
type: variable # What kind of name to lint
filter: { ... } # (optional) Narrow which names are checked
naming: { ... } # How the name must be formed
The name is used to reference the rule in apply blocks and in # pnl: ignore comments.
Fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique identifier, referenced in apply and # pnl: ignore |
type |
Yes | What kind of name to lint (variable, function, class, module, package) |
filter |
No | Narrow which names are checked (see Filters below) |
naming |
Yes | How the name must be formed (see Naming Constraints below) |
Types¶
variable¶
Targets variable names — any assignment that introduces a name into a scope.
Sub-targets (set via filter.target):
| Value | What it covers |
|---|---|
attribute |
Class-level attributes (self.x, x: int = ...) |
parameter |
Function/method parameters |
local_variable |
Variables declared inside a function body |
constant |
Module-level constants (typically ALL_CAPS) |
Supported filter fields: target
Supported naming fields: prefix, suffix, regex, source + transform, case
Example — enforce UPPER_CASE for module-level constants:
rules:
- name: constant-upper-case
type: variable
filter: { target: constant }
naming: { case: UPPER_CASE }
apply:
- name: all
rules: [constant-upper-case]
modules: "**"
Example — enforce attribute names match their type annotation:
rules:
- name: attribute-matches-type
type: variable
filter: { target: attribute }
naming: { source: type_annotation, transform: snake_case }
apply:
- name: domain-layer
rules: [attribute-matches-type]
modules: contexts.*.domain
function¶
Targets function and method definitions — any def statement at any scope level.
Supported filter fields: target, return_type, decorator
Supported naming fields: prefix, suffix, regex, case
Example — require is_ / has_ / should_ prefix on boolean-returning methods:
rules:
- name: bool-method-prefix
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }
apply:
- name: all
rules: [bool-method-prefix]
modules: "**"
Example — require _impl suffix on @staticmethod functions:
rules:
- name: static-impl-suffix
type: function
filter: { decorator: staticmethod }
naming: { suffix: [_impl] }
apply:
- name: all
rules: [static-impl-suffix]
modules: "**"
class¶
Targets class definitions — any class statement.
Supported filter fields: base_class, decorator
Supported naming fields: prefix, suffix, regex, case
Example — enforce a specific pattern for exception classes:
rules:
- name: exception-naming
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
apply:
- name: all
rules: [exception-naming]
modules: "**"
Example — require DTO suffix on dataclasses:
rules:
- name: dataclass-dto-suffix
type: class
filter: { decorator: dataclass }
naming: { suffix: [DTO] }
apply:
- name: all
rules: [dataclass-dto-suffix]
modules: "**"
module¶
Targets the filename of each .py file (without the .py extension). Useful for enforcing that module names reflect their contents.
Supported filter fields: none
Supported naming fields: prefix, suffix, regex, source + transform, case
Example — enforce that a module's filename matches the primary class it contains:
rules:
- name: domain-module-naming
type: module
naming: { source: class_name, transform: snake_case }
apply:
- name: domain-layer
rules: [domain-module-naming]
modules: contexts.*.domain
A file custom.py that contains only class CustomObject is a violation — the file should be named custom_object.py.
package¶
Targets the directory name of each Python package (a directory containing __init__.py).
Supported filter fields: none
Supported naming fields: prefix, suffix, regex, case
Example — require all package names to be lowercase:
rules:
- name: package-snake-case
type: package
naming: { case: snake_case }
apply:
- name: all
rules: [package-snake-case]
modules: "**"
Filters¶
Filters are specified in the filter block of a rule:
rules:
- name: my-rule
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_] }
Multiple filter fields can be combined — a name must satisfy all of them to be checked.
target¶
Narrows which names within the rule type are checked based on their role in the code.
For variable rules¶
| Value | Matches |
|---|---|
attribute |
Class-level attribute assignments, including annotated attributes (x: int = 1) |
parameter |
Function or method parameters |
local_variable |
Variables assigned inside a function body |
constant |
Module-level assignments (typically treated as constants) |
Supported rule types: variable
Example — lint only class attributes:
Matches names that are assigned at the class body level, including annotated attributes.
rules:
- name: attribute-matches-type
type: variable
filter: { target: attribute }
naming: { source: type_annotation, transform: snake_case }
apply:
- name: all
rules: [attribute-matches-type]
modules: "**"
| Name | Context | Result |
|---|---|---|
user_id: UserId = ... |
class body | Pass — name matches type annotation in snake_case |
userId: UserId = ... |
class body | Violation — name does not match user_id |
user_id = 1 |
function body | Not checked — local variables are ignored |
Example — lint only function/method parameters:
Matches names declared as function or method parameters (including self and cls by convention — though you may want to exclude them with additional patterns).
rules:
- name: param-snake-case
type: variable
filter: { target: parameter }
naming: { case: snake_case }
apply:
- name: all
rules: [param-snake-case]
modules: "**"
| Name | Context | Result |
|---|---|---|
user_id |
function parameter | Pass — snake_case |
userId |
function parameter | Violation — camelCase not allowed |
MAX_RETRIES |
module level | Not checked — constants are ignored |
Example — lint only local variables inside functions:
Matches names assigned inside a function or method body (not parameters, not class-level attributes).
rules:
- name: local-var-snake-case
type: variable
filter: { target: local_variable }
naming: { case: snake_case }
apply:
- name: all
rules: [local-var-snake-case]
modules: "**"
| Name | Context | Result |
|---|---|---|
result |
inside function body | Pass — snake_case |
tmpVal |
inside function body | Violation — camelCase not allowed |
MAX_SIZE |
module level | Not checked — constants are ignored |
Example — lint only module-level constants:
Matches names assigned at module (top-level) scope.
rules:
- name: constant-upper-case
type: variable
filter: { target: constant }
naming: { case: UPPER_CASE }
apply:
- name: all
rules: [constant-upper-case]
modules: "**"
| Name | Context | Result |
|---|---|---|
MAX_RETRIES |
module level | Pass — UPPER_CASE |
defaultTimeout |
module level | Violation — not UPPER_CASE |
count |
function body | Not checked — local variables are ignored |
For function rules¶
| Value | Matches |
|---|---|
method |
Functions defined inside a class body |
function |
Functions defined at module level or inside other functions |
Supported rule types: function
Example — lint only module-level functions (not methods):
Matches def statements at module scope or nested inside other functions, but not methods defined inside a class.
rules:
- name: function-snake-case
type: function
filter: { target: function }
naming: { case: snake_case }
apply:
- name: all
rules: [function-snake-case]
modules: "**"
| Name | Context | Result |
|---|---|---|
process_order |
module-level def |
Pass — snake_case |
processOrder |
module-level def |
Violation — camelCase not allowed |
processOrder |
inside a class | Not checked — methods are ignored |
Example — lint only class methods:
Matches def statements inside a class body.
rules:
- name: method-snake-case
type: function
filter: { target: method }
naming: { case: snake_case }
apply:
- name: all
rules: [method-snake-case]
modules: "**"
| Name | Context | Result |
|---|---|---|
get_user |
inside a class | Pass — snake_case |
getUser |
inside a class | Violation — camelCase not allowed |
getUser |
module-level def |
Not checked — functions are ignored |
return_type¶
Matches functions whose return type annotation equals the specified type name.
Supported rule types: function
Accepted values: any Python type name as a string, e.g. bool, str, int, None
The filter matches functions with the given -> <type> annotation. Functions without a return type annotation, or with a different annotation, are not checked.
Example — require a boolean-indicating prefix on bool-returning functions:
rules:
- name: bool-method-prefix
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }
apply:
- name: all
rules: [bool-method-prefix]
modules: "**"
| Signature | Result |
|---|---|
def is_active(self) -> bool: |
Pass — starts with is_ |
def has_permission(self) -> bool: |
Pass — starts with has_ |
def validate(self) -> bool: |
Violation — no matching prefix |
def process(self) -> str: |
Not checked — return type is str, not bool |
def run(self): |
Not checked — no return type annotation |
Example — require a descriptive prefix on str-returning functions:
rules:
- name: str-getter-prefix
type: function
filter: { return_type: str }
naming: { prefix: [get_, format_, build_, to_] }
apply:
- name: all
rules: [str-getter-prefix]
modules: "**"
| Signature | Result |
|---|---|
def get_name(self) -> str: |
Pass — starts with get_ |
def format_label(self) -> str: |
Pass — starts with format_ |
def name(self) -> str: |
Violation — no matching prefix |
def is_active(self) -> bool: |
Not checked — return type is bool, not str |
Example — require a _or_none suffix on None-returning functions:
rules:
- name: none-returning-suffix
type: function
filter: { return_type: None }
naming: { suffix: [_or_none] }
apply:
- name: all
rules: [none-returning-suffix]
modules: "**"
| Signature | Result |
|---|---|
def find_user_or_none(self) -> None: |
Pass — ends with _or_none |
def find_user(self) -> None: |
Violation — missing _or_none suffix |
def find_user(self) -> User: |
Not checked — return type is User, not None |
decorator¶
Matches functions or classes that are decorated with the specified decorator name.
Supported rule types: function, class
Accepted values: any decorator name as a string (without @), e.g. staticmethod, classmethod, property, dataclass, abstractmethod
The filter matches the decorator by its bare name. Both @dataclass and @dataclasses.dataclass are matched by the value dataclass.
Example — require a suffix on static methods:
rules:
- name: static-method-suffix
type: function
filter: { decorator: staticmethod }
naming: { suffix: [_impl] }
apply:
- name: all
rules: [static-method-suffix]
modules: "**"
| Definition | Result |
|---|---|
@staticmethod / def compute_impl(cls): |
Pass — ends with _impl |
@staticmethod / def compute(cls): |
Violation — missing _impl suffix |
def compute(self): |
Not checked — not a static method |
Example — require a DTO suffix on dataclasses:
rules:
- name: dataclass-dto-suffix
type: class
filter: { decorator: dataclass }
naming: { suffix: [DTO] }
apply:
- name: all
rules: [dataclass-dto-suffix]
modules: "**"
| Definition | Result |
|---|---|
@dataclass / class UserDTO: |
Pass — ends with DTO |
@dataclass / class User: |
Violation — missing DTO suffix |
class User: |
Not checked — not a dataclass |
base_class¶
Matches classes that inherit from the specified base class.
Supported rule types: class
Accepted values: any class name as a string, e.g. Exception, BaseModel, ABC
The filter matches the direct base class name. class MyError(Exception) matches the value Exception.
Example — enforce a naming pattern for all exception classes:
rules:
- name: exception-naming
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
apply:
- name: all
rules: [exception-naming]
modules: "**"
| Definition | Result |
|---|---|
class UserNotFoundError(Exception): |
Pass — matches the regex |
class InvalidInputError(Exception): |
Pass — matches the regex |
class UserException(Exception): |
Violation — does not match the regex |
class User: |
Not checked — does not inherit from Exception |
Example — require a Schema suffix on Pydantic models:
Matches classes that inherit from BaseModel (e.g. Pydantic models).
rules:
- name: pydantic-schema-suffix
type: class
filter: { base_class: BaseModel }
naming: { suffix: [Schema] }
apply:
- name: all
rules: [pydantic-schema-suffix]
modules: "**"
| Definition | Result |
|---|---|
class UserSchema(BaseModel): |
Pass — ends with Schema |
class CreateUserSchema(BaseModel): |
Pass — ends with Schema |
class User(BaseModel): |
Violation — missing Schema suffix |
class User: |
Not checked — does not inherit from BaseModel |
Naming Constraints¶
Naming constraints are specified in the naming block of a rule:
rules:
- name: my-rule
type: function
naming: { prefix: [is_, has_] }
apply:
- name: all
rules: [my-rule]
modules: "**"
Each rule must have exactly one naming constraint (or one source + transform pair). The constraint is evaluated against every name that passes the rule's type and filter checks.
prefix¶
The name must start with one of the listed prefixes.
Accepted values: a list of one or more prefix strings.
Example — bool-returning methods must use a semantic prefix:
rules:
- name: bool-method-prefix
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }
apply:
- name: all
rules: [bool-method-prefix]
modules: "**"
| Name | Result |
|---|---|
is_active |
Pass — starts with is_ |
has_permission |
Pass — starts with has_ |
should_retry |
Pass — starts with should_ |
validate |
Violation — no matching prefix |
check_active |
Violation — check_ is not in the list |
Example — test functions must start with test_:
rules:
- name: test-function-prefix
type: function
filter: { decorator: pytest.mark }
naming: { prefix: [test_] }
apply:
- name: all
rules: [test-function-prefix]
modules: "**"
| Name | Result |
|---|---|
test_login_succeeds |
Pass — starts with test_ |
test_invalid_token |
Pass — starts with test_ |
login_succeeds |
Violation — missing test_ prefix |
check_login |
Violation — check_ is not in the list |
suffix¶
The name must end with one of the listed suffixes.
Accepted values: a list of one or more suffix strings.
Example — data-access classes must end with Repository or Service:
rules:
- name: repository-suffix
type: class
naming: { suffix: [Repository, Service] }
apply:
- name: all
rules: [repository-suffix]
modules: "**"
| Name | Result |
|---|---|
UserRepository |
Pass — ends with Repository |
OrderService |
Pass — ends with Service |
UserManager |
Violation — no matching suffix |
User |
Violation — no matching suffix |
Example — exception classes must end with Error:
rules:
- name: exception-suffix
type: class
filter: { base_class: Exception }
naming: { suffix: [Error] }
apply:
- name: all
rules: [exception-suffix]
modules: "**"
| Name | Result |
|---|---|
ValidationError |
Pass — ends with Error |
NotFoundError |
Pass — ends with Error |
InvalidInput |
Violation — does not end with Error |
NotFoundException |
Violation — ends with Exception, not Error |
regex¶
The name must match a regular expression.
Accepted values: a string containing a valid Python regular expression.
This is the most expressive constraint — use it when prefix, suffix, or case are not specific enough.
Example — exception class names must follow a structured pattern:
rules:
- name: exception-naming
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
apply:
- name: all
rules: [exception-naming]
modules: "**"
| Name | Result |
|---|---|
UserNotFoundError |
Pass — matches the pattern |
OrderInvalidError |
Pass — matches the pattern |
FilterError |
Violation — does not end with the required suffix group |
userNotFoundError |
Violation — does not start with an uppercase letter |
Example — module-level constants must be all-uppercase with underscores:
rules:
- name: constant-regex
type: variable
filter: { target: constant }
naming: { regex: "^[A-Z][A-Z0-9_]*$" }
apply:
- name: all
rules: [constant-regex]
modules: "**"
| Name | Result |
|---|---|
MAX_RETRIES |
Pass — all uppercase with underscores |
DEFAULT_TIMEOUT |
Pass — all uppercase with underscores |
API_V2_URL |
Pass — uppercase with digits and underscores |
max_retries |
Violation — lowercase |
MaxRetries |
Violation — mixed case |
_PRIVATE |
Violation — starts with underscore, not matched by ^[A-Z] |
source + transform¶
The name must be derived from another element in the code, after applying a transformation. This is used for relational naming — where the name of one thing must reflect another.
Both fields must be specified together.
source values¶
| Value | What it reads |
|---|---|
type_annotation |
The type annotation of the variable (e.g. SubscriptionRepository from x: SubscriptionRepository) |
class_name |
The name of a class defined in the module (used with type: module) |
transform values¶
| Value | What it does |
|---|---|
snake_case |
Converts PascalCase or camelCase to snake_case (e.g. SubscriptionRepository → subscription_repository) |
Example — variable name must match its type annotation:
rules:
- name: attribute-matches-type
type: variable
filter: { target: attribute }
naming: { source: type_annotation, transform: snake_case }
apply:
- name: all
rules: [attribute-matches-type]
modules: "**"
| Declaration | Result |
|---|---|
subscription_repository: SubscriptionRepository |
Pass — name matches transformed type |
order_service: OrderService |
Pass — name matches transformed type |
repo: SubscriptionRepository |
Violation — repo does not match subscription_repository |
svc: OrderService |
Violation — svc does not match order_service |
source_object_context: ObjectContext |
Pass — name ends with _object_context (prefix + expected form is allowed) |
The {prefix}_{expected} form is accepted. If the expected derived name is object_context, then source_object_context passes because it ends with _object_context.
Example — module filename must match the class it contains:
rules:
- name: domain-module-naming
type: module
naming: { source: class_name, transform: snake_case }
apply:
- name: all
rules: [domain-module-naming]
modules: "**"
| File | Class | Result |
|---|---|---|
custom_object.py |
CustomObject |
Pass — filename matches transformed class name |
order_service.py |
OrderService |
Pass — filename matches transformed class name |
custom.py |
CustomObject |
Violation — custom does not match custom_object |
service.py |
OrderService |
Violation — service does not match order_service |
case¶
The name must follow a specific casing convention.
Accepted values:
| Value | Pattern | Example |
|---|---|---|
snake_case |
all lowercase, words separated by underscores | my_variable_name |
PascalCase |
each word starts with uppercase, no separators | MyClassName |
UPPER_CASE |
all uppercase, words separated by underscores | MAX_RETRIES |
Example — enforce snake_case for function names:
rules:
- name: function-snake-case
type: function
naming: { case: snake_case }
apply:
- name: all
rules: [function-snake-case]
modules: "**"
| Name | Result |
|---|---|
get_user |
Pass |
calculate_total_price |
Pass |
getUserById |
Violation — camelCase |
GetUser |
Violation — PascalCase |
GETUSER |
Violation — all uppercase |
Example — enforce UPPER_CASE for constants:
rules:
- name: constant-upper-case
type: variable
filter: { target: constant }
naming: { case: UPPER_CASE }
apply:
- name: all
rules: [constant-upper-case]
modules: "**"
| Name | Result |
|---|---|
MAX_RETRIES |
Pass |
DEFAULT_TIMEOUT |
Pass |
max_retries |
Violation — lowercase |
maxRetries |
Violation — camelCase |
Example — enforce PascalCase for classes:
rules:
- name: class-pascal-case
type: class
naming: { case: PascalCase }
apply:
- name: all
rules: [class-pascal-case]
modules: "**"
| Name | Result |
|---|---|
MyService |
Pass |
my_service |
Violation |
myService |
Violation |
Summary¶
Rule fields¶
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique identifier, referenced in apply and # pnl: ignore |
type |
Yes | What kind of name to lint (variable, function, class, module, package) |
filter |
No | Narrow which names are checked |
naming |
Yes | How the name must be formed |
Rule types¶
| Type | What it targets | Supported filters | Notes |
|---|---|---|---|
variable |
Variables by scope/role | target |
Use target to narrow to attributes, parameters, etc. |
function |
Function and method definitions | target, return_type, decorator |
|
class |
Class definitions | base_class, decorator |
|
module |
Module (file) names | none | Supports source + transform |
package |
Package (directory) names | none |
Filters¶
| Filter | variable |
function |
class |
module |
package |
|---|---|---|---|---|---|
target |
attribute, parameter, local_variable, constant |
method, function |
— | — | — |
return_type |
— | any type string | — | — | — |
decorator |
— | any decorator name | any decorator name | — | — |
base_class |
— | — | any class name | — | — |
Naming constraints¶
| Constraint | Value type | Use when |
|---|---|---|
prefix |
list of strings | Names must start with one of several prefixes |
suffix |
list of strings | Names must end with one of several suffixes |
regex |
string (regex) | Names must match a complex pattern |
source + transform |
string + string | Names must be derived from another code element |
case |
snake_case, PascalCase, or UPPER_CASE |
Names must follow a casing convention |