psl supports two config file formats: a standalone YAML file or an inline section inside pyproject.toml.

Config File Discovery

When you run psl check without --config, the tool searches upward from the current working directory for one of:

  • .python-structure-linter.yaml
  • pyproject.toml (containing a [tool.python-structure-linter] section)

The first matching file is used, and its parent directory becomes the project root — every path in the config is resolved relative to that root.

To use a specific config file, pass it explicitly:

psl check --config path/to/.python-structure-linter.yaml

With --config, the project root is the config file's parent directory. A missing or invalid config file is a config error (exit code 2).

YAML Format

Create .python-structure-linter.yaml in your project root:

include: [src]
exclude: ["**/__pycache__/**"]

rules:
  - name: context-layers
    type: directory-exists
    description: Each context has domain, application, and adapters layers
    within: src/contexts/{ctx}
    require: [domain, application, adapters]

  - name: one-class-per-domain-module
    type: symbol-count
    description: Domain modules define a single class
    within: src/contexts/{ctx}/domain/**
    symbol: class
    max: 1
    ignore_files: [__init__.py]

Each rule carries a name, a type, a description, and the options for that type. Unlike naming rules, a structure rule scopes itself with its own within / source / mirror path pattern — there is no separate apply block. See Path Patterns for the segment syntax used in those patterns.

pyproject.toml Format

You can embed the same configuration inside pyproject.toml using the [tool.python-structure-linter] namespace. A rule's options are flat keys under its table — there are no nested option tables:

[tool.python-structure-linter]
include = ["src"]
exclude = ["**/__pycache__/**"]

[[tool.python-structure-linter.rules]]
name = "context-layers"
type = "directory-exists"
description = "Each context has domain, application, and adapters layers"
within = "src/contexts/{ctx}"
require = ["domain", "application", "adapters"]

[[tool.python-structure-linter.rules]]
name = "one-class-per-domain-module"
type = "symbol-count"
description = "Domain modules define a single class"
within = "src/contexts/{ctx}/domain/**"
symbol = "class"
max = 1
ignore_files = ["__init__.py"]

Both formats are equivalent — use whichever fits your project's conventions.

Top-Level Keys

Key Description
rules List of structure rule definitions (each with its own type and options)
include Path roots to scan (optional)
exclude Path globs to drop from the scan (optional)

There is no apply key. Each rule targets its own paths through within, source, or mirror, so rules and scopes are declared together.

include / exclude

Control which part of the tree is scanned. Both take project-root-relative glob patterns:

include:
  - src
exclude:
  - "**/__pycache__/**"

Behavior:

  • Neither — the entire tree under the project root is scanned.
  • include only — only paths under the given roots are scanned.
  • exclude only — the whole tree is scanned except paths matching the given globs.
  • Bothinclude is applied first, then exclude filters within that result.