Purpose

Real projects have distinct layers — domain, infrastructure, API — each with its own naming conventions. Instead of applying every rule globally, you can scope each rule set to the layer where it belongs, reducing false positives and making the intent of each rule explicit.

Configuration

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: domain-module-naming
    description: Module filename must match the primary class name in snake_case
    type: module
    naming: { source: class_name, transform: snake_case }

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

  - 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: 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: "**"

The domain-layer apply block targets every contexts/<context>/domain package, while global-exceptions runs the exception naming rule across the entire codebase.

Violation Example

# contexts/billing/domain/service.py

max_retry = 3                          # constant not in UPPER_CASE

class BillingService:
    def validate(self) -> bool:        # bool method missing prefix
        return self._status == "active"
# contexts/billing/domain/exceptions.py

class BillingError(Exception):         # exception missing semantic suffix
    pass

Passing Example

# contexts/billing/domain/service.py

MAX_RETRY = 3

class BillingService:
    def is_valid(self) -> bool:
        return self._status == "active"
# contexts/billing/domain/exceptions.py

class BillingNotFoundError(Exception):
    pass

Output

$ pnl check
contexts/billing/domain/service.py:3
    [constant-upper-case] Module-level constants must use UPPER_CASE
    max_retry (expected case: UPPER_CASE)

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

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

Found 3 violation(s).