Skip to content

reflow-collections

reflow-collections expands a multi-entry collection literal across lines once its single-line form overflows code-line-length, and leaves a literal that fits on its row. It reaches dict, list, set, and tuple literals, a tuple only where it carries its parentheses.

Inside the expanded form, a non-atomic entry (a function call, a nested collection, a computed expression) takes a row of its own, whereas a run of atomic entries (ints, floats, strings, single-name identifiers) packs across as few rows as fit, each row taking at most max-atomics entries within the budget. A non-atomic entry in the middle of such a run splits it into two runs packed on their own.

The rule runs the inverse move too, rejoining a construct broken somewhere other than an entry boundary, and the rejoin reaches a multi-line subscript, a collection used as a dict key, and a comprehension broken across its for and if clauses. A subscript and a comprehension only ever rejoin and never expand the way a literal does, so one too wide to fit, or one carrying a comment or a multi-line string, keeps its breaks. A construct that would overflow once joined keeps its break, as does one whose nested call is already written one argument per line, carries a comment, or passes max-args. A collection inside an f-string replacement field is left as written whatever its width.

A literal the author already laid out as a bracketed column, its opening bracket ending a line and its closing bracket opening one, is the exception to the rejoin, because its breaks already sit at entry boundaries. keep-multiline-literals, on by default, keeps it multi-line and re-expands it to the canonical layout rather than joining it back, so the column survives along with the : alignment align-colons pads. Keeping a column this way needs two or more entries. A construct enclosing a kept literal has no one-line form either, so it keeps its break as well, whereas any other break rejoins.

A member the expansion keeps as written rather than laying out moves whole into the expanded form, its continuation rows shifted to the column the entries sit at, so a kept call or subscript reads under its siblings. A member whose rows align under its own opening bracket keeps the whole construct as written instead, and one running through a multi-line string keeps the string's own columns.

A dict expands once it has more than max-dict-entries entries whatever its width, and every collection enclosing it expands with it.

A dict entry whose key: value width overflows the budget breaks at the : and hangs its value one indent step in, row by row rather than across the whole literal, a layout only a dict takes. Setting wrap-dict-entries to false leaves such an entry on one line.

Every width the rule reads counts the separator closing an entry's row at the position alphabetize-siblings leaves it in, on the rejoin as well as the expansion. An entry the sort moves last sheds the comma it carries and one the sort moves up gains one before either is measured, so the layout the rule picks stays put once the sort is written. Each construct is then measured at the column it settles at:

  1. A literal written on one row is measured at the width strip-stranded-padding settles it to, past the padding inside its brackets and at one space after each :, which is the width a rejoin writes it back at.
  2. A member the expansion moves keeps the calls inside it measured at the columns its rows end up on, and a call the move pushes past the budget explodes in the same pass.
  3. A literal following one the rule expands on the same line is measured where that expansion leaves it, on the closer's row at the statement's indent rather than under the continuation column the source wrote.
  4. A dict value whose key the rule lays across rows is measured from the key's last row.

Each move sits behind its own facet, explode gating the count trigger as well as the width one, whereas the rejoin has none.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.
explodebooltrueExplodes a collection that overflows the line budget or exceeds its entry cap to one entry per line. false turns every explosion off, leaving the count cap with no effect.
keep-multiline-literalsbooltrueKeeps a literal the author wrote as a bracketed column of two or more entries, one per line. false joins one back onto a single line where it fits the budget, and any other multi-line layout rejoins either way.
max-atomicspositive int | false8Caps how many atomic entries, meaning ints, floats, strings, and single names, one packed row of an expanded collection carries. false removes the cap and packs each row by width alone.
max-dict-entriespositive int | false3Expands a dict once its entry count exceeds the cap, whatever its width. false disables the count trigger.
wrap-dict-entriesbooltrueBreaks an over-wide key: value at its : and hangs the value beneath. false leaves the oversized entry on one line.

A short tuple inside a call's argument list, like numpy.zeros((3, 4)), stays inline, since it fits the budget and max-dict-entries reads dicts alone. A dict literal with eight non-atomic entries expands whatever its length. A four-entry dict expands at the default max-dict-entries of 3 even when it fits the line.

