Skip to content

trailing-comment-extraction (PCF004)

Added in 1.0.0 Usually fix By default

Part of the PCF rule category.

View source · View documentation source · Search issues

Fix is usually available.

What it does

Checks overlong ordinary trailing comments after canonical spacing. When the complete canonical code-plus-comment line exceeds line-length, PCF004 can remove whitespace immediately before the comment, move the comment directly above the physical code line at the code line's indentation, and wrap it as a standalone block.

When comment-trailing-extraction-syntax-aware is enabled, overlong comments in decorators, compound statement headers, arguments, and parenthesized or continuation contexts remain inline to preserve their physical association. Syntax-aware extraction applies to function, class, loop, conditional, with, try/except/else/finally, match, and case headers, plus decorators, arguments, and parenthesized or continuation contexts. It does not protect ordinary trailing comments merely because they appear inside a compound statement body.

When comment-trailing-extraction-content-aware is enabled, overlong comments remain inline if their content would be unsafe to reinterpret as standalone comment text. The unsafe-content check respects the existing standalone comment settings: list-like text is unsafe when comment-format-list-items is enabled, table-like text is unsafe when comment-preserve-tables is enabled, and the same pattern applies to enabled headings, doctests, code fences, block quotes, directives, and code-detection settings. Disabling one detector only removes that detector from the safety check; if the same text also matches another enabled detector, it still remains inline. Within this content-aware check, leading symbolic operator-like tokens such as -, *, >, |, +, comparison operators, and arrows are always unsafe, even when a matching structure setting is disabled. Ordinary prose that starts with words such as and, or, or not can still extract.

When comment-task-marker-mode is no-wrap or hanging, extracted task-marker comments use the same task-marker treatment as standalone task markers. Content-aware code detection checks the task-marker payload rather than the full TODO:-style prefix, so annotation-like marker text such as TODO: fix_parser is not mistaken for Python code.

When a moved block would directly follow an existing same-indent standalone comment, a blank line keeps the independently authored comments separate. The rule generates the complete canonical block itself and therefore works when PCF001 is disabled.

That directly generated block uses the same unconditional atomic inline-markup recognition as PDF101 and PCF001. Non-empty same-line backtick spans with matching delimiter runs, supported Markdown links and images, CommonMark-style autolinks, and boundary-valid reStructuredText interpreted text, roles, references, embedded targets, literals, and substitutions are never split internally. Markdown link support includes escaped or nested labels, empty inline components, all three standard title delimiters, and destination parentheses nested up to three levels. Emphasis, raw HTML or XML, shortcut references, definitions, and multiline constructs are not recognized as atomic inline markup. A line-leading run of at least three backticks or tildes is fence-like ordinary text rather than ambiguous inline markup, including when an info string follows the opener; the content-aware structure checks can still keep it inline when configured to preserve fences. url-aware-wrapping only selects balanced rather than greedy breaks around destination-bearing constructs.

After ordinary-comment, overlong-line, and syntax-position eligibility checks, strong evidence of incomplete or over-bounded inline markup produces an unfixable finding instead of extraction. Generic unmatched delimiters without stronger supported-markup evidence remain ordinary comment text. This ambiguity guard runs before the configurable content-awareness filter, is always active, and is not disabled by comment-trailing-extraction-content-aware = false. Trailing-comment whitespace is normalized rather than interpreted as a hard break because extraction does not create a semantic line relationship with the code moved below it.

Widths use tab-expanded columns with indent-width as the tab size. If indentation leaves no positive wrapping width, a non-empty overlong comment is still moved above the code but its text remains on one unwrapped line. Long words and recognized markup are not split. Source outside the replacement retains mixed line endings and the file's final-newline state. When url-aware-wrapping is enabled, destination-bearing tokens can trigger balanced line selection; disabling it restores greedy selection without making those tokens splittable.

Why is this useful?

Extracting ordinary long comments prevents explanatory text from obscuring code, while rule selection and safety settings let projects keep harmless spacing normalization without enabling comment movement.

Ruff compatibility

Ruff can report overlong lines and spacing issues, but it does not extract and wrap trailing comments in this way. PCF004 leaves Ruff, type-checker, formatter, and security directives unchanged.

Examples

