You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As per #24572 (comment), propose removing the compact multi-line layout (Layout 2) where the formatter packs all arguments on a single indented line. Expressions would go directly from single-line (Layout 1) to one-per-line with trailing comma (Layout 3).
If adopted as the default, this also resolves the long-standing COM812 formatter incompatibility (#9216).
The formatter produces three layouts for comma-separated content:
# Layout 1: single line (fits within line width)result=func(a, b, c)
# Layout 2: compact multi-line (all args on one indented line, no trailing comma)result=func(
a, b, c, d, e, f, g, h
)
# Layout 3: one-per-line (with trailing comma)result=func(
a,
b,
c,
d,
e,
f,
g,
h,
)
This proposal removes Layout 2. Expressions that don't fit on a single line would always use Layout 3.
Motivation
1. Readability
Layout 2 can be misleading at a glance — it's ambiguous whether the expression takes one argument or many:
# Layout 2 — at a glance, this looks like a single argumentresult=func(
this_function_actually(takes, two), parameters
)
# Layout 3 — structure is immediately clearresult=func(
this_function_actually(takes, two),
parameters,
)
COM812 flags this (newline before ) without trailing comma), adds a comma, and the formatter re-expands to Layout 3 — requiring multiple ruff format → ruff check --fix → ruff format passes to converge. Users must disable COM812 entirely to avoid this.
Without Layout 2, the formatter would always produce Layout 3 (with trailing comma) for expressions that don't fit on a single line. COM812 and the formatter would converge in a single ruff format pass.
3. Diff-friendliness
Layout 2 lacks a trailing comma, which means adding or removing an argument always touches the existing last line:
# Layout 2 — no trailing comma, 2 lines changed to add an argument
result = some_function(
- first_argument, second_argument, third_argument, fourth_argument+ first_argument, second_argument, third_argument, fourth_argument, fifth_argument
)
Layout 3 with trailing comma keeps diffs minimal — only the new line is added:
# Layout 3 with trailing comma — 1 line added
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument,
+ fifth_argument,
)
This is the core purpose of COM812 — reducing diff noise when parameters change. Layout 2 directly undermines this goal.
ruff format re-expands to Layout 3 (trailing comma forces one-per-line)
This means pre-commit hooks always report changes on the first run, even when the final state is identical to the original. Users resort to multi-pass workarounds:
# Workaround: run format → check → format in a loop
- repo: localhooks:
- id: ruffentry: bash -c 'ruff format "$@";for i in 1 2 3; doruff check --fix "$@";ruff format "$@";done;ruff check "$@" && ruff format --check "$@"' -- types: [python]
The multi-pass workaround also introduces potential bugs: each intermediate write to disk can trigger file watchers, invalidate caches, and cause stale reads in AI coding agents and IDE language servers that monitor file changes. When running pre-commit run --all-files, the loop touches every Python file in the repo multiple times even when the final state is identical to the original.
Without Layout 2, ruff format alone produces COM-compliant output. A single ruff format pass is sufficient — no ruff check --fix needed, no loops, no workarounds:
AI coding agents frequently produce over-wrapped expressions. With Layout 2, the formatter can't normalize these to a consistent style in one pass — the trailing comma presence or absence changes the output:
# AI-generated (trailing comma prevents collapsing in "respect" mode)result=func(
arg1,
arg2,
)
# Formatter can't collapse this to `func(arg1, arg2)` because the# trailing comma signals "keep expanded"
Without Layout 2, the formatter's behavior becomes more deterministic — expressions either fit on one line (Layout 1) or expand to one-per-line (Layout 3).
6. Consistency with other constructs
Lists, dicts, sets, and imports already go directly from Layout 1 to Layout 3 — they don't have a Layout 2 intermediate form. Only function calls, function definitions, pattern arguments, and subscript tuples use the compact layout (via an inner group() wrapper). Removing it would make all comma-separated constructs behave consistently.
Affected constructs
Layout 2 currently applies to:
Function call arguments
Function definition parameters
Match pattern arguments
Subscript tuples (e.g., matrix[a, b, c])
Lists, dicts, sets, imports, type parameters, with statements, and other comma-separated constructs already go directly from Layout 1 to Layout 3.
Possible solutions
Four magic-trailing-comma modes and their behavior:
All four modes on over-wrapped short expressions with trailing comma:
# Input — AI-generated, fits on one lineresult=func(
arg1,
arg2,
)
# respect — stays expanded (trailing comma is "magic")result=func(
arg1,
arg2,
)
# ignore — collapses (trailing comma ignored)result=func(arg1, arg2)
# force — stays expanded (trailing comma is still "magic")result=func(
arg1,
arg2,
)
# normalize — collapses (trailing comma ignored + comma removed)result=func(arg1, arg2)
"force" and "normalize" both resolve the COM812 conflict but differ on trailing comma semantics:
"force" respects existing trailing commas — the user's intent to keep expressions expanded is preserved
"normalize" ignores existing trailing commas — the formatter fully owns layout decisions based on line width
Either could be gated behind preview, edition = "next", or made the default. They are also compatible with removing Layout 2 globally — if Layout 2 is removed for everyone, "force" becomes equivalent to "respect" and "normalize" becomes equivalent to "ignore" for the layout, with trailing comma addition as the only remaining difference.
Scope: Should this apply to all four affected constructs (calls, defs, patterns, subscripts) equally, or should some keep Layout 2?
Relationship to COM812: If Layout 2 is removed globally, should the COM812 incompatibility warning be removed? Should COM812 itself be reconsidered (since the formatter would handle trailing commas)?
Summary
As per #24572 (comment), propose removing the compact multi-line layout (Layout 2) where the formatter packs all arguments on a single indented line. Expressions would go directly from single-line (Layout 1) to one-per-line with trailing comma (Layout 3).
If adopted as the default, this also resolves the long-standing
COM812formatter incompatibility (#9216).Current formatter behavior
The formatter produces three layouts for comma-separated content:
This proposal removes Layout 2. Expressions that don't fit on a single line would always use Layout 3.
Motivation
1. Readability
Layout 2 can be misleading at a glance — it's ambiguous whether the expression takes one argument or many:
This was raised independently by multiple users:
magic-trailing-commaoption withnormalizemode to make formatter COM-safe #24572 (comment)2.
COM812compatibilityThe formatter and
COM812currently conflict because Layout 2 has no trailing comma:COM812flags this (newline before)without trailing comma), adds a comma, and the formatter re-expands to Layout 3 — requiring multipleruff format→ruff check --fix→ruff formatpasses to converge. Users must disableCOM812entirely to avoid this.Without Layout 2, the formatter would always produce Layout 3 (with trailing comma) for expressions that don't fit on a single line.
COM812and the formatter would converge in a singleruff formatpass.3. Diff-friendliness
Layout 2 lacks a trailing comma, which means adding or removing an argument always touches the existing last line:
Layout 3 with trailing comma keeps diffs minimal — only the new line is added:
# Layout 3 with trailing comma — 1 line added result = some_function( first_argument, second_argument, third_argument, fourth_argument, + fifth_argument, )This is the core purpose of
COM812— reducing diff noise when parameters change. Layout 2 directly undermines this goal.4.
pre-commitand CI usabilitypre-commitexpects each hook to be idempotent in a single pass. The currentruff format+COM812combination fundamentally requires multiple passes:ruff formatproduces Layout 2 (no trailing comma)ruff check --fixadds trailing comma (COM812)ruff formatre-expands to Layout 3 (trailing comma forces one-per-line)This means
pre-commithooks always report changes on the first run, even when the final state is identical to the original. Users resort to multi-pass workarounds:The multi-pass workaround also introduces potential bugs: each intermediate write to disk can trigger file watchers, invalidate caches, and cause stale reads in AI coding agents and IDE language servers that monitor file changes. When running
pre-commit run --all-files, the loop touches every Python file in the repo multiple times even when the final state is identical to the original.Without Layout 2,
ruff formatalone produces COM-compliant output. A singleruff formatpass is sufficient — noruff check --fixneeded, no loops, no workarounds:5. AI-generated code
AI coding agents frequently produce over-wrapped expressions. With Layout 2, the formatter can't normalize these to a consistent style in one pass — the trailing comma presence or absence changes the output:
Without Layout 2, the formatter's behavior becomes more deterministic — expressions either fit on one line (Layout 1) or expand to one-per-line (Layout 3).
6. Consistency with other constructs
Lists, dicts, sets, and imports already go directly from Layout 1 to Layout 3 — they don't have a Layout 2 intermediate form. Only function calls, function definitions, pattern arguments, and subscript tuples use the compact layout (via an inner
group()wrapper). Removing it would make all comma-separated constructs behave consistently.Affected constructs
Layout 2 currently applies to:
matrix[a, b, c])Lists, dicts, sets, imports, type parameters,
withstatements, and other comma-separated constructs already go directly from Layout 1 to Layout 3.Possible solutions
Four
magic-trailing-commamodes and their behavior:"respect""ignore""force""normalize"COM812safeskip-magic-trailing-comma = trueAll four modes on the compact multi-line case:
All four modes on over-wrapped short expressions with trailing comma:
"force"and"normalize"both resolve theCOM812conflict but differ on trailing comma semantics:"force"respects existing trailing commas — the user's intent to keep expressions expanded is preserved"normalize"ignores existing trailing commas — the formatter fully owns layout decisions based on line widthEither could be gated behind
preview,edition = "next", or made the default. They are also compatible with removing Layout 2 globally — if Layout 2 is removed for everyone,"force"becomes equivalent to"respect"and"normalize"becomes equivalent to"ignore"for the layout, with trailing comma addition as the only remaining difference.Open questions
Per #24572 (comment):
previewflag (opt-in, may stabilize later)edition = "next"(opt-in, stable within the edition, requires designing the edition mechanism)[format]or[lint.flake8-commas]?format.magic-trailing-comma = "force"([formatter] Addmagic-trailing-commaoption withforcemode to make formatter COM-safe #24609) — formatter owns trailing comma behavior, single-pass convergencelint.flake8-commasoption — linter-side, but requiresruff check --fixafterruff format(two passes)edition = "next"COM812: If Layout 2 is removed globally, should theCOM812incompatibility warning be removed? ShouldCOM812itself be reconsidered (since the formatter would handle trailing commas)?Prior work
COM812formatter incompatibility"force"suggestionpre-commitconvergence motivationmagic-trailing-commaoption withnormalizemode to make formatter COM-safe #24572 —"normalize"mode (postponed)magic-trailing-commaoption withnormalizemode to make formatter COM-safe #24572 (comment) — layout changes should not be coupled with trailing comma behaviormagic-trailing-commaoption withnormalizemode to make formatter COM-safe #24572 (comment) — consider removing compact layout globally or viaedition = "next"magic-trailing-commaoption withnormalizemode to make formatter COM-safe #24572 (comment) — open a new issue to discuss the style changemagic-trailing-commaoption withforcemode to make formatter COM-safe #24609 —"force"mode (preview feature)cc @MichaReiser @dylwil3 @charliermarsh