The Canonical Case

short_dict has three entries, long_dict has seven on one line, single_entry_dict is one entry broken across rows, and held_dict is three entries written one per line. long_dict expands to one entry per line and reads as a column of key: value pairs, because it passes max-dict-entries, whereas short_dict stays inline, held_dict keeps its column because keep-multiline-literals is on by default, and single_entry_dict rejoins onto one line because a one-entry dict has no column to keep.

short_dict = {"alpha": 1, "beta": 2, "gamma": 3}
long_dict = {
    "alpha": 1,
    "beta": 2,
    "gamma": 3,
    "delta": 4,
    "epsilon": 5,
    "zeta": 6,
    "eta": 7
}
single_entry_dict = {"default_action": "noop"}
held_dict = {
    "alpha": 1,
    "beta": 2,
    "gamma": 3
}
python

More Examples

bare and parens each list seven names, one without parentheses and one with, and both lines run past the budget. bare is left as written and parens expands into packed rows, because a tuple without parentheses has no bracket pair to break inside, whereas a parenthesized tuple expands the same way a list does.

The tuple key (alpha, beta) in mapping and the subscript key registry[lookup_index] in keyed are each broken across rows, and the tuple key in overflow is too wide to rejoin. The keys in mapping and keyed rejoin onto one row, so each reads as one member the alignment and ordering rules can place in a run, whereas the key in overflow expands to one element per row because its joined form would pass the budget.

mixed_atomics lists literals, names, attribute chains such as pkg.CONST, and unary operands such as ~mask, all of which count as atomic, whereas split_by_call puts the call lookup() between two runs of strings. mixed_atomics packs as one run across two rows, and in split_by_call the call takes a row of its own with the strings before and after it packed as two separate runs, because a non-atomic entry splits the run around it.

selected subscripts a four-entry dict with [chosen_key] on one line. The dict expands to one entry per line and }[chosen_key] closes it on the last row, because the dict passes max-dict-entries, and the enclosing subscript reads the expanded dict as a break it cannot rejoin, so the construct reaches one layout rather than alternating between expanded and rejoined on each run.

over_threshold has four entries and fits its line, under_threshold has three, and already_expanded has four written one per line. over_threshold expands to one entry per line, because it passes the default max-dict-entries, whereas under_threshold sits at the cap and stays inline, and already_expanded keeps the layout it has.

held_list is written as a bracketed column, its opening [ ending a row and its closing ] opening one, the registry subscript is broken inside its brackets, and the tuple key in repaired_collection_key is a bracketed column of its own. held_list keeps its column and packs onto one row, and the subscript and the tuple key rejoin onto one line, whatever keep-multiline-literals is set to, because a subscript and a dict key whose joined form fits always rejoin, and only a literal's own entry boundaries make a column the author built.

