Skip to main content

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)

Source distribution for python-structure-linter 0.1.0
File Size Uploaded
python_structure_linter-0.1.0.tar.gz 74.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for python-structure-linter 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page