collection-built-by-statements (SMP531)¶
Flags an empty dict, list or set that the statements right after it only fill with entries, which the literal could hold instead.
Why is this bad?¶
Filling a collection one statement at a time spreads one value over several lines, and a reader has to follow each add to learn what the collection holds. A literal shows the whole value where it is created, and the entries sit in the order they are added.
Example¶
Example: fix
Before
1def headers(token: str) -> dict[str, str]:
2 headers = {}
3 headers["Accept"] = "application/json"
4 headers["Cache"] = "no-store"
5 return headers
After
1def headers(token: str) -> dict[str, str]:
2 headers = {"Accept": "application/json", "Cache": "no-store"}
3 return headers
What it does¶
Active on¶
An assignment of {}, [] or set() to one name is reported when the statements right after it, one after another,
each add an entry to that name: name[key] = value for a dict, name.append(value) for a list and name.add(value)
for a set. The run stops at the first statement that is not such an add.
Keys and values must be constants or plain names, and must not read the collection itself.
A list filled with appends: the entries are written in the order they were added.
Example: fix
Before
1def names(first: str, second: str) -> list[str]: 2 out = [] 3 out.append(first) 4 out.append(second) 5 return out
After
1def names(first: str, second: str) -> list[str]: 2 out = [first, second] 3 return out
A set filled with constants: a set display takes the constants in the order given.
Example: fix
Before
1def unique() -> set[int]: 2 seen = set() 3 seen.add(1) 4 seen.add(2) 5 return seen
After
1def unique() -> set[int]: 2 seen = {1, 2} 3 return seen
Exceptions¶
An entry that calls something or reads an attribute or a subscript: the call runs in its statement’s place, and moving it into the literal would reorder it against the other entries and the collection’s creation.
Example: not reported
1def build() -> dict[str, int]: 2 table = {} 3 table["k"] = compute() 4 table["j"] = 1 5 return table 6 7 8def compute() -> int: 9 return 1
An entry that reads the collection: the literal does not exist yet while its entries are built.
Example: not reported
1def build(name: str) -> dict[str, object]: 2 table = {} 3 table["self"] = table 4 table[name] = 1 5 return table
A statement between the literal and its first add: the run is empty, so nothing is reported.
Example: not reported
1def build(log: list[str]) -> dict[str, int]: 2 table = {} 3 log.append("start") 4 table["b"] = 2 5 return table
Two keys, or two set elements, that are the same: the second add replaces the first, which a literal would show as a repeated key that Ruff reports. Values that hash alike count as the same, such as
1andTrue, and so do two mentions of one name.Example: not reported
1def build() -> dict[str, int]: 2 table = {} 3 table["k"] = 1 4 table["k"] = 2 5 return table
Example: not reported
1def build() -> set[object]: 2 seen = set() 3 seen.add(1) 4 seen.add(True) 5 return seen
An annotated assignment: the literal would need the annotation, which the rule does not rewrite.
Example: not reported
1def build() -> dict[str, int]: 2 table: dict[str, int] = {} 3 table["k"] = 1 4 return table
A comment between two adds: the comment would lose the statement it stands beside.
Example: not reported
1def build() -> dict[str, int]: 2 table = {} 3 table["a"] = 1 4 # the second key 5 table["b"] = 2 6 return table
A list that Ruff’s FURB113 reports: with Ruff’s preview mode on and FURB113 selected, Ruff reports two or more appends to a list, even with appends to other lists between them, and this rule leaves that list to Ruff.
Example: not reported
pyproject.toml1[tool.ruff.lint] 2preview = true 3select = ["FURB113"]
1def build() -> list[int]: 2 result = [] 3 result.append(1) 4 result.append(2) 5 return result
Fix¶
The fix replaces the run with one literal of the same kind and keeps the assignment’s name. A tuple used as a key or an entry is parenthesized, since a comma would end it.
The fix is safe when every key and value is a constant. Building such a literal cannot raise, so it stops at the same point as the statements. A name may be undefined or hold an unhashable value, and then the statements would leave the collection partly filled while the literal leaves it unbound. Those runs are fixed as unsafe.
Example: fix
Before
1def keyed(name: str, value: int) -> dict[str, int]:
2 result = {}
3 result[name] = value
4 return result
After
1def keyed(name: str, value: int) -> dict[str, int]:
2 result = {name: value}
3 return result
Ruff overlap¶
Ruff’s unnecessary-collection-call (C408) rewrites
dict() and list() calls into literals, and does not look at the statements that follow them.
Other tools¶
Ruff: variation of preview rule FURB113, which merges consecutive
appendcalls intoextendand which this rule leaves to it; this rule covers dicts, sets and single appends right after an empty literal.flake8-simplify reports a dictionary filled by the statements after it as SIM904. It covers dictionaries only, and it does not fix them.
pylint reports the same dictionaries as dict-init-mutate. It covers dictionaries only, with no fix.