nested lists alpha, a helper call already written across rows, and zeta in a list that already spans rows. The list expands one entry per row, and the call moves whole with its beta and gamma rows re-indented one step under helper(, because a member already laid out across rows is kept rather than laid out again, and its continuation rows shift to the column the entries sit at.

code-line-length = 50 is set, and the yield inside walk carries two tuples, the first past the budget and the second written under a continuation column. The first tuple expands to one component per row and the second packs beside the first's closing ), because a literal following an expanded one on the same line is measured where that expansion leaves it, on the closer's row at the statement's indent, rather than under the continuation column the source wrote.

class_name, trailing_bracket_shares_its_line, soft_wrapped_set, soft_wrapped_spread, and soft_wrapped_single are each broken with an opening bracket sharing a row with an entry or a closing bracket trailing one, whereas tuple_key_column puts both brackets on rows of their own. The five rejoin onto one line, the one-element tuple keeping the trailing , that makes it a tuple, and tuple_key_column keeps its column, because a break that sits on no entry boundary is a wrap rather than a column the author built, and only the trailing , after "second": 2 is dropped as the kept column is written in the canonical layout.

small_matrix fits its line, and tall_matrix lists five inner lists past the budget. tall_matrix expands one inner list per line and each inner list such as [100, 200, 300] stays inline, because each inner list fits at its own column, so the matrix reads one row per line rather than one cell per line.

code-line-length = 40 is set, and queued_record_ids and pending_batch_key would share a row at exactly forty columns if they packed together. All three entries end up one per line, because resolve(key) is a call and takes a row of its own, so the row above it has to close with a ,, and that comma counts toward the budget and takes the row to forty-one columns.

code-line-length = 40 is set, and the tuple key in lookup runs past it with the value (1, 2) and a trailing comment after it. The key expands to one component per row and the value stays on the closing row beside the :, because the value is measured from the column the : sits at on the key's last row rather than from the key's rows read as one line, which leaves room for (1, 2).

config, mixed, and multiple_hung_rows each carry an entry whose key: value row runs past the budget, and nested_value_passes_through carries an over-wide nested dict under "limits". Each over-wide entry breaks at its : and hangs its value one indent step in, because a dict entry that overflows breaks row by row rather than across the whole literal, so "alpha" and "beta" stay on one row, mixed hangs a row between fitting siblings, multiple_hung_rows hangs two rows in one dict, and the "limits" value expands as a nested dict rather than hanging.

short_list fits its line, long_list lists seven city strings past the budget, and held_list is five integers written one per line. long_list expands and its seven strings pack onto one row inside the brackets, because atomic items pack across as few rows as fit under code-line-length and max-atomics, whereas short_list stays inline and held_list keeps its column and packs onto one row as 2, 3, 5, 7, 11 rather than rejoining.

atomics lists ten integers and nonatomics lists six calls, each in a parenthesized tuple past the budget. atomics packs its integers across two balanced rows and nonatomics puts one call per row, because a parenthesized tuple takes the same layout as a list, with atomic entries packing across as few rows as fit under code-line-length and max-atomics and a non-atomic entry taking a row of its own.

list_comp, dict_comp, set_comp, and gen_exp are each broken across rows, and each joined form fits the budget. All four rejoin onto one line, because a comprehension is one expression rather than a run of entries, so the rule only ever rejoins one and never expands it, and one too wide to fit would keep its breaks.

cascade, tiered_dict, shallow_dict_in_dict, and walks_through_singleton each nest a collection inside another on one over-wide line, and outer_holds_with_inner and outer_pinned_inner_holds each carry an inner list written across rows. Every nested collection expands or stays inline on its own width at its own column, rather than following whatever the outermost one does.

In cascade the outer list expands and its over-wide first dict expands with it while {"name": "bob", "role": "user"} stays inline, in tiered_dict the four-entry inner dict under "primary_database" expands on its own count and width while "cache": {"ttl": 60} fits, and walks_through_singleton keeps its [{ tight while the six-entry dict inside it expands. The inner list of outer_holds_with_inner keeps its column and packs onto one row inside an outer list that keeps its own column, whereas outer_pinned_inner_holds keeps the same inner column under an outer list its long strings already keep past the budget.

code-line-length = 30 is set, and checksums lists two seven-digit and two twelve-digit integers on one line. The list expands and its last row packs both twelve-digit integers at exactly thirty columns, because no entry follows that row, so it carries no trailing , and none is counted, whereas counting one would have taken the row one column over and split the pair.

keep-multiline-literals = false is set, fitting_joins is written one item per line with a joined form that fits, and overflowing_explodes runs past the budget on one line. fitting_joins rejoins onto one line as [10, 20, 30] and overflowing_explodes still expands into packed rows, because the facet releases only the kept column, leaving the width trigger under explode in force.

wrap-dict-entries = false is set, and the config dict runs past the budget on one line with an entry keyed extremely_long_key_name_that_pushes_its_row_past_the_budget that passes the budget even on a row of its own. The dict expands one entry per line and the over-wide entry stays on one row rather than breaking at its :, because the facet turns off only the hang, leaving the width and count triggers in force.

No Change

The list [alpha, beta, gamma] sits inside a replacement field of the f-string passed to ValueError, on a row past the budget. The list is left as written, because the rule never rewrites inside an f-string's replacement field, so no line break is ever written into the string.

No Change

The dict inside the f-string in ROWS carries four entries, one past max-dict-entries, the count that normally expands a dict and every collection enclosing it. Neither the dict nor the ROWS list expands, because a collection inside a replacement field is outside the rule's reach, so its count never fires the trigger for the list around it.

No Change

one_element is a one-element tuple past the budget with its trailing ,, and grouping is a parenthesized sum past the budget. Both are left as written, because breaking a single element across rows gains nothing, and a parenthesized expression is not a tuple, so the rule does not reach it whatever its ( ) spelling.

No Change

canonical_inline fits on its line, canonical_multi_line already sits one entry per line with a joined form that would overflow, and canonical_hung_entry already hangs its over-wide entry's value on the row after the :. All three pass through with no edit and nothing is reported, because each already sits in the layout the rule would write.

No Change

explode = false is set, and the config dict runs past the budget on one line because of its extremely_long_key_name_that_pushes_its_row_past_the_budget entry. The whole dict stays on its line, because the hang that breaks an over-wide entry at its : is part of the expansion the facet turns off.

No Change

explode = false is set, overflowing_list runs past the budget, over_count_dict has five entries, and already_multiline is written across rows. All three are left as written, because the facet turns off both the width trigger and the count trigger, and the already multi-line list keeps its column under keep-multiline-literals.

  1. The "alpha" and "beta" arms of dispatch each return a long inline dict, and the wildcard arm returns None. Each dict explodes to one entry per line, its keys sort so "comment_text" leads, and the : of each entry pads into one column within its arm, while align-match-case joins the wildcard's return None onto its case _: line.

  2. A one-line configure call carries a four-entry dict in its settings keyword. reflow-collections explodes the dict, reflow-calls re-indents it to the keyword column, alphabetize-siblings sorts the keywords and the dict entries, and align-equals and align-colons pad the = and : columns. The dict entries sit one indent step past the keyword column, with the closing } back at the keyword column.

  3. deque([(start, 0)]), {start} assigns a call and a one-item set on one row that crosses the 50-column budget. reflow-collections never breaks a one-item set, so the row's tail counts at its full width rather than only up to its {. deque( explodes with its list argument packed on a row of its own, where it fits, rather than the inner tuple exploding under a column the explode would then have moved.

  4. The list in the cccc keyword of g crosses the 40-column budget, and its first member is the call fn(100, 1000). reflow-collections explodes the list, and reflow-calls leaves fn(100, 1000) on one line, because exploding it would measure against a tail the explode is about to remove, so the call is measured where its entry ends up and fits there.

  5. The tuple on the left of the = breaks before its closing parenthesis, which the source indents under the tuple's first element. reflow-collections moves that closer to the indent of the row its opener sits on, the statement's own indent. reflow-calls then explodes the type(self).prepare_context(...) call after the closer with its arguments hanging from the statement's indent, because the call is measured from that row rather than from the continuation row the source wrote.

  6. One key of _hash_action carries a stranded space before its closing parenthesis, (False, False, False, True ), and strip-stranded-padding removes it. align-colons pads the run onto one : column, because it measures each row at the width it has once that removal is written, whereas the padded width would have read as a breach of the 40-column budget and the run would have skipped its column over a breach that never happens.

  7. CONFIG is written as one overlong line with its keys out of order. The dict explodes to one entry per row, the keys sort, and every : pads into one column one space past the widest key, "beta_extended", so even the short "zeta" row takes the full padding to reach that column.

  8. tagdefs writes two spaces after each key's :, and alphabetize-siblings moves "stderr" ahead of "stdout", the comment above "stdout" moving with it. The "stdout" value, last after the sort, stays on one row at exactly the 40-column budget, because reflow-collections measures a value from the plain ": " past its key rather than from the two spaces the source wrote, and the row is written back at that width, whereas the "stderr" value explodes.

  9. __all__ lists six entries on one overlong line, three short names interleaved with three thirty-character ones. alphabetize-siblings sorts them so the short names lead, and reflow-collections packs them onto rows within the 88-column budget. The packing measures the widths the sort leaves, not the widths as written, which is why "cc" shares a row with the first long name rather than joining the two short entries above it, and the six entries settle onto three rows of two with no row widening afterward.

  10. Two tuples sit on one row that crosses the 88-column budget, one inside isinstance(...) and one after in. The tuple inside isinstance comes first, so the overflow goes to that call, and the rest of the row counts at its full width rather than only up to the second tuple's (. isinstance( explodes with toklist and (type(None), basestring, list) each on a row of their own, and (None, "", []) stays on one row, where it already fits, the tuple inside isinstance staying packed as well.

  11. The dict passed to self.call_exception_handler lists 'message', 'exception', and 'loop', each with a trailing comma, and the sort moves 'message' last. alphabetize-siblings settles which entry ends the dict, so reflow-collections measures each entry with the separator that order leaves after it, and the 'message' entry, last after the sort, drops its comma, fits its row within the 79-column budget, and the layout settles once the sort is written.

  12. self.loggerMap is assigned a dict with spaces just inside its braces, and m a dict with no space after any :. strip-stranded-padding removes the spaces from the first and writes a space after each : in the second, and reflow-collections measures each literal at the width those edits leave, so the first stays on its row and only the second, now past the 40-column budget, explodes.

  13. The first element of the set s is written across two lines as (1, and 2). alphabetize-siblings sorts each element by the text it has once joined onto one row, not by the broken text in the source, so (1, 2) sorts after the long tuple.

    Sorting puts the long tuple first, which gives it a trailing comma, and measuring it with that comma takes it one column past the 40-column budget, so it explodes rather than staying on one row.

  14. The % tuple in the TypeError call crosses the 60-column budget, with type(other).__qualname__ as its first element and two closing parentheses stacked at the end of its last row. reflow-collections explodes the tuple to one element per row with its ) on a row of its own, and reflow-calls moves the TypeError closer onto its own row as well. The type(other).__qualname__ call stays on one line, because the explode moves self.__class__.__qualname__ and the two closers off its row, and type(other).__qualname__, alone fits the budget.

  15. d is a dict with # prose: keep at the end of its line, written with "b" before "a" and too wide for the 40-column budget. reflow-collections explodes the dict, which moves the marker onto the closing } line, and the marker still keeps the dict out of alphabetize-siblings, so "b" stays ahead of "a" and the entries keep their order and their separators, because the marker applies from the line of either the { or the }.

  16. _FORMATS lists FMT_BINARY and FMT_XML in the order alphabetize-siblings leaves them, so the last entry gains no separator, and align-colons pads FMT_XML onto a : column that puts its row exactly at the 45-column budget. The column stays, because reflow-collections measures that row at exactly the budget rather than past it, and the padding is not stripped back.

  17. non_adjacent.append( (la, lb, 0) ) carries a space after append( and a space before its closing ), and the row crosses the 40-column budget by exactly those two spaces. strip-stranded-padding removes both, and reflow-collections measures the tuple at the width that removal leaves, so the tuple stays on its row, the first space narrowing the column the tuple starts at and the second narrowing the text trailing it.

  18. self.tk.call(_flatten((self._w, cmd)) + self._options(cnf)) passes two calls joined by +, and the row crosses the 40-column budget. _flatten( explodes directly, because reflow-calls measures it with the + self._options( that trails it on the same row counted in, whereas measuring the call alone would have left the row for reflow-collections to break at the tuple first and the call would then have exploded on a later pass anyway.

  19. PRIMARY overflows its line with values written in the older Optional and Union forms, and target-version = "3.10" allows the | syntax. modernize-annotations rewrites each value to | form first, so every rule downstream measures the shorter text, and the dict then explodes, its keys sort so "delta_long" moves ahead of "gamma", and align-colons pads the : column against the rewritten widths. With nothing left reading either name, from typing import Optional, Union is removed.

  20. d opens with { # note and lists "zz" before "a": 1, "b": 2 packed on one row. The comment on the { row keeps reflow-collections from splitting that row, and a packed row is never reordered, so the dict keeps its source order under alphabetize-siblings. The "zz" entry is measured with the comma it carries as written rather than one a sort would have moved, its value is too wide for the 40-column budget, so that value explodes while "a": 1, "b": 2 stay packed on their shared row.

  21. s is a set packed across three rows with # note at the end of the first. The comment inside the set keeps its order fixed under alphabetize-siblings, so no element moves, and reflow-collections measures each element with the comma it carries as written rather than the one a sort would have left. The one element too wide for the 40-column budget explodes onto rows of its own, and the packed rows around it keep their commas and their places.

  22. dirlist.extend([ ... ]) writes a space just inside each bracket of its list, and strip-stranded-padding reports both. reflow-collections explodes the list, rebuilding the bracket interior and carrying that padding away before the strip is written, and the first member call ends up alone on a row inside the exploded list with no padding ahead of it. The _os.path.expanduser(...) call is measured from that row and explodes, because at that column it crosses the 60-column budget, rather than being measured from the row the source wrote.

  23. total is assigned a % template whose tuple arrives exploded, first and second each on their own line. reflow-collections condenses the tuple onto one line first, and prefer-fstring then measures the conversion against the budget and rewrites "%s and %s" % (first, second) to `f"{first} and {second}", each member inline in the template.

  24. specs is assigned a tuple whose last element is tuple(_mapdict_values(statemap)), on a row past the 40-column budget. reflow-calls leaves a literal that reflow-collections is about to explode unwalked, so reflow-collections explodes the outer tuple and lays out the tuple( and _mapdict_values( calls inside it against the rows the explode leaves. Both calls explode at the column they end up at, because at that deeper column neither fits the budget.

  25. The % tuple in warning ends with a self.userCfg[configType].Get(...) call, and exploding the tuple moves that final item four columns right, past the 79-column budget. reflow-collections reshapes the reflow-calls layout of that call against the row it ends up on, so the call explodes in the same run as the move, rather than on a later pass.

  26. Inside ensure_running, the cmd %= (...) statement spans two rows between fds_to_pass and main_kws["authkey_r"] above it and exe and args below. align-equals builds its runs the same way when it reserves as when it aligns, so the multi-row statement closes the run above it, and exe and args pad onto a narrow column of their own rather than the wider one fds_to_pass and main_kws["authkey_r"] set. reflow-collections reads that same run boundary and joins the tuple within the 80-column budget, rather than exploding it from a column the aligner never uses.

  27. handler_map's return annotation crosses the 50-column budget, and the only collection literal on that row is the [Values, list[str]] list inside Callable. reflow-collections explodes that literal, and reflow-signatures keeps self inline, because it reads the opening row as ending at the literal's bracket rather than exploding a lone parameter to recover width the explode is about to give back.

  28. y is a four-element set on one line past the 30-column budget. reflow-collections packs the rows at the widths alphabetize-siblings leaves in each slot, not the widths the source wrote, so the rows fill the budget as densely as a list's would and the sort is written without widening any row.

  29. table carries # prose: skip[alphabetize-siblings] on its closing line, listing "bb" before "aa". reflow-collections ordinarily measures each entry with the comma the sort is about to write, and the skip directive turns the sort off, and with it the measurement against the sorted order, so every entry is measured with the comma it already carries, and "bb" counts a trailing comma and breaks after its : rather than being measured as the last entry.

  30. foo({"alpha": 1, "beta": 2, "gamma": 3, "delta": 4}) passes one dict as its only argument. reflow-collections explodes the dict to one entry per row, and the call explodes around the dict, its ( and { on separate rows and its } and ) on separate rows, because an argument that spans rows explodes the call whatever the argument count and however short the one-line form. Inside, alphabetize-siblings sorts the keys so "delta" moves ahead of "gamma", and align-colons pads each : into one column.

  31. configure(...) carries a coords keyword whose tuple of three calls is too wide for one line. reflow-collections explodes the tuple and reflow-calls re-indents its elements under the keyword, alphabetize-siblings sorts more, name, and other, and align-equals pads each = into one column. coords keeps its source position, because its tuple calls functions such as compute_alpha_coordinate() and an entry whose value runs code is never moved, and the tuple's elements sit one indent step deeper than the keyword rows with its ) back at the keyword column, as a list or dict argument would.