python-structure-linter
A filesystem structure and symbol-placement linter for Python projects. Declare rules about how your source tree is laid out — which directories must exist, which components may live where, whether tests mirror source, how many symbols a module may define — and catch violations at commit time.
psl is the structural sibling of python-dependency-linter (import direction) and python-naming-linter (identifier names). It never inspects imports or names — only the shape of the tree and the population of each module.
Installation
pip install python-structure-linter
Or with uv:
uv add python-structure-linter
Quick Start
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]
Run:
psl check
Rule Types
Each rule declares a type (the primitive) plus type-specific options. The engine
is neutral — your project's taxonomy (layer names, component directories) lives
entirely in the config values, never in the rule vocabulary.
directory-exists
Every directory matching within must contain the require directories.
- name: context-layers
type: directory-exists
within: src/contexts/{ctx}
require: [domain, application, adapters]
allowed-children
A scope directory may only contain the allowed child names. With
group_level: optional, a child directory that is not itself an allowed name is
treated as a one-level grouping tier (e.g. a subdomain) and passes only if all of
its own children are allowed.
- name: domain-components
type: allowed-children
within: src/contexts/{ctx}/domain
kind: dir # dir (default) | file | any
group_level: optional # none (default) | optional
allow: [entities, value_objects, events, services, errors]
ignore: [__init__.py]
mirror
The source and mirror trees must mirror each other at directory granularity.
direction selects which way the requirement runs:
mirror-to-source(default): every mirror directory must have a source counterpart, so the mirror tree invents no structure of its own (a test's location is determined by the source it covers).source-to-mirror: every source directory must have a mirror counterpart.both: both requirements hold.
mirror_extra_allowed exempts mirror-side directories (e.g. test scaffolding)
from the mirror-to-source check; source_extra_allowed exempts source-side
directories from the source-to-mirror check.
- name: tests-mirror-src
type: mirror
source: src
mirror: tests
direction: mirror-to-source
granularity: directory
ignore: [__pycache__]
mirror_extra_allowed: [doubles, fixtures, conftest]
symbol-count
Bound the number of top-level symbols (classes or functions) a module may define.
Nested definitions are not counted. visibility: public counts only names that
do not start with an underscore, so you can constrain a module's public surface
(e.g. exactly one public function) while leaving private helpers unrestricted.
- name: one-error-per-file
type: symbol-count
within: src/contexts/{ctx}/domain/**/errors/**
symbol: class
max: 1
ignore_files: [__init__.py]
- name: one-public-function-per-checker
type: symbol-count
within: src/checks/**
symbol: function
visibility: public # any (default) | public
min: 1
max: 1
Path Patterns
within, and the tree roots in mirror, are project-root-relative paths whose
segments may be:
| Token | Matches |
|---|---|
| literal | a segment with that exact name |
* |
exactly one segment |
** |
zero or more segments |
{name} |
one segment, captured as name |
File-targeting rules (symbol-count) match .py files; the others match
directories.
Inline Ignore
File-based rules honor an inline ignore comment:
# psl: ignore[one-error-per-file]
Use # psl: ignore (no brackets) to skip every rule for that file.
Include / Exclude
include restricts the scanned roots; exclude drops matching paths. Both take
project-root-relative glob patterns.
Configuration File
psl reads .python-structure-linter.yaml, or [tool.python-structure-linter]
in pyproject.toml, searching upward from the current directory.
Pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/heumsi/python-structure-linter
rev: v0.1.0
hooks:
- id: python-structure-linter
License
MIT
Metadata
Release files for python-structure-linter 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_structure_linter-0.1.0.tar.gz | 74.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_structure_linter-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.4 kB
Release files / python_structure_linter-0.1.0.tar.gz
| Download URL | python_structure_linter-0.1.0.tar.gz |
|---|---|
| Size | 74.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
176237b34d72526448eb523ccd5e54a2c66d2e0ab3793aaf689953a7ace64876
|
|
BLAKE2b-256 checksum How to use checksums |
8a20244db31c099601d727250496abe6c0c880fe99dabb15839898a38da429c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.
Transparency logRelease files / python_structure_linter-0.1.0-py3-none-any.whl
| Download URL | python_structure_linter-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c4fe672d563108c2816832fe907cc052a12e84fa69d81d9a8daa40c05c1fefd6
|
|
BLAKE2b-256 checksum How to use checksums |
8bc10a20089af20b0cbcfddf40a5980f287b6127378f14ddaff546a93b88c542
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 17, 2026.
Transparency log