Comprehensions & Generators
List Comprehensions
Syntax, Multiple for Clauses, Nesting
A list comprehension builds a new list by applying an expression to each element of an iterable, optionally filtering with a condition:
\[\texttt{[}\ \underbrace{\textit{expr}}_{\text{output}} \ \texttt{for}\ \underbrace{x}_{\text{variable}} \ \texttt{in}\ \underbrace{\textit{iterable}}_{\text{source}} \ \underbrace{\texttt{if}\ \textit{condition}}_{\text{optional filter}} \ \texttt{]}\]
Equivalent for loop:
result = []\ \ for x in iterable:\ \ \ \ if condition: result.append(expr)
Multiple for clauses: evaluated left-to-right like nested
for loops:
\[\texttt{[f(x,y)\ for\ x\ in\ A\ for\ y\ in\ B]}
\;\equiv\; \texttt{for x in A: for y in B: append(f(x,y))}\]
Produces a new list, evaluated eagerly (immediately).

Simple transform:
[f(x) for x in it]Filtered transform:
[f(x) for x in it if p(x)]Conditional value:
[a if cond else b for x in it](ternary in expression position, not filter)Cartesian product:
[(x,y) for x in A for y in B]— outer loop firstFlatten 2D:
[x for row in matrix for x in row]Nested list comp:
[[f(x) for x in row] for row in matrix]— creates 2D result; outer loop is the outerforWalrus in filter:
[y for x in it if (y:=f(x)) > 0]— compute once, use in both filter and output
# 1. Basic transform
squares = [x**2 for x in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
# 2. Filter
evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# 3. Conditional expression (ternary in output, NOT a filter)
clipped = [x if x > 0 else 0 for x in [-3,-1,0,2,5,-4,8]]
print(clipped) # [0, 0, 0, 2, 5, 0, 8]
# 4. Cartesian product: both for clauses
pairs = [(x,y) for x in [1,2,3] for y in ['a','b']]
print(pairs)
# [(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]
# 5. Flatten 2D matrix
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat = [x for row in matrix for x in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# 6. Multiple conditions
nums = [x for x in range(100) if x % 3 == 0 if x % 5 == 0]
print(nums) # [0, 15, 30, 45, 60, 75, 90]
# 7. Nested list comprehension (2D result)
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed) # [[1,4,7],[2,5,8],[3,6,9]]
# 8. Walrus operator: avoid double computation
import math
results = [root for x in range(20) if (root := math.sqrt(x)) > 3.5]
print(results[:4]) # [3.872..., 4.0, 4.123..., ...]
# 9. String processing
words = ["hello", "world", "python", "gate", "exam"]
titled = [w.title() for w in words if len(w) > 4]
print(titled) # ['Hello', 'World', 'Python']
# 10. Dict/set from comprehension intermediary
matrix2 = [[1,0,3],[0,5,0],[7,0,9]]
non_zero = [(i,j) for i,row in enumerate(matrix2)
for j,v in enumerate(row) if v != 0]
print(non_zero) # [(0,0),(0,2),(1,1),(2,0),(2,2)]Dictionary Comprehensions
Key-Value Mapping Expressions
\[\texttt{\{}\ \underbrace{k\texttt{:}v}_{\text{key:value}} \ \texttt{for}\ \langle\textit{vars}\rangle \ \texttt{in}\ \langle\textit{iterable}\rangle \ [\texttt{if}\ \langle\textit{condition}\rangle] \ \texttt{\}}\]
Produces a new dict; if the same key appears more than once, the last value wins (later iteration overwrites earlier).
# 1. Transform values
nums = {"a": 1, "b": 2, "c": 3}
squared = {k: v**2 for k, v in nums.items()}
print(squared) # {'a': 1, 'b': 4, 'c': 9}
# 2. Filter entries
passing = {k: v for k, v in nums.items() if v > 1}
print(passing) # {'b': 2, 'c': 3}
# 3. Invert: value -> key (assumes unique values)
orig = {"a": 1, "b": 2, "c": 3}
inv = {v: k for k, v in orig.items()}
print(inv) # {1: 'a', 2: 'b', 3: 'c'}
# 4. From two lists (zip)
keys = ["name", "age", "city"]
values = ["Alice", 25, "Delhi"]
d = {k: v for k, v in zip(keys, values)}
print(d) # {'name': 'Alice', 'age': 25, 'city': 'Delhi'}
# 5. Character frequency
text = "abracadabra"
freq = {ch: text.count(ch) for ch in set(text)}
print(freq) # {'r':2,'c':1,'a':5,'d':1,'b':2}
# 6. Conditional key transformation
words = ["hello", "WORLD", "Python", "GATE"]
normalised = {w.lower(): len(w) for w in words}
print(normalised) # {'hello':5,'world':5,'python':6,'gate':4}
# 7. Nested dict comprehension
matrix = [[1,2,3],[4,5,6],[7,8,9]]
indexed = {(i,j): matrix[i][j]
for i in range(3) for j in range(3)}
print(indexed[(1,2)]) # 6
# 8. Duplicate key: last wins
data = [("a",1),("b",2),("a",3),("b",4)]
d2 = {k: v for k, v in data}
print(d2) # {'a': 3, 'b': 4} (last 'a' and 'b' values win)Set Comprehensions
Unique-Element Expressions
\[\texttt{\{}\ \langle\textit{expr}\rangle \ \texttt{for}\ \langle\textit{var}\rangle \ \texttt{in}\ \langle\textit{iterable}\rangle \ [\texttt{if}\ \langle\textit{condition}\rangle] \ \texttt{\}}\]
Automatically deduplicates: equivalent to set([...\ comprehension\ ...]).
Unordered: iteration order is not guaranteed.
Note: {expr} without for is a set literal; empty
set must be set(), not {}.
# 1. Unique squares
sq = {x**2 for x in range(-4, 5)}
print(sorted(sq)) # [0, 1, 4, 9, 16] (deduped: (-2)^2 == 2^2)
# 2. Unique first letters
words = ["apple","ant","banana","blueberry","avocado"]
initials = {w[0] for w in words}
print(initials) # {'a', 'b'}
# 3. Common elements (set comprehension on two lists)
a = [1,2,3,4,5,3,2]
b = [3,4,5,6,7,4,3]
common = {x for x in a if x in b}
print(common) # {3, 4, 5}
# 4. Characters appearing in both strings
s1, s2 = "abcde", "cdefg"
shared = {c for c in s1 if c in s2}
print(shared) # {'c', 'd', 'e'}
# 5. Deduplication with transformation
data = [3, -3, 1, -1, 2, -2, 1]
unique_abs = {abs(x) for x in data}
print(unique_abs) # {1, 2, 3}Generator Expressions
Lazy Iterators from Expressions
A generator expression has the same syntax as a list comprehension but uses parentheses (or no delimiters when the sole argument to a function):
\[\texttt{(}\ \langle\textit{expr}\rangle\ \texttt{for}\ x\ \texttt{in} \ \langle\textit{iterable}\rangle\ [\texttt{if}\ \langle\textit{cond}\rangle] \ \texttt{)}\]
Key differences from list comprehension:


import sys
# Memory: generator vs list comprehension
lst = [x**2 for x in range(1000)]
gen = (x**2 for x in range(1000))
print(sys.getsizeof(lst)) # ~8056 bytes
print(sys.getsizeof(gen)) # ~104 bytes (just the generator object)
# Generator as argument (no extra parens needed)
total = sum(x**2 for x in range(1001)) # sum of squares 0..1000
print(total) # 333833500
any_neg = any(x < 0 for x in [1, 2, -3, 4])
print(any_neg) # True (short-circuits at -3!)
all_pos = all(x > 0 for x in [1, 2, 3, 4])
print(all_pos) # True
# Lazy pipeline: large file simulation
def read_lines():
for i in range(1, 6):
yield f"line {i}: data={i*10}"
lines = (line for line in read_lines()) # lazy source
numbers = (int(line.split("=")[1]) for line in lines) # lazy parse
big = (n for n in numbers if n > 20) # lazy filter
result = list(big) # materialise
print(result) # [30, 40, 50]
# Exhaustion: generator can only be iterated ONCE
g = (x for x in range(3))
print(list(g)) # [0, 1, 2]
print(list(g)) # [] EMPTY -- generator exhausted!
# Compare with list (reusable)
lst2 = [x for x in range(3)]
print(list(lst2)) # [0, 1, 2]
print(list(lst2)) # [0, 1, 2] still works!
# next() on generator
g2 = (x**2 for x in range(5))
print(next(g2)) # 0
print(next(g2)) # 1
print(next(g2)) # 4
print(list(g2)) # [9, 16] rest consumedGenerator Functions
yield Statement
Suspending and Resuming Execution
Any function containing a yield statement becomes a
generator function. Calling it returns a generator object
(an iterator) without executing any of the function body.
Execution model:
Calling
gen_func()returns a generator object; body not run yetnext(gen)runs until the nextyield, suspends, and returns the yielded valuenext(gen)again resumes from after theyieldWhen the function returns (or falls off the end), a
StopIterationis raised;return valuesetsStopIteration.value
State preserved between calls: local variables, loop counters, execution pointer — the frame is kept alive, not torn down.

# Basic generator
def gen123():
print("start")
yield 1
print("after 1")
yield 2
print("after 2")
yield 3
print("done")
g = gen123()
print(type(g)) # <class 'generator'>
print(next(g)) # start / 1
print(next(g)) # after 1 / 2
print(next(g)) # after 2 / 3
try:
next(g) # done / StopIteration
except StopIteration:
print("exhausted")
# for loop consumes automatically
for v in gen123():
print(v) # start / 1 / after 1 / 2 / after 2 / 3 / done
# return in generator: sets StopIteration.value
def gen_with_return():
yield 1
yield 2
return "final" # StopIteration.value = "final"
g2 = gen_with_return()
print(next(g2)) # 1
print(next(g2)) # 2
try:
next(g2)
except StopIteration as e:
print(e.value) # final
# Infinite generator (only possible with generator; not with list!)
def natural_numbers(start=1):
n = start
while True:
yield n
n += 1
gen = natural_numbers()
print([next(gen) for _ in range(7)]) # [1, 2, 3, 4, 5, 6, 7]
# Fibonacci generator
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Practical: large file reading without loading all into memory
def read_chunks(text, size=5):
for i in range(0, len(text), size):
yield text[i:i+size]
for chunk in read_chunks("Hello World Python GATE", size=5):
print(repr(chunk), end=" ")
# 'Hello' ' Worl' 'd Pyt' 'hon G' 'ATE'