List Comprehensions in Python: Clean Syntax, Real Performance, Practical Limits
List comprehensions let you build a new list from an iterable in a single expression. They are not just syntactic sugar — CPython compiles them differently from for loops, and the result is measurably faster for most workloads.
Basic Syntax
# General form:# [expression for item in iterable if condition]
# Basic: squares of 0..9squares = [x**2 for x in range(10)]print(squares)# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# With condition: only even squareseven_squares = [x**2 for x in range(10) if x % 2 == 0]print(even_squares)# [0, 4, 16, 36, 64]
# Transform stringswords = ["hello", "world", "python"]upper = [w.upper() for w in words]print(upper)# ['HELLO', 'WORLD', 'PYTHON']Comprehension vs for Loop — Performance
Comprehensions are faster because CPython uses the LIST_APPEND bytecode opcode instead of calling list.append() as a method lookup on each iteration. The difference is typically 20-35% for simple cases.
import timeit
# For loop versiondef squares_loop(n): result = [] for x in range(n): result.append(x**2) return result
# Comprehension versiondef squares_comp(n): return [x**2 for x in range(n)]
n = 100_000loop_time = timeit.timeit(lambda: squares_loop(n), number=100)comp_time = timeit.timeit(lambda: squares_comp(n), number=100)
print(f"Loop: {loop_time:.3f}s")print(f"Comprehension: {comp_time:.3f}s")# Comprehension is typically fasterConditional Expressions (if-else in the Expression)
The if condition at the end filters elements. An if-else in the expression position transforms them:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Filter: keep only evensevens = [x for x in numbers if x % 2 == 0]print(evens) # [2, 4, 6, 8, 10]
# Transform: label each number as even or oddlabels = ["even" if x % 2 == 0 else "odd" for x in numbers]print(labels)# ['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even']
# Both: filter and transformbig_evens = [x * 10 for x in numbers if x > 5 and x % 2 == 0]print(big_evens) # [60, 80, 100]Nested Comprehensions
Nested comprehensions flatten nested structures or generate cartesian products:
# Flatten a 2D listmatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]flat = [item for row in matrix for item in row]print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Cartesian productsizes = ["S", "M", "L"]colours = ["red", "blue"]variants = [f"{s}-{c}" for s in sizes for c in colours]print(variants)# ['S-red', 'S-blue', 'M-red', 'M-blue', 'L-red', 'L-blue']
# Generate a 3x3 identity matrixidentity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]for row in identity: print(row)# [1, 0, 0]# [0, 1, 0]# [0, 0, 1]The order of for clauses in a comprehension matches the order they would appear in nested for loops — outermost first.
Set and Dict Comprehensions
The same syntax works for sets and dicts:
# Set comprehension — unique values onlywords = ["cat", "dog", "cat", "bird", "dog"]unique_lengths = {len(w) for w in words}print(unique_lengths) # {3, 4} (cat/dog=3, bird=4)
# Dict comprehensionnames = ["Alice", "Bob", "Carol"]name_lengths = {name: len(name) for name in names}print(name_lengths) # {'Alice': 5, 'Bob': 3, 'Carol': 5}
# Invert a dictoriginal = {"a": 1, "b": 2, "c": 3}inverted = {v: k for k, v in original.items()}print(inverted) # {1: 'a', 2: 'b', 3: 'c'}Where the Readability Limit Is
Comprehensions have a readability limit. A good heuristic: if it does not fit comfortably on two lines, rewrite it as a loop.
# Readable — one condition, simple expressionresult = [x**2 for x in range(100) if x % 3 == 0]
# Getting harder to read — two loops and a conditionflat_filtered = [item for row in matrix for item in row if item > 3]
# Too complex — use a regular loop# Bad:result = [f(x) for x in data if condition1(x) and condition2(x) and x not in seen]
# Good:result = []for x in data: if condition1(x) and condition2(x) and x not in seen: result.append(f(x))Generator Expression vs List Comprehension
If you only need to iterate the result once, use a generator expression (round brackets instead of square) — it produces values lazily and uses far less memory:
# List comprehension — builds the full list in memorytotal = sum([x**2 for x in range(1_000_000)])
# Generator expression — computes one value at a timetotal = sum(x**2 for x in range(1_000_000)) # no brackets needed inside sum()
# Memory comparisonimport syslist_comp = [x**2 for x in range(1000)]gen_exp = (x**2 for x in range(1000))print(sys.getsizeof(list_comp)) # ~8856 bytesprint(sys.getsizeof(gen_exp)) # ~112 bytes (just the generator object)Use a list comprehension when you need the full list (multiple iterations, indexing, len()). Use a generator expression when you are feeding the result directly into sum(), max(), any(), all(), or another single-pass consumer.