An overlong trailing comment moves above the code and wraps as a standalone block:

line-length = 42
value = compute()  # This trailing comment has enough words that it must move above the code line.
# This trailing comment has enough words
# that it must move above the code line.
value = compute()

An overlong task-marker trailing comment extracts without wrapping its task-marker payload by default:

line-length = 30
value = compute()  # TODO: alpha beta gamma delta epsilon zeta eta theta
# TODO: alpha beta gamma delta epsilon zeta eta theta
value = compute()

Set comment-task-marker-mode to hanging to extract and reflow task-marker payloads with marker-width hanging indentation:

line-length = 30
comment-task-marker-mode = "hanging"
value = compute()  # TODO: alpha beta gamma delta epsilon zeta eta theta
# TODO: alpha beta gamma delta
#       epsilon zeta eta theta
value = compute()

Set comment-task-marker-mode to none to extract the same text as an ordinary standalone comment instead of using task-marker-specific continuation indentation:

line-length = 30
comment-task-marker-mode = "none"
value = compute()  # TODO: alpha beta gamma delta epsilon zeta eta theta
# TODO: alpha beta gamma delta
# epsilon zeta eta theta
value = compute()

The complete canonical code-plus-comment line determines whether a comment fits. Even a short comment moves when the code makes the combined line too long:

line-length = 40
very_long_variable_name = compute_expensive_value()  # why
# why
very_long_variable_name = compute_expensive_value()

Syntax-aware extraction keeps overlong comments inline where moving them would weaken their association with nearby syntax:

line-length = 32
if enabled:  # explanation long enough to move above the header
    pass
@decorator  # explanation long enough to move above the decorator
def function(
    value,  # explanation long enough to move above the argument
):
    pass

When extraction is suppressed, PCF004 leaves the original inline spacing unchanged. PCF002 owns trailing code-to-# delimiter spacing, and PCF003 owns directive normalization for recognized directives that remain inline:

line-length = 32
if enabled:# explanation long enough to move above the header
    pass

Content-aware extraction keeps standalone-like and operator-like text inline by default:

line-length = 30
value = compute()  # - alpha beta gamma delta epsilon
other = compute()  # >>> alpha beta gamma delta epsilon

Disabling the relevant standalone structure setting allows that structure-like content to extract, unless it is still operator-like or matches another enabled detector:

line-length = 30
comment-preserve-doctests = false
comment-format-list-items = false
comment-format-block-quotes = false
value = compute()  # - alpha beta gamma delta epsilon
other = compute()  # >>> alpha beta gamma delta epsilon
value = compute()  # - alpha beta gamma delta epsilon
# >>> alpha beta gamma delta
# epsilon
other = compute()

Standalone settings affect the content-safety check directly. Here table preservation is disabled, so the table-like trailing comment can move; list formatting is also disabled, but the - marker still stays inline because it is operator-like:

line-length = 24
comment-preserve-tables = false
comment-format-list-items = false
table = compute()  # :--- | ---:
bullet = compute()  # - alpha beta gamma delta epsilon
# :--- | ---:
table = compute()
bullet = compute()  # - alpha beta gamma delta epsilon

Set both extraction safety settings to false to restore aggressive extraction:

line-length = 32
comment-trailing-extraction-syntax-aware = false
comment-trailing-extraction-content-aware = false
if enabled:  # explanation long enough to move above the header
    pass
value = compute()  # - alpha beta gamma delta epsilon
# explanation long enough to
# move above the header
if enabled:
    pass
# - alpha beta gamma delta
# epsilon
value = compute()

PCF004 generates markup-aware wrapping itself, so selecting it without PCF001 still keeps a complete inline link indivisible. With URL-aware wrapping enabled, it can balance the surrounding words to avoid isolating the destination-bearing token:

line-length = 40
url-aware-wrapping = true
value = compute()  # alpha beta [label](https://example.com/path) alpha after
# alpha
# beta [label](https://example.com/path)
# alpha after
value = compute()

Ambiguous inline markup is reported without moving the comment, even when content-aware extraction is disabled:

line-length = 32
comment-trailing-extraction-content-aware = false
value = compute()  # Read [the label](missing destination words that need moving.
PCF004: Line 1: Trailing comment should be extracted

Options