Skip to content

align-colons ​

align-colons pads the space before : so consecutive entries share one column, and every construct that pairs a name with a value across a : then reads as two columns, names on the left and values on the right:

  • Dictionary keys.
  • Annotated assignments (class fields, and module- and function-scope variables).
  • Function-signature parameter annotations.
  • The name: description entries of every Google-style docstring section (Args:, Returns:, Raises:, and the rest).

A docstring entry with a parenthesized type gets a second column, its ( one space past the widest name in the run, so the types read as a field of their own and the : column then sits past the widest name (type) pair. Both columns resolve in one pass, where the : column measures the widths the type-group padding produces rather than the ones the source wrote. Every aligned row keeps one space on each side of its :. Each docstring section resolves its own column, so a wide Args: entry never moves the Returns: table, and single-statement match arms likewise form a table of their own, one that align-match-case aligns.

The rule reads each context on its own, treating a group as the consecutive members at the same indentation under the same parent construct. A blank line, an own-line comment, or a statement of another kind ends the group. A one-member group takes no padding, since strip-stranded-padding strips whatever it carries, so a one-key dict reads as plain code rather than a one-row table. When the padding shifts a value whose rows continue past its opening row, those rows move by the same number of columns on the terms align-equals sets for its values, so a string part or an operand written beneath the value's first row stays beneath it. In the dict, annotation, and parameter contexts the column stays within code-line-length, each row that moves counted at the column it moves to. A dict entry whose padded line would cross the budget still joins the column where reflow-collections can break its value open, expanding a collection or, while reflow-calls runs and no skip holds the call for it, exploding a call one argument per row, because the value then takes its multi-line layout at the padded column. The value breaks open only where that leaves the run in fewer columns, or in as many columns with fewer rows unpadded, so an expansion that merely moves the unpadded row elsewhere in the run is skipped. Any other row whose aligned line would cross the budget starts a new column rather than pushing its neighbors past the margin. Where alphabetize-siblings sorts the dict, an entry expands this way only when it would be the dict's sole entry spanning rows, whether or not the sort reorders it. A reorder sets a blank line on either side of each entry spanning rows once two do, which would leave the expanded entry in a run of its own, and a dict already in order holds to the same limit so the layout a reorder leaves stays put on the next run. A dict the sort leaves in order keeps the blank lines its source writes, so an entry whose row crosses the budget even without its padding breaks open where it stands and keeps its place in the run. A docstring section has no such cap, because wrap-docstrings runs directly after and rewraps each entry's description to docstring-line-length from the column the padding sets.

A name (type): head at the docstring body indent under no Title-case heading aligns the same way, because wrap-docstrings passes such a head through as written rather than joining it to the paragraph above. Each contiguous run of those heads resolves its own two columns, so prose or a blank line between two runs keeps one run's widths out of the other's column, whereas a head directly beneath a paragraph joins no run at all, because the wrap folds it into that paragraph. A ( written flush against its name documents a call rather than a type, as in divmod(self, other): the pair, so no type column opens and the call keeps the form its author wrote, whereas its : still aligns with the run because the padding sits after the ). The rule reads the module's own docstring as well as every class and function docstring.

Configuration ​

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.
align-docstring-entriesbooltruePads the space before the : of each docstring name: description entry so a run shares one column, and the space before each parenthesized type so the types share a second column. false removes that padding, including any the source already carries, and leaves each ( one space past its name and each : flush against the name or the closing paren, while dicts, annotations, and parameters still align.
max-shiftpositive int | 0 | false16How far apart the widest and narrowest rows of a run may be for the run to still align on one column. A positive N caps that gap, 0 forbids any padding so every row sits flush, and false removes the cap so a run of any width aligns on one column. A row marked # prose: skip stays out of its group.

max-shift limits how much padding one key may take. The rule reads each group of : entries in source order and extends a column while the gap between the widest and narrowest keys stays within the limit, starting a new column at the first key that would exceed it. Setting max-shift to false removes the limit, so a group of any width aligns on one column, and 0 forbids padding altogether, so every : sits flush against its key. The per-rule facets reference covers the full semantics.

align-docstring-entries gates the two docstring columns alone, so the dict, annotation, and parameter contexts keep aligning under max-shift whichever way the facet is set. Some docstring parsers, docstring_parser and griffe among them, read everything before the : as the parameter's name, padding included, so a project that builds help text or API docs from its docstrings sets the facet to false. Unlike turning the whole rule off, which leaves existing padding as written, false removes the padding a docstring run already carries, so each run resolves as it would under max-shift = 0, even where max-shift itself is false.

The Canonical Case ​

The keys of capitals vary in width, from "USA" up to "France". Every : moves to the column one space past the widest key, so the literal reads as two columns, keys on the left and values on the right.

capitals = {
    "USA"    : "Washington",
    "France" : "Paris",
    "Japan"  : "Tokyo",
    "Spain"  : "Madrid",
}
python

More Examples ​

The key "a_substantially_longer_configuration_key" is far wider than "id" and "name", a spread the default max-shift cap would split. With max-shift set to false in config.toml the cap is removed, so the whole run aligns on one column and even "id" pads the full distance to the widest key.

config.toml sets max-shift = 0, which forbids any shift of a : off its key. The padded "alpha" : 1, "gamma" : 2, and "beta" : 3 rows each collapse to plain "key": value, and even the equal-width "alpha" and "gamma" rows get no shared-column padding, every : sitting flush against its own key.

Each Attributes: entry of Session carries a parenthesized type. Every ( moves to one shared column, host and cipher padded out to the width of retry_budget, and the : column then sits past the widest name (type) pair, so the colon measures the widths the type padding produces rather than the ones the source wrote.

The keys "name", "host", and "port" sit within max-shift of each other and align as one column. Padding them out to the width of "deeply_nested_fallback_descriptor" would exceed the cap, so that trailing key becomes its own group of one and keeps its : flush against the name.

The narrow id sits more than max-shift from the wide database fields above it. The run breaks at id in reading order, so primary_database_connection and backup_database_connection share one column and id and short open a second, much narrower one from that point on.

Field widths climb steadily in source order, from cpu up to persistent_storage_quota. The narrow rows cpu, disk, and memory share one : column, and that column breaks at network_throughput_gauge, because padding the narrow rows out that far would exceed max-shift, leaving the two wide fields to align in a column of their own.

config.toml sets align-docstring-entries = false, and the Args: section of run arrives hand-padded. The padding before each : is removed, leaving every colon flush against its name, whereas the name and retry_budget fields above still align on one : column, because the facet turns off the docstring context alone.

The entry plain carries no parenthesized type, so it has no ( to place on the type column and joins none. host (str) and retry_budget (int) open their parens on a shared column, and the : of plain still pads out to the colon column all three entries share.

The # comment on its own line between "https" and "ssh" closes one run and starts a fresh one. "http" and "https" align above it and "ssh" and "ftp" align below it, each pair on the column its own widest key sets rather than one shared column across the comment.

The overrides dict mixes keyed entries with a **defaults unpacking. The unpacking drops out of the alignment group and the column reaches across it, so "host", "user_agent", and "proxy" share the one column "user_agent" sets, because the unpacking carries no key and no : of its own.

Every entry of paths sits flush against its :, so aligning them inserts one space before each colon and another after it, and config.toml sets code-line-length to 42.

Both inserted spaces count toward the budget. At the shared column that puts the "label" row, with its long "docs/annual-summary.html" value, one column past the limit, so that row partitions out of the run, keeps its : flush against the key, and gains only the one space after it, whereas "verbose" and "dry" align beneath it.

Two entries of limits carry trailing # comments and two do not. All four rows align on the single : column set by "memory" rather than splitting at the commented lines, because each comment belongs to its own entry and does not break the run.

Each entry of config carries a trailing # comment after its value. The : separators align on the column set by "timeout" and each comment stays two spaces after its value, because a trailing comment belongs to its entry rather than breaking the run.

The docstring of build has both an Args: and a Returns: section. Each section resolves its own : column, so the wide source_channel sets the Args: column whereas ok and payload align on the much narrower column their own section needs, and one section's widest entry never shifts the other's table.

The two entries of config carry extra spaces after the :, as "a": 1 and "bbb": 2. Aligning the : column also collapses the gap after each colon to one space, so the rows are written as "a" : 1 and "bbb" : 2 with the hand padding removed.

The keys of mapping mix string literals, the integer 42, and the bare name HEADER. All four colons sit on the column set by HEADER, the widest key as printed, because the column math reads each key's display width rather than its expression kind.

The module-level annotations x and yyyy sit on adjacent lines. Both : move to a shared column at the width of yyyy, exactly as class fields and dict entries do, and each aligned row keeps one space on either side of the colon, so the over-padded x: int is written as x : int beside yyyy : str.

The values of config are themselves dicts, and each nested dict aligns its own : column at its own indent, "host" and "port_number" taking the same column inside both blocks, and the outer keys "development" and "production" align on their own column, unaffected by the key widths inside them.

The docstring of emit contains the text Args: twice, once inside a prose sentence and once as the section header. Only the section header opens a block the rule aligns, so the sentence stays as written whereas channel, severity, and payload under the header align on one : column.

The four annotated parameters of dispatch sit one per line. Every parameter : moves to the column one space past request_id, the widest name, so user, priority, and timeout gain padding before the colon. The : after -> Response closes the signature rather than annotating a parameter, so it stays as written.

"_overlapped" and "status" each continue their value onto a row starting at the column of the value's first string. Padding the : moves each value right, and the row beneath it moves the same number of columns, so the second string still sits beneath the first and the % operand beneath its string. "hang"'s call hangs its argument one indent step past the entry and closes at the entry's indent, so its rows stay where they were written.

The heads handler, timeout, and codec sit at the docstring body indent of Frame under no Title-case section heading. The run aligns its type column and its : column exactly the way a section's entries do, because the docstring entry reader treats a name (type): head at the body indent as a loose run of entries.

No Change

The options dict arrives hand-aligned at the width of "verify_certificates", and the spread from "ttl" up to that widest key sits exactly at the max-shift cap. The rule reads the whole run into one group, so the hand alignment round-trips unchanged and every : keeps its column.

  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. CASES already holds its keys in order, and the "restore-of-both-in-a-cluster" row crosses the code-line-length of 80 with or without its padding, so reflow-collections breaks its tuple open across three rows. The entry keeps its place in the run it shares with "restore-of-the-working-tree" and "shallow-abbreviated-depth", padded to their column with no blank line after it, because alphabetize-siblings reorders nothing here and so keeps the blank lines the source writes rather than setting one on either side of each entry spanning rows.

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

  4. The # prose: skip[reflow-calls] comment holds HANDLERS from reflow-calls, so reflow-collections leaves the dispatch call on one row rather than exploding it into the column "variable" sets. "cache" and "pr" align on the narrower column their own keys set, and "variable" stands unpadded beneath them.

  5. The dispatch call would cross the budget padded out to the column "variable" sets, yet with reflow-calls off reflow-collections leaves the call on one row, because exploding a call belongs to reflow-calls. "cache" and "pr" align on the narrower column their own keys set, and "variable" stands unpadded beneath them.

  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.

    1. reflow-collections
    2. align-colons

    The trailing comment on "cache" keeps MUTATIONS as written, since reflow-collections lays out no literal a comment sits inside. The "pr" set therefore never expands, so "pr" stays unpadded, because padding it to the column of either neighbor would carry its row past the budget. With "pr" out of the run, "cache" and "variable" each stand alone, and every : stays flush against its key.

  7. The dispatch call fits the budget beside "pr", but padding "pr" out to the column "variable" sets would carry the call three columns past it. reflow-collections measures the call at that padded column and explodes it one argument per row, its closing ) on a row of its own at the entry's indent, so "pr" joins the : column align-colons pads "cache" and "variable" out to.

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

  9. At a 32-column budget, the f-string prefer-fstring writes for "%(name)s: %(value)s" % {...} runs past the budget, so reflow-collections expands the dict. reflow-collections reads the ( of describe( as a bracket a layout rule can break, since the rewrite around it reaches past the dict it lays out, so align-colons pads "name" and describe( explodes in the same pass.

  10. Padded to the column "aaaaaaaaaaaaaaaa" sets, the "cc" row would cross the budget, so with every value on one row X splits into two columns, "aaaaaaaaaaaaaaaa" and "bbbbbbbb" in one and "cc" and "dd" in the other. reflow-collections explodes the configure call because that leaves the dict in one column rather than two, even though no row stands alone either way.

  11. Service declares three fields and three methods, each set out of order. The fields sort and pad both their : and their = into one column each, the methods sort separately so _cleanup leads restart and shutdown, and space-statements writes one blank line between every member and one directly under the class Service: header.

  12. render's docstring starts its summary on the """ line and carries an Args: section. frame-docstrings moves the summary off the opener line, align-colons pads the : after template, context_map, and escape_html into one column, and wrap-docstrings wraps the summary to the docstring budget across two lines.

  13. StationSchema is a pandera.DataFrameModel whose columns follow the order its fields are declared in, and its header carries # prose: keep. alphabetize-siblings leaves station, name, and latitude in that order, while align-colons and align-equals still pad the : and the = into one column each. expand-docstrings rewrites the docstring to the three-line form and space-statements writes one blank line below it, so every rule but alphabetize-siblings still runs over the kept body.

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

  15. paired lists three entries with uneven spacing before their :, and single lists one entry written "only_key" : 1. align-colons pads paired's : into one column one space past "second", and strip-stranded-padding removes the stray space in single, leaving "only_key": 1, because a lone entry forms no alignment group.

  16. ENDPOINTS is already aligned, the reviewers entry padded out to the column the attempts key sets, when the one-row comments entry is added beneath it. reflow-collections measures the opening row of reviewers at the canonical ": " rather than with the padding it already holds, so that row stays in the run, and the comments list, whose row would cross the budget at the column, expands so comments joins it. The dict comes out the same as it would from input carrying no padding at all.

  17. MUTATIONS sits on one row far past the budget, so reflow-collections writes one entry per row. Each value is measured at the column align-colons shifts it to once those rows align, so the "pr" set, which fits beside its own key but not at that column, expands in the same pass that writes the rows.

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

    1. align-colons

    The key ( "pp", "qq" ) carries a space inside each parenthesis, and with strip-stranded-padding off nothing removes them. align-colons measures the key at the 14 columns it is written in, so every : in MUTATIONS lands on one column rather than the key's : standing two columns past the others.

  19. The key ( "pp", "qq" ) is 14 columns as written and 12 once strip-stranded-padding drops the spaces inside its parentheses. reflow-collections measures each value at the column that settled width sets, the same column align-colons pads every key out to, so the "pr" set fits the budget there and stays on one row. Measured at the column the written width would set, two columns further right, the set would cross the budget and expand.

  20. Both Attributes: entries of Process run past the docstring budget before anything wraps them. align-colons pads both : to one column past command_message with no line cap of its own, and wrap-docstrings then reflows each description within docstring-line-length and hangs the wrapped lines from that padded column, so both entries reach their final layout in one pass rather than drifting across several.

  21. render's parameters template, context_map, and escape_html already list in the same order as the docstring's Args: entries. alphabetize-siblings leaves that order as written, align-colons pads the three entries' : into one column, and wrap-docstrings wraps each description with its continuation lines hanging under that column, because Args: entries mirror the signature rather than sorting alphabetically.

  22. The # prose: skip[align-colons] comment holds MUTATIONS from align-colons, so no : column forms for reflow-collections to measure a value against. The "pr" set stays on one row once the dict expands, because it fits the budget beside its own key, and every : stays flush against its key.

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

  24. The rows of "pr" and "qq" would both cross the budget at the column "variable" sets. Wherever alphabetize-siblings reorders a dict, it sets a blank line on either side of each entry spanning rows once two do, which would leave each expanded entry alone in a run of its own, and a dict already in order like this one holds to the same limit so the layout a reorder leaves stays put on the next run. reflow-collections therefore expands neither, and the two sets stay on their rows, aligned as a pair on the narrower column their own keys set, with "cache" above and "variable" below left unpadded.

  25. alphabetize-siblings rewrites MUTATIONS in sorted order and drops the blank line above "pr", so "pr", "variable", and "xy" share one run. reflow-collections measures the "pr" set at the column "variable" sets in that run and expands it, whereas the blank line as written would have kept "pr" in a narrower run beside "xy", where the set fits on one row.

  26. alphabetize-siblings sorts MUTATIONS, and the "cache" list already spans rows. Expanding the "pr" set as well would leave two entries spanning rows, and a sort that reorders a dict sets a blank line on either side of each once two do, which would put each expanded entry in a run of its own. reflow-collections therefore expands nothing, and since padding "pr" out to the column "cache" sets would carry its row past the budget, every row is left unpadded.

  27. "pr" is the last entry as written, so it carries no comma, and its row lands exactly on the budget at the column "variable" sets. alphabetize-siblings moves it ahead of "variable", where it gains a comma that carries the row one column past the budget. reflow-collections reads the order and the comma the sort leaves, so it expands the "pr" set in the same pass rather than leaving align-colons a row it cannot pad.

  28. The **SHARED unpacking stays transparent to the : run, so "cache", "pr", and "variable" share one column, whereas the blank line above "a" starts a run of its own. reflow-collections measures each value at the column align-colons shifts it to within its own run, so the "pr" set expands because its row crosses the budget at the column "variable" sets, while "a" and "bb" align on a narrower column beneath the blank line.

  29. retries, timeout_seconds, and window are annotated assignments with trailing comments, and three alignment rules pad the same rows, each checking its own padding against the 72-column budget. Every rule measures the row's settled form rather than the intermediate text the rule before it left, which is what lets the :, the =, and the # all reach their columns with the widest row, retries, still inside the budget.

  30. configure takes five typed parameters on one line, two with defaults. reflow-signatures explodes the signature to one parameter per line, align-colons pads each name to one : column, and align-equals pads the = of mango and delta into one column, while the parameters keep their source order even with alphabetize-siblings in the run, because reordering parameters would change every positional call.

  31. Padded to the column "bbbbbbbb" sets, the "a" row would cross the budget, so with every value on one row "a" stands unpadded above a column "bbbbbbbb", "cccccccccccccccccccc", and "ddddddddddddd" share. Expanding the list still leaves the dict in two columns, because "cccccccccccccccccccc" is 19 columns wider than "a", past the max-shift limit of 16. No row stands alone once the list expands, so reflow-collections expands it, "a" aligns with "bbbbbbbb", and the two wider keys align beneath them.

  32. "python_version" would cross the budget padded to the column "platform_python_implementation" sets, and reflow-collections could explode its ".".join( call to join that column. Expanding it would still leave one row unpadded, because "sys_platform" is 18 columns narrower than "platform_python_implementation", past the max-shift limit of 16, just as "platform_python_implementation" stands unpadded while every value stays on one row. The call therefore stays on one row, and "python_version" and "sys_platform" align as a pair beneath the unpadded row.

  33. configure's signature arrives exploded, its four typed parameters carrying defaults at widths from host to timeout_seconds. align-colons pads every name to one : column set by timeout_seconds, and align-equals pads every annotation to one = column set by float, so all four rows read as name, type, default.

  34. The "pr" set in MUTATIONS fits the budget beside its own key, but padding the : of "pr" out to the column "variable" sets would carry the set six columns past it. reflow-collections measures the set at that padded column and expands it, so "pr" joins the column align-colons pads every other key out to rather than standing unpadded and splitting the run in two.