A filesystem structure linter for Python projects. Declare rules about how your source tree is laid out and enforce them with a single CLI command.

What It Does

  • Require directories to exist, restrict which components may live where, mirror tests against source, and bound how many symbols a module defines
  • Scope each rule to specific parts of the tree with path patterns
  • Integrate into CI or pre-commit to keep your project structure consistent

For Python developers who want to enforce team-specific structural conventions that names and imports can't express. psl never inspects imports or names — only the shape of the tree and the population of each module.

Key Features

Feature Description
Rule Types directory-exists, allowed-children, mirror, and symbol-count
Path Patterns Scope rules with literal, *, **, and {name} segments
Include/Exclude Restrict the scanned roots and drop paths with glob patterns
Inline Ignore Suppress a file's symbol-count checks with # psl: ignore
Pre-commit Drop-in integration with pre-commit hooks

Quick Start

Install:

pip install python-structure-linter

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

Output:

$ psl check
src/contexts/orders
    [context-layers] Each context has domain, application, and adapters layers
    missing required directory 'adapters'

src/contexts/orders/domain/entities/order.py:14
    [one-class-per-domain-module] Domain modules define a single class
    2 top-level class definitions found, at most 1 allowed

Found 2 violation(s).

More Examples

Mirror Tests Against Source

Require every test directory to have a matching source directory, so the tests tree invents no structure of its own:

rules:
  - name: tests-mirror-src
    type: mirror
    description: The tests tree mirrors the src tree
    source: src
    mirror: tests
    direction: mirror-to-source
    mirror_extra_allowed: [doubles, fixtures, conftest]

This catches tests/contexts/orders/domain/entities when src/contexts/orders/domain/entities does not exist — the test tree drifted from the code it covers. Scaffolding directories like doubles, fixtures, and conftest are exempt.

Constrain a Module's Public Surface

Count only names that don't start with an underscore, so you can require exactly one public function per module while leaving private helpers unrestricted:

rules:
  - name: one-public-function-per-checker
    type: symbol-count
    description: Each checker module exposes exactly one public function
    within: src/checks/**
    symbol: function
    visibility: public
    min: 1
    max: 1
    ignore_files: [__init__.py]

This catches a checker module that exports two public functions, or none, while _normalize and _walk beside them stay uncounted.