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
    type: variable
    filter: { target: attribute }
    naming: { source: type_annotation, transform: snake_case }

  - name: bool-method-prefix
    type: function
    filter: { return_type: bool }
    naming: { prefix: [is_, has_, should_] }

  - name: domain-module-naming
    type: module
    naming: { source: class_name, transform: snake_case }

  - name: constant-upper-case
    type: variable
    filter: { target: constant }
    naming: { case: UPPER_CASE }

  - name: exception-naming
    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] max_retry (expected case: UPPER_CASE)

contexts/billing/domain/service.py:6
    [bool-method-prefix] validate (expected prefix: is_ | has_ | should_)

contexts/billing/domain/exceptions.py:3
    [exception-naming] BillingError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 3 violation(s).