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.yamlpyproject.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.
includeonly — only paths under the given roots are scanned.excludeonly — the whole tree is scanned except paths matching the given globs.- Both —
includeis applied first, thenexcludefilters within that result.