Skip to content

simplify-comprehensions

simplify-comprehensions removes the layer that does no work, so a constructor wrapped around a literal, a comprehension, or a generator is replaced by the form underneath it, and set([row.width for row in rows]) reads {row.width for row in rows}. set([x for x in xs]) builds a list, throws it away, and builds a set from it, and the reader unwinds two constructions to reach one value, so that call reads set(xs). dict() becomes {}, tuple([1]) becomes (1,), and dict(alpha=1) becomes {"alpha": 1}.

The brace form is written only where it is unambiguous. An empty set() stays a call because {} names an empty dict rather than an empty set, so set([]) becomes set() and never {}. A dict(...) call becomes the brace form only where its argument carries key-value pairs a literal or a dict comprehension can express, so dict(**defaults) and dict(defaults, extra=1) stay as written.

A comprehension whose element repeats its target unchanged spells a copy, so [row for row in rows] becomes list(rows) and {key: value for key, value in rows} becomes dict(rows). A guard or a second generator makes the comprehension do work no constructor call does, and both forms stay as written. Where a wrapper and a copy meet, the rewrite settles in one step, so list(row for row in rows) becomes list(rows) directly rather than passing through an intermediate comprehension. It stops short of list(list(rows)), since a doubled constructor reads no better than the comprehension it would replace. An f-string or t-string replacement field is not visited, so a call written inside one keeps whatever form its author gave it.

set, dict, list, and tuple are builtins a module is free to rebind, and a rebound name no longer names the constructor. A module that binds any of the four to something of its own therefore keeps every call to that name exactly as written, while the other three still collapse.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.

The Canonical Case

set([1, 4, 9]) passes a list literal to set(), and set(("alpha", "beta")) passes a tuple literal, each already spelling the set's members. Both calls become the brace form, so squares reads {1, 4, 9} and labels reads {"alpha", "beta"}.

squares = {1, 4, 9}
labels  = {"alpha", "beta"}
python

More Examples

marks = set([...]) spans three lines, and # the first trails the 1 member on its own line. Only the delimiters on either side of the members change, set([ to { and ]) to }, so the comment keeps trailing 1 on the line where it was written.

set([row.width for row in rows]) wraps a list comprehension in set(), and list([row.label for row in rows]) wraps one in list(). The set() call and the list brackets become one pair of braces, and the list() call is removed, so widths reads {row.width for row in rows} and labels reads [row.label for row in rows], because a list comprehension already builds a list.

[row for row in rows], {row for row in rows}, and {key: value for key, value in rows} each repeat their target unchanged as the element, so each spells a plain copy of rows. Each becomes the constructor call it was building, list(rows), set(rows), and dict(rows).

dict(), list(), tuple(), and set() are each called with no argument. dict(), list(), and tuple() become {}, [], and (), whereas unique = set() stays a call, because {} already names an empty dict and no literal spells an empty set.

list(row for row in rows), set([row for row in rows]), and dict((key, value) for key, value in rows) each wrap a comprehension that only copies its input. Each becomes list(rows), set(rows), or dict(rows) in one rewrite, rather than first unwrapping to a bare [row for row in rows] that a later pass would still have to rewrite.

[row for row in [1, 2]] copies the list literal [1, 2], and [row for row in [cell for cell in cells]] copies a comprehension that is itself a copy of cells. values becomes [1, 2] with no list(...) call left, and nested becomes list(cells) in the same pass, rather than first producing list(list(cells)), which nothing downstream would clean up.

dict({"size": 10}) wraps a dict literal that is already the mapping the call would build. The call is removed, so config becomes {"size": 10}.

list(row.width for row in rows) and set(row.label for row in rows) each pass a bare generator expression as the constructor's only argument. Each call is removed and the constructor's own brackets carry the comprehension, so list(...) becomes [row.width for row in rows] and set(...) becomes {row.label for row in rows}.

list([1, 2, 3]) passes a list literal to list(), and list(("a", "b")) passes a tuple literal. Both calls are removed, so items becomes [1, 2, 3] and names becomes ["a", "b"], with every member unchanged.

tuple([1, 2]) passes a two-member list, tuple([only]) passes a one-member list, and tuple((only,)) passes a one-member tuple that already carries its trailing comma. Each becomes a tuple literal, and both one-member forms end in the comma a one-element tuple needs, so single becomes (only,) with the comma added and kept becomes (only,) with its source comma kept rather than doubled.

set((only,)) and list((only,)) each pass a one-member tuple to a constructor. Both become the direct literals {only} and [only], dropping the comma along with the parentheses, because the comma in (only,) is grammar marking the expression as a tuple rather than content.

dict((row.key, row.width) for row in rows) and dict([(row.key, row.label) for row in rows]) each yield one two-member tuple per row. Both become dict comprehensions, {row.key: row.width for row in rows} and {row.key: row.label for row in rows}, because each pair splits at its comma into a key and a value, so the mapping is built directly rather than through a dict() call.

The kind value inside the dict() call that builds labels is a two-line string wrapped in grouping parentheses, and those parentheses sit outside the value's own parsed range. The rewritten dict literal carries the whole parenthesized string, because the rule reads the value's full extent rather than starting inside the opening parenthesis, so nothing is truncated.

The module binds list = [9], so list no longer names the builtin constructor, whereas set keeps its usual meaning. values = list([1, 2]) stays as written and unique = set([1, 2]) becomes unique = {1, 2}, because a call to a rebound name is never rewritten.

dict([(1, "one"), (2, "two")]) passes a list of two-member tuples, and dict(((3, "three"), (4, "four"))) passes a tuple of them. Each pair drops its parentheses and its comma becomes a :, so codes becomes {1: "one", 2: "two"} and ranges becomes {3: "three", 4: "four"}, with no dict() call left around them.

set({1, 2}) wraps a set literal that is already the set the call would build. The call is removed, so unique becomes {1, 2}.

set([row async for row in rows]) wraps an async list comprehension in a set() call. The call and the list brackets are replaced by one pair of braces, so the result is {row async for row in rows} with the comprehension body as written, because an async comprehension is never treated as a plain copy of its input, so it is not rewritten to set(rows).

set([]) and set(()) each pass an empty sequence literal to set(). Both become the bare set() call rather than a brace form, because {} already names an empty dict.

dict(alpha=1, beta=2) passes two keyword arguments, alpha and beta, each naming a key a brace literal can carry directly. The call becomes {"alpha": 1, "beta": 2}, with each keyword's name quoted as its dict key.

No Change

[row.width for row in rows] and {row.label for row in rows} each produce an element, row.width or row.label, that differs from the target row. Both comprehensions stay exactly as written, because each transforms its input rather than copying it, and list(rows) or set(rows) would lose that transformation.

No Change

[row for row in list(rows)] and [row for row in set(rows)] each copy their target the way a rewritable comprehension does, but each iterable is itself a constructor call. Both comprehensions stay as written, because the rewrite would spell list(list(rows)) and list(set(rows)), and a doubled constructor reads no better than the comprehension it would replace.

No Change

active's comprehension carries an if row.live guard, and nested's carries two for clauses over two levels. Both comprehensions stay as written, because the rule rewrites only a plain copy of a single iterable, and a guard or a second generator does work no constructor call does.

No Change

dict(**defaults) unpacks a mapping without naming any key, and dict(defaults, extra=1) passes a positional argument plus a keyword. Both dict() calls stay as written, because a {...} literal would need keys the call never names.

For per-statement opt-outs, the Suppression chapter covers the # prose: skip[simplify-comprehensions] directive, which covers every line a wrapped call spans.