Skip to main content

pymaxlines

PyPI Python: 3.12 | 3.13 | 3.14 CI License: MIT

A Python linter that fails when a file or function has too many code lines. Run it standalone or as a pre-commit hook.

Requires Python 3.12+.

Why

A file that runs into the thousands of lines is hard to navigate, test, and review, yet few Python linters enforce a limit. Ruff has no max-lines rule and does not plan to add one. McCabe complexity catches convoluted control flow but ignores sheer size — a 600-line function with simple branches passes just fine.

The problem compounds with large language model (LLM) coding agents. Long files exhaust the context window and push agents toward destructive rewrites — splitting a file on a token boundary instead of a logical one. Enforcing a line budget per file and per function keeps the codebase in a shape that both humans and agents can work with.

pymaxlines counts only code lines, the way oxlint's max-lines rule does with skipBlankLines and skipComments, so docstrings, comments, and blank lines stay free. It applies one limit per file and one per function, with separate thresholds for test files.

Quick example

A failing run prints one line per finding and exits 1:

$ pymaxlines src/app/service.py
src/app/service.py: 412 code lines (max 400)
src/app/service.py:88: function 'handle_request' has 73 code lines (max 60)

Installation

Run directly with uvx (no install needed):

uvx pymaxlines

Or install into a project:

uv add --dev pymaxlines

When installed, python -m pymaxlines is equivalent to the pymaxlines command.

Pre-commit hook

Add the hook to .pre-commit-config.yaml and run pre-commit install or prek install:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/jeffzi/pymaxlines
    rev: v0.5.0
    hooks:
      - id: check-max-lines

The hook receives explicit file paths from the pre-commit framework and checks only those files. The shipped hook passes --force-exclude, so exclude patterns from [tool.pymaxlines] are honored automatically. To check every file pre-commit passes and ignore exclude patterns, add args: [--no-force-exclude]. Pre-commit's own exclude: key remains available for coarser filtering. Limits and skip-* settings from [tool.pymaxlines] in the repo root still apply.

Usage

File discovery

With no arguments, pymaxlines checks every *.py file under the current directory recursively:

$ pymaxlines
pkg/big.py: 412 code lines (max 400)

Pass one or more directories to scope the check:

pymaxlines src/ tests/

Discovery skips these directories at any depth: .git, .venv*, node_modules, __pycache__, .tox, .nox, .eggs. The .venv* entry is a glob, so .venv, .venv-3.12, and .venv312 are all pruned. Symlinked directories are also skipped. A path reached more than once (for example a file also covered by a directory argument) is reported once, in first-seen order. Within each directory, files are visited in sorted order for deterministic output.

Files named explicitly on the command line are checked as given, even if they match a skip directory, an exclude glob, or lack a .py suffix. Pass --force-exclude to apply exclude globs to explicit paths too. Files and directories can be mixed in one invocation.

Exclude globs

Use --exclude to skip files or directories by glob pattern:

pymaxlines --exclude "migrations" --exclude "generated/*.py"

A glob matches the discovered path (src/generated/parser.py) or the final path component (migrations). A matched directory is not descended into. --exclude on the command line replaces the exclude list from the config file.

Source and test files

A file is a test file when, relative to the current directory, the path has a tests component, or the filename starts with test_ or ends with _test.py. A file outside the current directory is classified by its filename only. Everything else is a source file.

Flags

Flag Applies to Default Meaning
--max-lines source 400 code lines per file
--max-lines-test test 800 code lines per file
--max-lines-per-function source 60 code lines per function; 0 disables
--max-lines-per-function-test test 0 code lines per function; 0 disables
--skip-blank-lines all True exclude blank lines from counts
--skip-comments all True exclude comment-only lines
--skip-docstrings all True exclude standalone docstrings
--report-unused-disable-directives all False fail on directives that suppress nothing
--force-exclude explicit False apply exclude globs to explicitly passed paths
--exclude GLOB discovery skip matching files/directories (repeatable)
--config PATH read config from PATH instead of pyproject.toml
-v, --version print version and exit

Each --skip-* flag has a --no-skip-* counterpart, and --report-unused-disable-directives has --no-report-unused-disable-directives. --max-lines and --max-lines-test have no disable value — 0 fails any file containing code. All four limit flags reject negative values.

Exit codes

Code Meaning
0 No findings. When discovery matches no .py files, pymaxlines prints a warning to stderr
and still exits 0.
1 One or more findings, or a file that could not be read or parsed.
2 Invalid CLI usage, a negative limit, or an invalid/unknown config key.

Configuration

pymaxlines reads defaults from [tool.pymaxlines] in the working directory's pyproject.toml:

[tool.pymaxlines]
max-lines = 300
max-lines-per-function = 40
exclude = ["migrations", "generated/*.py"]

Supported keys

Key Type Default
max-lines integer 400
max-lines-test integer 800
max-lines-per-function integer 60
max-lines-per-function-test integer 0
skip-blank-lines boolean true
skip-comments boolean true
skip-docstrings boolean true
report-unused-disable-directives boolean false
force-exclude boolean false
exclude list of strings []

Precedence

A flag on the command line overrides the config file. A key absent from the config file keeps its built-in default. Order: built-in default < config file < CLI flag.

Error handling

An unknown key, a wrong-typed value, or a negative limit in the config file exits 2 with an error naming the key and the file. --config PATH reads from a specific file; a missing or invalid TOML path exits 2 with an error naming the path.

Suppressing a finding

Add a # pymaxlines: disable comment to exempt a file or function from the checks instead of raising the global limit.

File-level — place the directive on a comment-only line before the first statement (after the module docstring is fine):

"""This generated module is intentionally large."""
# pymaxlines: disable=max-lines

import re
# ...

Function-level — trail the directive on any line of the def header, from def through the colon. On a decorated function, place it on the def line, not a decorator line:

def big_handler(
    request: Request,
    db: Session,
):  # pymaxlines: disable=max-lines-per-function
    ...

Bare disable# pymaxlines: disable without =rule disables every rule at its scope. At file scope it suppresses both the file check and every function check; on a def line it exempts that function.

Separate several rules with commas: # pymaxlines: disable=max-lines,max-lines-per-function. Only one directive is allowed per line — a second pymaxlines: segment is an error.

A directive can share its # line with other comments: def big_handler(...): # noqa: C901 # pymaxlines: disable.

Any # segment whose first word is pymaxlines (any case, not followed by a word character) is a directive attempt and must use the canonical pymaxlines: <action> form. A segment that matches but does not parse fails the run — a wrong-case prefix (PYMAXLINES:), a missing colon (pymaxlines disable), and a non-colon separator (pymaxlines-disable) are common examples, but any non-canonical form triggers the same error. A word character immediately after pymaxlines suppresses the check, so pymaxlines_disable is not treated as an attempt.

Each rule is only valid at certain scopes:

Rule Valid scopes
max-lines file
max-lines-per-function file, def

An unknown rule name, a malformed directive, or a misplaced directive fails the run with a diagnostic message, even when the file is within its limits. A directive is misplaced when it appears on a comment-only line after the module's first statement, inside a function body, or trailing a non-def statement (including decorator lines). Naming a file-scope-only rule on a def line is a separate error — # pymaxlines: disable=max-lines on a def line fails with rule 'max-lines' does not apply to a function; use max-lines-per-function.

Pass --report-unused-disable-directives to fail the run on directives that suppress no findings. pymaxlines reports a directive for a disabled check (limit 0) as unused. It reports a bare disable as unused only when neither check would have fired, and never reports a directive that already failed validation.

What counts as a code line

By default (all --skip-* flags on):

  • Blank lines, comment-only lines, and standalone docstrings (module, class, or function) are free.
  • Whitespace-only lines inside a multi-line string are free.
  • Every other line counts once, including non-blank lines inside multi-line strings.
  • A function's count includes its def line and every code line up to the end of its body, nested functions included.
  • Methods, async functions, and nested functions are each checked against the per-function limit; lambdas are not.
  • A function-level directive exempts only the function whose def line it sits on — a nested function still needs its own directive.
  • Decorator lines count toward the file total but not toward the decorated function's count.

Turning a --skip-* flag off (e.g. --no-skip-blank-lines) makes that category count toward both file and function totals.

pymaxlines reports unreadable files and directories it cannot list (missing, permission denied, undecodable, or syntax errors) and fails the run.

Contributing

Install Task, uv, and dprint, then run task install to sync dependencies and install the git hooks. task --list shows the full development workflow. task check runs every hook; task test:matrix runs the suite on each supported Python version.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymaxlines-0.5.0.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymaxlines-0.5.0-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file pymaxlines-0.5.0.tar.gz.

File metadata

  • Download URL: pymaxlines-0.5.0.tar.gz
  • Upload date:
  • Size: 20.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymaxlines-0.5.0.tar.gz
Algorithm Hash digest
SHA256 8036f95a71c226d801e4999a2acf7a69e2b93645beea805157f59db162e18c38
MD5 aaa4f0064495804c2123b875a1176fd9
BLAKE2b-256 7caab1d21b40eef99c5b01cc7553bb36b970d15729fee08d6b63d600e485e3df

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymaxlines-0.5.0.tar.gz:

Publisher: publish.yml on jeffzi/pymaxlines

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymaxlines-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: pymaxlines-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymaxlines-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 840695de7d8cff07d8c7029d4b7ec61c56573fa8d7fd70103dfeec1381c92e5a
MD5 3ce2679d73bfd889763c88190ee76e60
BLAKE2b-256 f79affe5e7e6a8faf5f0179b66e1dcecf989e3381ff88dacd67e79605d11d26d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymaxlines-0.5.0-py3-none-any.whl:

Publisher: publish.yml on jeffzi/pymaxlines

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.0

2 files

0.6.0

2 files

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 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