Parsimony
A minimalist line-breaker that adds the fewest breaks to fit the line.
Unlike black/blue/ruff, which explode the outermost bracket first and stack every closing bracket on its own line, Parsimony makes the smallest change that fits an over-long line, using three complementary strategies.
Explode a bracket — open the minimum number of brackets and coalesce adjacent ones:
response = authed_client().get(reverse('forwarding:detail', args=[c.id]))
becomes
response = authed_client().get(reverse(
'forwarding:detail',
args=[c.id],
))
The same applies to the parenthesised containers in def/async def and
class headers — a long parameter list or base-class list is exploded one
element per line:
def some_method(self, param1: str, param2: int, param3: bool = False, param4: float = 0.0):
...
becomes
def some_method(
self,
param1: str,
param2: int,
param3: bool = False,
param4: float = 0.0,
):
...
Break a method chain — split a long .-chain one segment per line.
This is done for lines that can't be shortened by exploding a bracket:
queryset = SomeModel.objects.filter(active=True).order_by('-last_modified_at')
becomes
queryset = (
SomeModel
.objects
.filter(active=True)
.order_by('-last_modified_at')
)
Break a condition — split the boolean operators of an over-long
if, elif or while header one per line:
if some_condition_value and another_condition_value and a_third_condition_value:
...
becomes
if (
some_condition_value
and another_condition_value
and a_third_condition_value
):
...
Each keeps related code grouped and gives long expressions a clean one-operation-per-line shape.
Usage
Format files or directories (recurses for *.py):
parsimony -i src/ # rewrite in place
parsimony --check src/ # print a diff, exit 1 if anything would change
parsimony src/file.py # print formatted output to stdout
Read from stdin and write to stdout:
parsimony < src/file.py
cat src/file.py | parsimony
Installation
pip install kaapstorm-parsimony
Algorithm
While a physical line exceeds LINE_LENGTH, explode one container that
intersects an over-long line, chosen by:
- multi-item containers only (>= 2 args/elements); single-item containers are never opened — see "Contract" below.
- outermost (shallowest) of those.
A "container" is a call, list, tuple, set, dict or subscript, plus the
parameter list of a def/async def and the base/keyword list of a
class. A function's parens are implicit and a class's are optional
(class Bar:), but both explode like any other bracket. A trailing comma
after *args / **kwargs is valid in a def, so the exploded form stays
correct.
Then re-measure and repeat. "Explode" = each element on its own line at
a +4 hanging indent, with a trailing comma, and the closing bracket
dedented to the opening line's indent. Because we only open the chosen
container and never its single-item parents, adjacent openers like
get(reverse( remain coalesced. Coalescing is not about depth, only
about not opening single-item wrappers.
Exploding a container shifts its children one level deeper, so any breaks they already carry are re-indented to match — the same adjustment chain breaking makes.
When no multi-item container intersects a remaining over-long line, and
that line is an if, elif or while header whose condition is a
boolean expression, wrap the condition in parentheses and put a break
before every and / or. Operands the author already parenthesised stay
on one line — a hand-written group is a grouping decision, not a joint.
When neither applies, fall back to breaking the outermost method chain on
it (>= 2 call segments, as shown above): wrap it in parentheses and put
the head plus each .attr on its own +4 line. Bracket explosion is
preferred — a chain is only broken when opening a bracket cannot fix the
line — which keeps breaks minimal. Any brackets already opened inside a
segment are re-indented to stay aligned under their now-deeper segment.
Contract
The tool only ever adds breaks to over-long lines; it never removes or
rewrites existing breaks. This makes it idempotent, safe to re-run, and
safe to combine with hand-formatting. Preserving existing line breaks
is intentional, so it cannot be combined with ruff format, which would
re-flow its output back into the staircase. Pair it with ruff-as-linter
(E501 off) instead.
Multi-item-only is also a correctness guardrail: opening a single-item
subscript would turn x[0] into x[0,] (== x[(0,)]), which changes
meaning. Restricting to multi-item containers avoids that. It also
avoids ugly single-element splits.
Limitations
- Lines long for non-bracket, non-chain, non-condition reasons — ternaries, arithmetic chains, pure attribute chains (no calls), long string literals — are left untouched and reported, not fixed.
- Boolean expressions outside an
if/elif/whilecondition — in anassert, areturn, an assignment — are not broken. Neither is a negated condition (if not (a and b):), nor a boolean expression nested inside a bracket in the header (if check(a and b and c):). - No "join" pass: it will not re-flow code that another tool has already split. It only acts on lines that are physically too long.
- Comments inside brackets and pre-existing trailing commas are not specially handled.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kaapstorm_parsimony-0.1.4.tar.gz.
File metadata
- Download URL: kaapstorm_parsimony-0.1.4.tar.gz
- Upload date:
- Size: 41.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a38a283ce47cbf81c8837583170ef66cef73acd97b022ddb4776acdda3dae6b6
|
|
| MD5 |
79b7ccb8cf29913bd70e79d4ddb699ed
|
|
| BLAKE2b-256 |
28943c555ef7e69563a341c0c9a62ebe6ea0d97c0bd025f6b802a9cc50002de9
|
Provenance
The following attestation bundles were made for kaapstorm_parsimony-0.1.4.tar.gz:
Publisher:
publish.yml on kaapstorm/parsimony
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kaapstorm_parsimony-0.1.4.tar.gz -
Subject digest:
a38a283ce47cbf81c8837583170ef66cef73acd97b022ddb4776acdda3dae6b6 - Sigstore transparency entry: 2329032228
- Sigstore integration time:
-
Permalink:
kaapstorm/parsimony@df54d165e4fab13a6ea70ca8b8406fbad15a4c10 -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/kaapstorm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@df54d165e4fab13a6ea70ca8b8406fbad15a4c10 -
Trigger Event:
push
-
Statement type:
File details
Details for the file kaapstorm_parsimony-0.1.4-py3-none-any.whl.
File metadata
- Download URL: kaapstorm_parsimony-0.1.4-py3-none-any.whl
- Upload date:
- Size: 20.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f48c7eac41c449e07d63b1cac53dee9afd624f2540e95306476194ed2d5e12ea
|
|
| MD5 |
d32ebc7459938569b90c863be97212f8
|
|
| BLAKE2b-256 |
b7d22372c07ccfdb5867cd1c62541543ae2a4e54d389fbcc7c75f2c899f2a762
|
Provenance
The following attestation bundles were made for kaapstorm_parsimony-0.1.4-py3-none-any.whl:
Publisher:
publish.yml on kaapstorm/parsimony
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kaapstorm_parsimony-0.1.4-py3-none-any.whl -
Subject digest:
f48c7eac41c449e07d63b1cac53dee9afd624f2540e95306476194ed2d5e12ea - Sigstore transparency entry: 2329032282
- Sigstore integration time:
-
Permalink:
kaapstorm/parsimony@df54d165e4fab13a6ea70ca8b8406fbad15a4c10 -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/kaapstorm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@df54d165e4fab13a6ea70ca8b8406fbad15a4c10 -
Trigger Event:
push
-
Statement type: