If you write Python and still use plain for loops to build lists or process data, you are wasting resources. We see it in the projects that come to us: slower code, unnecessary memory usage, server loads that grow for no reason. Here at Meteora Web, we think in terms of efficiency and cost. A well-written list comprehension is not just more elegant – it consumes fewer CPU cycles and makes code maintainable. A generator, instead of loading everything into RAM, can reduce memory usage by 90% on real datasets. This guide gets straight to the point: understand how they work, when to use them, and when not to, with copy‑and‑paste examples.
List Comprehension: syntax, power, and limits
List comprehension is the Pythonic way to create lists in one line. The basic structure is [expression for element in iterable if condition]. The order mirrors nested loops: first the for, then optional if.
Practical example: filtering orders
Imagine you have a list of order totals and want only those above €100. Classic loop:
orders = [45, 120, 80, 200, 55]
filtered = []
for price in orders:
if price > 100:
filtered.append(price)
print(filtered) # [120, 200]With list comprehension:
filtered = [price for price in orders if price > 100]
print(filtered) # [120, 200]Fewer lines, same readability. But don't overdo it. If the condition is complex or you need three nested for loops, stick to a normal loop. Readability matters more than brevity.
Transforming every element
You can transform data on the fly. Example: applying 22% VAT to net prices:
net_prices = [10, 25, 50]
with_vat = [round(p * 1.22, 2) for p in net_prices]
print(with_vat) # [12.2, 30.5, 61.0]Nested list comprehensions
They work like nested loops. To flatten a matrix:
matrix = [[1,2,3], [4,5], [6]]
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6]Note the order: outer loop first, then inner. Read left to right as if they were nested.
Generators: when memory becomes a problem
A list comprehension builds the entire list in memory. For a million elements, that's several MB of RAM. A generator, however, produces one item at a time – lazy evaluation. The syntax is identical but with parentheses instead of brackets.
numbers = range(1_000_000)
# Generator: consumes no memory
gen = (x * 2 for x in numbers)
print(next(gen)) # 0
print(next(gen)) # 2You cannot index a generator or know its length without exhausting it. But you can iterate over it with a for loop or convert it to a list (losing the advantage).
Yield: custom generators
Using yield you create generator functions. Example: reading a log file line by line without loading it all into RAM:
def read_log(path):
with open(path, 'r') as f:
for line in f:
yield line.strip()
for entry in read_log('server.log'):
if 'ERROR' in entry:
print(entry)Each call to next() resumes execution from where it paused. It's like a frozen loop. Useful for data pipelines or infinite streams.
Iterators: the engine under the hood
List comprehensions, generators, for loops – everything relies on iterators. An iterable is any object that returns an iterator via iter(). The iterator implements __next__() and raises StopIteration when exhausted.
numbers = [1, 2, 3]
it = iter(numbers)
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# print(next(it)) # StopIterationYou can create custom iterators by defining __iter__ and __next__. Example: a counter that starts at a value and stops at a limit:
class Counter:
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
val = self.current
self.current += 1
return val
for num in Counter(1, 5):
print(num) # 1 2 3 4 5Generators are a simpler way to write iterators, but understanding the mechanics helps you debug and optimize them.
Performance comparison: list comprehension vs generators
We always measure. Use timeit to test:
import timeit
# List comprehension: 1 million squares
list_time = timeit.timeit('[x2 for x in range(1_000_000)]', number=1)
print(f"List comprehension: {list_time:.4f} sec") # ~0.06 sec
# Generator: same calculation, lazy
gen_time = timeit.timeit('(x2 for x in range(1_000_000))', number=1)
print(f"Generator: {gen_time:.4f} sec") # ~0.000001 sec (no execution!)
# If we then iterate the generator
gen = (x**2 for x in range(1_000_000))
def consume():
for _ in gen:
pass
gen_iter_time = timeit.timeit('consume()', globals={'consume': consume}, number=1)
print(f"Generator iterated: {gen_iter_time:.4f} sec") # ~0.08 secThe takeaway: creating a generator is instant, but total iteration is similar to a list comprehension. The real win comes when you don't need to consume everything. For example, if you're searching for the first element that satisfies a condition, a generator stops early.
Common mistakes and how to avoid them
- Using list comprehension when a generator is needed: if you're passing data to a function that accepts an iterable (e.g.,
sum(),any(),max()), there's no need for an intermediate list. Use a generator:sum(x**2 for x in range(10)). - Reusing a generator: once exhausted, it's empty. If you need to iterate twice, convert it to a list (or recreate it).
- Overly complex list comprehensions: if you have more than two
forloops or conditions, write a normal loop with comments. Code must be understandable by others (or by you in six months). - Ignoring variable scope: in list comprehensions, the loop variable leaks into the namespace in Python 2; in Python 3 it's local. Either way, avoid modifying external variables inside a comprehension.
A quick decision checklist
- Do I need to produce a list? → List comprehension.
- Do I need to process a stream one item at a time? → Generator.
- Do I only want to iterate once and stop early? → Generator.
- Is the logic complex? → Normal loop or generator function with yield.
- Is readability the top priority? → Write the clearest code, even if longer.
In a nutshell – what to do now
Right now. Review your Python code: find for loops that populate lists. Replace them with list comprehensions where the transformation is linear. Where possible, switch to generators to reduce memory. Benchmark with timeit. Learn the iterator protocol – it's the foundation of async libraries and web frameworks. At Meteora Web, we use these concepts every day in our Laravel and Python projects to handle data streams efficiently, even on shared servers. Saving resources means lower costs and happier clients.
If you want to dive deeper into iterators in real‑world scenarios, check out the official Python documentation on iterators.
Sponsored Protocol