Skip to content

reflow-parentheses ​

reflow-parentheses removes a parenthesis pair that groups nothing, one wrapped around an expression only to span lines or out of habit, and breaks a pair whose joined form crosses code-line-length into a row per operand, since that pair is the one bracket able to bring the row back inside the budget.

The removal test is structural rather than textual, so a pair is removed only where removing it leaves both the parse and the grouping a reader sees unchanged. That test keeps each of the following pairs in place:

  1. A precedence-bearing pair such as (a + b) * c, because dropping it would rebind the multiplication.
  2. A generator and a walrus binding, each keeping the parentheses the grammar requires of it.
  3. The parentheses that form a one-element tuple, which are part of the tuple rather than a wrapper around it.
  4. A pair around one boolean operator inside a chain of the other, since (a and b) or c parses the same without it and would then read as three peers rather than two.
  5. A pair whose interior carries a comment, since closing the break would move the comment off the line it describes.

A multi-line pair joins onto one line when the joined form fits the budget. The join tests whether closing a soft line break would change the spacing inside a string rather than what kind of leaf the expression carries, so a comparison against a string literal joins exactly as one against a number does. A run closing against a bracket closes to nothing rather than to a space, so no padding is left behind it. A line break between two parts of a string run stays where its author wrote it, so a pair whose interior stacks a run one part per row never joins, and stack-adjacent-strings keeps the run as written. A pair whose every break sits inside a bracket the interior itself opens, a call's argument list being the common case, is removed in place whatever the joined width, since the pair carries none of those breaks and reflow-calls settles the rows inside.

Where the joined form crosses the budget, the pair breaks rather than staying as the author left it. The opening bracket takes its row alone, the closing bracket opens the row beneath the last operand, and the interior sits between them one indent step in. An interior that fits a row of its own takes that row whole, and one that does not takes a row per operand, each row led by the operator joining it to the row above, so a reader finds every and in one column rather than at three different row ends. An operand its author broke across rows outside any bracket of its own joins onto its row where it fits there, unless the break stacks a string run one part per row. A conditional expression divides at its if and its else, leaving each branch and the test whole on its row. A boolean chain forming the test of an if, elif, or while takes a row per operand even where it would fit one row, since a test breaks at its boolean operator first. A pair the author already broke takes the same layout, so the operator arrives in the same place whether the rule opened the row or the author did.

The rule also adds a pair where none exists, around a boolean chain or a conditional expression, written on one row or spread across rows only inside pairs its author wrote, that forms the test of an if, elif, or while, the value of a return or an assignment, or the last element of a bare tuple standing as such a value. Each of those ends its statement's row outside every bracket, so the added pair opens at the end of the head and closes the statement, and the row breaks at the expression's own operator before reflow-calls reads it, leaving a call inside an operand inline wherever its row now fits. An over-budget expression anywhere else carrying no parentheses is left for line-overflow to report.

The if condition in latest carries no parentheses and runs past the 88-column budget. The rule adds a pair around the condition and lands each operand of its and on a row of its own, the layout a test takes whether or not its joined form fits one row inside the pair. The call to abs then fits its row, so reflow-calls leaves it inline rather than breaking its argument list open.

def latest(transcript, moment):
    stamps = []
    if transcript:
        for path in transcript.parent.glob("*.jsonl"):
            with suppress(OSError):
                written = path.stat().st_mtime
                if (
                    path != transcript
                    and abs(written - moment) <= RECENT.total_seconds()
                ):
                    stamps.append((written, path))
    return max(stamps, default=(0.0, None))[1]
python

The break reaches an operator chain and a conditional expression alone and leaves any other interior at the layout its author wrote, and a conditional its author spread across rows keeps those rows. A pair wrapping one operand of a wider chain or of a conditional expression is left as written too, since opening rows inside a row that overflows either way gains the reader nothing. A pair sitting inside a bracket the rule leaves standing is left as written for the same reason, so the construct that bracket belongs to lays out the rows around it.

Both directions measure a row as the later rules leave it, at the column strip-stranded-padding and align-equals settle it to and once reflow-calls has exploded the calls ahead of the pair, and a pair's row ends at the opening paren of a later pair on it that breaks. The measure also counts each rewrite prefer-fstring, shed-super-args, and simplify-comprehensions makes on the row, so a row one of them brings back inside the budget ends on one line.

Both directions are written in one pass, so a pair nested inside another redundant pair is removed in the same run and each pair tests its own join against the text the earlier removals produce. A break writes its operands through those same removals, leaving the rows it opens carrying the text the rule leaves rather than the text it was handed.

