Skip to main content

pymaxlines

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

A pre-commit hook that fails when a Python file or function has too many lines of code.

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

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.3.0
    hooks:
      - id: check-max-lines

To run it outside pre-commit:

uvx pymaxlines file1.py file2.py

When the package is installed in an environment, python -m pymaxlines is equivalent to the pymaxlines command and lets you pick the interpreter explicitly.

Usage

The hook runs on .py files. A file is a test file when the path passed on the command line contains a tests/ component, or the filename matches test_*.py or *_test.py. Everything else is a source file. The flag table below uses these terms in its "Applies to" column.

Defaults, all overridable with 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 (unreleased — ships after v0.3.0)

--max-lines and --max-lines-test have no disable value — 0 fails any file containing code. All four limit flags reject negative values.

Pass flags through the hook's args:

- id: check-max-lines
  args: [--max-lines, "300", --max-lines-per-function, "40"]

Each --skip-* flag has a --no-skip-* counterpart. To count comment-only and blank lines:

- id: check-max-lines
  args: [--no-skip-comments, --no-skip-blank-lines]

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.

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) is a directive attempt and must use the canonical pymaxlines: <action> form. pymaxlines rejects three near-misses and fails the run: a wrong-case prefix (PYMAXLINES:), a missing colon (pymaxlines disable), and a non-colon separator (pymaxlines-disable).

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 (missing, 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.4.0.tar.gz (13.3 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.4.0-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pymaxlines-0.4.0.tar.gz
Algorithm Hash digest
SHA256 1101d51baeb8f4a99b26c6b8f43459c5340919b05d03b5273e4aca024077f24e
MD5 6161ebbb31bbd2efa4fdca10b4b55d1c
BLAKE2b-256 2578911c71e74ea31a3783be4ee6904488fb52a0e609f733817ecfcf0347d1d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymaxlines-0.4.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.4.0-py3-none-any.whl.

File metadata

  • Download URL: pymaxlines-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 14.3 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.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cc1b36e6a09fc45e8f730ac8eef65ba0c358e1bd5fef9f4c905d9f3b0a6f7a6f
MD5 7d923d7b0ae48c1067eb2666a805e11b
BLAKE2b-256 f902fcfcd11f7460c9efe4b2568d6da9c0b1f2cb5c48d2f0da4b150e8646af61

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymaxlines-0.4.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

0.5.0

2 files

This release

0.4.0 This release

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