Square brackets build a list: every value, in memory, before the next step starts. Round brackets build a generator, which hands over one value at a time and keeps nothing. If the values are read once, by sum, max, any or a for loop, the list is memory you paid for and never used.
Before
readings = range(1_000_000) total = sum([r * 1.2 for r in readings]) # a million floats, held at once print(round(total))
599999400000
After
Drop the square brackets. Inside a function call, the parentheses are already there.
readings = range(1_000_000) total = sum(r * 1.2 for r in readings) # one float at a time print(round(total))
599999400000
Same answer. The difference is what sat in memory on the way.
See the difference
import sys readings = range(1_000_000) as_list = [r * 1.2 for r in readings] as_gen = (r * 1.2 for r in readings) print(f"list: {sys.getsizeof(as_list):,} bytes") print(f"generator: {sys.getsizeof(as_gen):,} bytes")
list: 8,448,728 bytes generator: 200 bytes
On my machine the list is about 8 MB, and that's only the list's own slots: each float inside it is a separate object on top. The generator stays the same small size whatever the input, because it only remembers where it is. In the browser the exact numbers are smaller (it runs 32-bit Python), but the gap is the same kind.
Why it works
A generator runs your expression lazily: sum asks for the next value, the generator computes one, sum adds it, and that value can be thrown away. Nothing ever needs the million values at the same time, so nothing stores them.
When not to use it
A generator can be read once. The second pass gets nothing:
readings = (r for r in [3, 7, 5]) print(max(readings)) print(max(readings, default="empty"))
7 empty
If you need the values twice, need len(), indexing or sorting, keep the list. And for a small collection the saving is too small to matter: pick whichever reads better. The generators lesson goes further, into writing your own with yield.