Configuration ​

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.

The Canonical Case ​

total = (base + tax) and label = (name) each wrap an expression that parses the same without its parentheses. Both pairs are removed, leaving total = base + tax and label = name.

total = base + tax
label = name
python

More Examples ​

status = ( opens a pair around an operator chain whose first operand compares against the string "ready". The interior joins onto one row and the pair is removed, exactly as one comparing against a number would, because the join test reads whether closing a soft break would respace a string's interior rather than what kind of leaf the chain carries, and no string here spans a row.

run returns a bare tuple whose last element is a conditional expression, on a row past the 88-column budget. The rule adds a pair around that element, its ( ending the return row and its interior whole on the row beneath, because the interior fits that row, so the call to loads stays inline on it.

ready = ( opens a multi-line pair around (server.up) and (cache.warm), each wrapped in a redundant pair of its own, under a code-line-length of 34. The join test measures the interior with those two inner pairs already removed, so the expression fits and joins to ready = server.up and cache.warm.

The if condition (matched and counted or ignored and skipped) is over the code-line-length of 40. The break cuts at the or alone, leaving each and group whole on its row, so the two rows show the structure the expression has, whereas cutting at every operator would write four peer rows that read as one flat run.

The elif condition is over budget and carries two nested pairs, one grouping an and inside the surrounding or chain and, inside it, a redundant one around the long nextchar not in comparison on a continuation row aligned to self.whitespace_split. The outer pair breaks into a row per operand, the redundant pair is removed, and its continuation row moves left with the token it was aligned to, whereas the grouping pair stays as written across its two rows, its and still trailing the first of them.

ready = ( opens a multi-line pair around server.up and cache.warm under a code-line-length of 32. The interior joins onto one line and the pair is removed, because the joined form ready = server.up and cache.warm fits the budget exactly.

Inside f, return('<not given>'), elif(y):, return(x), assert(x), yield(x), and return (x)if y else(x) each write a pair directly against a keyword. Each pair is removed and replaced by a single space rather than by nothing, on the closing side as much as the opening one, so return('<not given>') becomes return '<not given>' and elif(y): becomes elif y:. That space is what lets a pair whose bare form would fuse into the keyword, return(x) reading as returnx, be removed at all.

The if condition in check is (flags.alpha or flags.beta) and (flags.gamma or flags.delta), over the code-line-length of 40. The condition gains a pair and breaks at its and, and both operand pairs stay as written on their rows, because each carries precedence, wrapping an or group inside the and, and each wraps one operand rather than the whole condition, so breaking either would open rows inside the break around it.

The redundant pair inside g(...) is removed, which moves everything inside it one column left. The continuation row was written at the column where the second operand starts on the opening row, and a row aligned to a code token follows that token, so it moves one column left as well. A row whose indent only happened to sit one indent step in would have stayed where it was.

assert (alphas + betas) or (gammas + deltas) is written with each pair broken across rows, under a code-line-length of 38. The first pair joins and is removed, and the second is measured against the line that join wrote, so joining gammas + deltas up as well would push the line past the budget, and its pair and rows stay as written.

def total() -> (int): wraps the single name int in a pair that changes nothing about how the annotation parses. The pair is removed, leaving def total() -> int:.

Each test is a boolean chain whose author broke the comparison operand after its ==, and each runs past the 88-column budget joined, so the rule stacks one operand per row. The while test's comparison joins onto its own row, whereas the if test's keeps the break after ==, because the joined comparison fits its row in the first and would run past the budget in the second.

Each value is a % format whose string run its author stacked one part per row. The rule never joins the parts of a run onto one row, so msg breaks into a row per operand although its joined form would fit the budget, and differing, whose joined form would not, takes the same layout, each run keeping its second part on a row of its own.

Inside gg(...), removing the redundant pair moves the rest of the opening row one column left, and the continuation row beneath it is indented with a tab. The continuation row keeps the indentation it was written with, because a tab has no fixed column count, so no single-column deletion moves the row left by one.

The value bound to label is a conditional expression too wide for one row even inside a pair. The rule adds a pair and lands the conditional on three rows, the if and the else each leading the row they open, so each branch and the test read on rows of their own.

The elif condition in pick runs past the 88-column budget by one column, while its joined form would fit the row inside a pair at 87. The rule adds a pair and still lands each operand of its and chain on a row of its own, because a test breaks at its boolean operator first whatever room the row inside the pair leaves.

The if condition in gate was already broken across three rows by its author, with each and left at the end of the row above, under a code-line-length of 40. The pair is rewritten exactly as an unbroken over-budget pair would be, so the opening ( ends its row, each operand takes a row of its own one indent step in, and every and moves to the start of the row its operand opens, leaving all three in one column.

The if condition in dispatch sits on one row over the code-line-length of 40, and its interior does not fit on a row of its own either. The pair breaks into one operand per row, so the opening ( ends its row, each operand takes a row one indent step in with its and at the start, and the ) closes on a row of its own ahead of the :.

The if condition in latest carries no parentheses and runs past the 88-column budget. The rule adds a pair around the condition and lands each operand of its and on a row of its own, the layout a test takes whether or not its joined form fits one row inside the pair. The call to abs then fits its row, so reflow-calls leaves it inline rather than breaking its argument list open.

ready = ( wraps server.up and cache.warm under a code-line-length of 31, one column short of the joined form's 32. The pair breaks rather than joining, and the interior takes one row whole one indent step in, because it fits that row, so the chain is not split into one operand per row.

Inside the report_invalid_enum_member_names call, the pair around ", ".join(repr(name) for name in invalid_names) opens at the end of one row, holds its interior on the next, and closes on a third row of its own, and joining onto one row would overflow the budget. The pair is removed in place, its interior row left where it was written and its closer's row deleted so the call's ) closes the interior row, because the call's own parentheses already carry the break, so neither the joined width nor the columns the pair sat at matter.

Inside gg(...), removing the redundant pair moves the second argument that follows its ) left along the same row. A later row aligned to that argument moves left by the same number of columns, so it stays lined up under its own opener rather than staying at the column the opener used to sit at.

The comprehension filter opens with if ( ending its row, with the operands on the rows beneath it and, on the last row, a pair grouping an and inside the surrounding or chain. The outer pair is removed in place and the rows beneath it stay where they were written, because an opener that ends its row leaves nothing on that row to align to, whereas the inner pair stays as the group it marks.

Inside gg(...), a redundant pair is removed, and a nested ( ends the first row with two rows indented beneath it. Those rows keep their column, because they are indented under an opener that ends its row rather than aligned to anything the removal moves, even where that column happens to match the one directly past the removed opener. Matching columns are not evidence of alignment.

No Change

squares = (item for item in items) wraps a bare generator expression. The pair stays as written, because Python requires those parentheses for the generator to parse at all.

No Change

single = (item,) uses its parentheses to form a one-element tuple rather than to wrap a complete expression. The pair stays exactly as written, because it is part of the tuple.

No Change

total = ( # running sum opens a pair whose first line carries a comment. The pair and its multi-line form stay exactly as written, because joining the break would move # running sum off the line it describes.

No Change

held = (alpha and beta) or gamma wraps an and group inside an or chain, and same = (alpha and beta) and gamma nests and inside and. Both pairs stay, for different reasons. The first parses the same without its parentheses, so the pair is what shows the reader two groups rather than three peers, whereas removing the second would change the parse, turning a nested BoolOp into a flat three-value one.

No Change

ready = ( wraps server.up and (cache.warm or cache.cold) under a code-line-length of 46. The inner pair stays, because server.up and cache.warm or cache.cold would regroup the parse, so its two characters count toward the joined width the outer pair is measured at. The joined form would reach 48 columns, so the outer pair keeps its break rather than joining past the budget.

No Change

scaled = (base + bonus) * factor and power = 2 ** (depth + 1) each wrap a + expression inside an operator that binds tighter. Both pairs stay exactly as written, because removing either would rebind the expression to a different result.

No Change

result = (count := total) wraps a := binding, and the pair stays and the line passes through unchanged, because a pair around a walrus is never removed, so the walrus reads the same in every position rather than as a positional exception.

  1. The if condition is wrapped in a grouping pair, with the line break inside the find call's own parentheses rather than inside the pair. reflow-parentheses joins the interior onto one row and drops the redundant pair in a single pass, because the pair owns none of the breaks, leaving reflow-calls a call that already fits its row, so it changes nothing.

  2. _c asserts two calls joined by and, each wrapped in a redundant grouping pair, on a row past the 60-column budget. reflow-calls explodes the first call and puts its closing ) back at the statement's indent, and reflow-parentheses measures the second pair from the column that closer leaves, where it fits, so the pair's interior joins onto that row and both pairs drop, rather than the second staying wrapped for a width the explode has already removed.

For per-statement opt-outs, the Suppression chapter covers the # prose: skip[reflow-parentheses] directive, which keeps every line a wrapped statement spans as written.