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 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. 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 break reshapes only a pair that already exists and never adds one, so an over-budget expression carrying no parentheses is left for line-overflow to report. It reaches an operator chain alone and leaves any other interior at the layout its author wrote. A pair wrapping one operand of a wider chain 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 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.

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 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.

result = (alphas + betas) or (gammas + deltas) is written with each pair broken across rows, under a code-line-length of 40. 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:.

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 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 :.

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

The if condition in check is (flags.alpha or flags.beta) and (flags.gamma or flags.delta), over the code-line-length of 40. Both pairs stay as written, 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 a row that overflows either way. The overflow is left for line-overflow to report.

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 returns 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.