Skip to main content

pymaxlines

PyPI version Python: 3.12 | 3.13 | 3.14 CI status 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 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 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

$ pymaxlines --max-lines 20 --max-lines-per-function 5 src/app/service.py
src/app/service.py:1: Too many lines in module (21 > 20) [max-lines]
src/app/service.py:14: Too many lines in function 'handle_request' (8 > 5, lines 14-22) [max-lines-per-function]
Found 2 errors.

Findings from the two size rules end with [max-lines] or [max-lines-per-function], which are the names you pass to # pymaxlines: disable=<rule>. The [unused-disable-directive] id appears only with --report-unused-disable-directives; it counts as an error like any other finding and cannot itself be suppressed.

Installation

Run directly with uvx (no install needed):

uvx pymaxlines

Or install into a project:

uv add --dev pymaxlines

The package is a standard PyPI wheel with no dependencies — pip install pymaxlines and pipx run pymaxlines work the same way. 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.7.0
    hooks:
      - id: check-max-lines

The shipped hook passes --force-exclude, so exclude patterns from [tool.pymaxlines] apply automatically. --force-exclude matches explicit paths by directory component and ancestor prefix, so bare directory names work while path globs may not. Add args: [--no-force-exclude] to skip them.

Usage

With no arguments, pymaxlines checks every *.py file under the current directory recursively. Pass directories or files to scope the check:

pymaxlines src/ tests/

Discovery skips .git, .venv*, node_modules, __pycache__, .tox, .nox, .eggs, and symlinked directories. Files named explicitly on the command line are checked as given, even if they match a skip directory or lack a .py suffix.

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

pymaxlines --exclude "migrations" --exclude "generated"

pymaxlines matches each pattern against the bare filename, the path relative to the directory being walked, and that path prefixed with the walked directory. A bare name like generated matches at any depth; running pymaxlines src/ accepts either generated/*.py or src/generated/*.py. --exclude on the command line replaces the exclude list from the config file.

A file is a test file when its path, relative to the working directory, contains a tests component, or when its name starts with test_ or ends with _test.py. A tests directory above the working directory does not count; only the filename conventions apply there. Everything else is a source file. Source and test files have separate limits.

Flags

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

The --skip-*, --report-unused-disable-directives, and --force-exclude flags each have a --no- counterpart. All four limit flags reject negative values.

Exit codes

Code Meaning
0 No findings (empty discovery prints a warning and still exits 0)
1 One or more findings, an invalid # pymaxlines: directive, a file that could not be read or parsed, or a broken stdout pipe
2 Invalid usage, negative limit, or config-file error (unknown key, wrong type, unparsable file, or a --config path that does not exist)

Size breakdown

--show-sizes prints a code-line breakdown of every file instead of checking limits, largest first. Counts respect --skip-* settings, so they match what the check enforces. The run exits 0 even when files exceed their limits; only unreadable files, parse failures, directive errors, and a broken stdout pipe produce exit 1.

Function rows show count/limit when the applicable per-function limit is non-zero; other rows show a plain count. A function row's span begins at its first decorator while its count starts at def.

$ pymaxlines --show-sizes --max-lines 20 --max-lines-per-function 5 src/app/service.py
src/app/service.py: 21/20 code lines!
  1-3    imports                 3
  6-22   class RequestHandler   14
  ├ 9-12   def __init__        3/5
  └ 14-22  def handle_request  8/5!
  25-28  def validate          3/5

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"]

Supported keys: max-lines, max-lines-test, max-lines-per-function, max-lines-per-function-test, skip-blank-lines, skip-comments, skip-docstrings, report-unused-disable-directives, force-exclude, and exclude (list of strings). --config, --version, and --show-sizes are command-line only. CLI flags override the config file; absent keys keep their built-in defaults.

Suppressing a finding

Add a # pymaxlines: disable comment to exempt a file or function 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

The directive must share a line with part of the def signature, from def through the closing colon. A comment-only line inside the parentheses is misplaced, and a decorator line sits above the header — a directive on either is reported as misplaced:

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

Directive syntax

# pymaxlines: disable without =rule disables every rule at its scope. Separate multiple 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.

max-lines is valid only at file scope. max-lines-per-function is valid at file scope and on def lines. The directive is case-sensitive and the colon is required; # PyMaxLines: disable or # pymaxlines disable exits 1. Use at most one pymaxlines: directive per line — combine rules with commas instead. An unknown rule, a malformed directive, or a misplaced directive exits 1.

What counts as a code line

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

  • Blank lines, comment-only lines, and standalone docstrings are free.
  • Every other line counts, including non-blank lines inside multi-line strings.
  • A def header counts toward the file total but not toward that function's own count.
  • Nested functions count toward the enclosing function.
  • Decorator lines count toward the file total but not toward the decorated function.
  • pymaxlines checks methods, async functions, and nested functions. It does not check lambdas.
  • A function-level directive exempts only its own def — nested functions still need their own.

Turning a --skip-* flag off makes that category count toward both file and function totals.

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 the pre-commit-stage hooks; task test runs the pytest suite; task test:matrix runs it 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.7.0.tar.gz (24.5 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.7.0-py3-none-any.whl (27.9 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pymaxlines-0.7.0.tar.gz
Algorithm Hash digest
SHA256 9d0393d204125e98ff7c6051be3a6a698aea27d979265e60b74097560f9c633e
MD5 c2d99fa708b98cb52d96fb9a9a6c5299
BLAKE2b-256 131c90145d3219c5bdfcb20273705fba7db13c7e2978ac6dcaa84c87ab9c89a3

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: pymaxlines-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 27.9 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.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d092a045064bfd41cfca2e953b3096e1148db49088c2cc484304177247440b5c
MD5 447eb92d4bb887d9d9932e82f49b380a
BLAKE2b-256 d014d84aec8c13ccdbf76424821454e9cb475254fa2e260f48044a89f83a4a6e

See more details on using hashes here.

Provenance

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

This release

0.7.0 This release

2 files

0.6.0

2 files

0.5.0

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