
Python Course
List Comprehensions in Python
One of the most beloved features of Python is the ability to build lists in a single, readable line using list comprehensions. Instead of writing a full for loop, an empty list, and repeated append() calls, you express the entire operation in one compact expression. Python programmers use list comprehensions constantly — they are faster to write, faster to read, and in many cases faster to run than the equivalent loop.
This lesson starts with the basics and builds to filtering, nested comprehensions, the walrus operator, performance considerations, and when a regular loop is the better choice.
The Problem List Comprehensions Solve
Consider doubling every number in a list. The traditional loop takes four lines to express one simple idea:
# Traditional loop — four lines for one idea
nums = [1, 2, 3, 4, 5]
doubled = []
for n in nums:
doubled.append(n * 2)
print(doubled)
# List comprehension — one line, same result
doubled = [n * 2 for n in nums]
print(doubled)
Both produce identical output. The comprehension reads almost like English: "give me n * 2 for each n in nums." That directness is the point.
Basic Syntax and Common Transformations
The structure is always: [expression for item in iterable]. The expression is evaluated once for every item — the result is always a new list.
words = ["hello", "world", "python", "is", "great"]
# Uppercase every word
upper = [w.upper() for w in words]
print(upper)
# Get the length of each word
lengths = [len(w) for w in words]
print(lengths)
# Capitalise and add exclamation
shout = [w.capitalize() + "!" for w in words]
print(shout)
# Extract first character of each word
initials = [w[0] for w in words]
print(initials)
# Build a list of (word, length) tuples
pairs = [(w, len(w)) for w in words]
print(pairs)
- Any expression works after the opening bracket — string methods, arithmetic, function calls, even tuple literals.
- The original iterable is never modified — comprehensions always return a new list.
- Any iterable works: lists, strings, tuples, ranges, dictionaries, sets, files.
Using range() in a List Comprehension
range() pairs naturally with comprehensions to generate numeric sequences without a pre-existing list.
# Squares of 1 through 10
squares = [x ** 2 for x in range(1, 11)]
print("Squares:", squares)
# Cubes of 1 through 5
cubes = [x ** 3 for x in range(1, 6)]
print("Cubes :", cubes)
# All even numbers from 0 to 20
evens = [x for x in range(0, 22, 2)]
print("Evens :", evens)
# Fibonacci-style: build list from range and formula
# Celsius to Fahrenheit for whole-degree values 0-40
temps_f = [round((c * 9/5) + 32, 1) for c in range(0, 45, 5)]
print("Temps °F:", temps_f)
Filtering with a Condition
Add an if clause at the end to include only items that pass a test. Items where the condition is False are silently discarded.
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
# Even numbers only
evens = [n for n in nums if n % 2 == 0]
print("Even:", evens)
# Odd numbers only
odds = [n for n in nums if n % 2 != 0]
print("Odd :", odds)
# Numbers divisible by 3
by3 = [n for n in nums if n % 3 == 0]
print("÷3 :", by3)
# Filter real data — keep in-stock products
products = [
{"name": "Laptop", "price": 1200, "stock": 5},
{"name": "Monitor", "price": 450, "stock": 0},
{"name": "Keyboard", "price": 80, "stock": 20},
{"name": "Webcam", "price": 120, "stock": 0},
{"name": "Mouse", "price": 35, "stock": 50},
]
in_stock = [p["name"] for p in products if p["stock"] > 0]
print("In stock:", in_stock)
affordable = [p["name"] for p in products if p["price"] < 200 and p["stock"] > 0]
print("Affordable and in stock:", affordable)
- Structure with filter:
[expression for item in iterable if condition] - You can combine conditions with
and/orin oneifclause. - The
iftests the original item — the expression beforefortransforms it.
Transform and Filter Together
The real power comes from combining transformation and filtering in the same comprehension — include only certain items and change them at the same time.
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Square only the even numbers
even_squares = [n ** 2 for n in nums if n % 2 == 0]
print("Even squares:", even_squares)
# Clean messy strings — strip whitespace, keep non-empty
raw = [" apple ", "", " ", "banana", " cherry ", " "]
clean = [s.strip() for s in raw if s.strip()]
print("Cleaned:", clean)
# Extract discounted prices for expensive items only
prices = [15, 450, 8500, 80, 1200, 35]
big_discounted = [round(p * 0.85, 2) for p in prices if p >= 100]
print("15% off (items ≥100):", big_discounted)
# Extract emails for active users
users = [
{"email": "a@x.com", "active": True},
{"email": "b@x.com", "active": False},
{"email": "c@x.com", "active": True},
]
active_emails = [u["email"].upper() for u in users if u["active"]]
print("Active emails:", active_emails)
- Python evaluates the condition first — only items that pass are then transformed.
- This single line replaces: empty list, loop, if-check, transformation, and append — five lines of code.
Inline if/else — The Ternary Expression
Use an inline if/else (ternary) directly in the expression part to choose between two output values for every item. Every item is included — the ternary just decides which value it gets. Note the position: the ternary goes before for, not after it.
nums = [1, 2, 3, 4, 5, 6]
# Label each number as even or odd — all items included
labels = ["even" if n % 2 == 0 else "odd" for n in nums]
print(labels)
# Replace negatives with 0, keep positives as-is
data = [-3, 7, -1, 4, 0, -9, 2]
clipped = [n if n > 0 else 0 for n in data]
print(clipped)
# Apply a 20% discount to expensive items, 10% to cheap ones
prices = [50, 1200, 30, 8500, 75, 450]
discounted = [round(p * 0.8, 2) if p >= 100 else round(p * 0.9, 2) for p in prices]
print("Discounted:", discounted)
# Grade assignment
scores = [95, 72, 88, 45, 61, 80]
grades = ["A" if s >= 90 else "B" if s >= 75 else "C" if s >= 60 else "F" for s in scores]
print("Grades:", grades)
- Structure:
[val_if_true if condition else val_if_false for item in iterable] - This is different from a trailing
if— the trailingifremoves items, the ternary chooses between two values while keeping all items. - Nested ternaries work but become hard to read quickly — limit to two conditions maximum.
Nested List Comprehensions
You can nest one comprehension inside another to work with two-dimensional data — grids, matrices, or lists of lists. The loop order in a nested comprehension matches the equivalent nested loops: outer loop first, inner loop second.
# Flatten a 2D matrix into a 1D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [val for row in matrix for val in row]
print("Flat:", flat)
# Filter while flattening — keep only even values
even_flat = [val for row in matrix for val in row if val % 2 == 0]
print("Even values:", even_flat)
# Build a 3x3 multiplication table
table = [[r * c for c in range(1, 4)] for r in range(1, 4)]
for row in table:
print(row)
# Transpose a matrix (swap rows and columns)
original = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = [[row[i] for row in original] for i in range(3)]
print("Transposed:")
for row in transposed:
print(row)
- Flatten:
[expr for outer in list for inner in outer]— reads left to right like the equivalent nested loops. - Build 2D:
[[expr for col in range] for row in range]— result is a list of lists. - Transpose is a classic data-science operation — reorganises a table so columns become rows.
- Beyond two levels of nesting, a regular loop with clear variable names is almost always clearer.
Comprehensions with String Operations
Strings are iterable in Python — you can loop over characters directly, which opens up powerful one-liner text processing.
# Extract all vowels from a string
sentence = "Python is an incredible language"
vowels = [c for c in sentence if c.lower() in "aeiou"]
print("Vowels:", vowels)
print("Count :", len(vowels))
# Count characters that are digits
mixed = "p3th0n 1s aw3some"
digits = [c for c in mixed if c.isdigit()]
print("Digits:", digits)
# Reverse each word in a sentence
words = "list comprehensions are powerful".split()
reversed_words = [w[::-1] for w in words]
print("Reversed words:", reversed_words)
# Clean a list of tags — lowercase, strip, remove empty
raw_tags = [" Python ", "DATA SCIENCE", "", " ai ", "Python", " "]
clean_tags = list({t.lower().strip() for t in raw_tags if t.strip()})
print("Clean unique tags:", sorted(clean_tags))
w[::-1]reverses a string using slice notation — a Pythonic trick worth knowing.- The last example uses a set comprehension
{...}insidelist()to deduplicate automatically.
Comprehension vs map() and filter()
Before list comprehensions, Python programmers used map() and filter() with lambdas. Comprehensions are now the preferred Pythonic style — they are more readable and avoid the extra list() wrapping.
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map() vs comprehension for transformation
doubled_map = list(map(lambda n: n * 2, nums))
doubled_comp = [n * 2 for n in nums]
print(doubled_map == doubled_comp) # True — identical results
# filter() vs comprehension for filtering
evens_filter = list(filter(lambda n: n % 2 == 0, nums))
evens_comp = [n for n in nums if n % 2 == 0]
print(evens_filter == evens_comp) # True — identical results
# map() can still be useful with named functions (no lambda needed)
import math
roots_map = list(map(math.sqrt, nums))
roots_comp = [math.sqrt(n) for n in nums]
print(roots_map == roots_comp) # True
- Comprehensions are more readable than
map/filter + lambdain most cases — PEP 8 recommends them. map(named_function, list)is still clean and valid when you already have a named function — no lambda needed.map()andfilter()return lazy iterators — they do not build the list until you wrap withlist().
Performance — Comprehension vs Loop
List comprehensions are generally faster than equivalent loops in Python because they are implemented in optimised C internally. The difference grows with list size.
import time
N = 1_000_000
# Loop timing
start = time.time()
result = []
for i in range(N):
result.append(i * 2)
loop_time = time.time() - start
# Comprehension timing
start = time.time()
result = [i * 2 for i in range(N)]
comp_time = time.time() - start
print(f"Loop : {loop_time:.4f}s")
print(f"Comprehension : {comp_time:.4f}s")
print(f"Speedup : {loop_time / comp_time:.1f}x faster")
- Comprehensions avoid the overhead of repeated
list.append()attribute lookups on each iteration. - The speedup is typically 1.5–2x for simple transformations — meaningful at scale.
- For very large data (millions of items), use a generator expression
(x for x in ...)instead — it does not build the full list in memory.
When Not to Use a List Comprehension
# BAD — comprehension used only for side effects (result thrown away)
items = ["apple", "banana", "cherry"]
# [print(item) for item in items] # don't do this
# GOOD — use a loop when the purpose is side effects
for item in items:
print(item)
# BAD — over-nested, unreadable
# result = [x for sublist in [lst for lst in matrix if any(x > 5 for x in lst)] for x in sublist if x % 2 == 0]
# GOOD — break complex logic into readable steps
big_rows = [lst for lst in matrix if any(x > 5 for x in lst)]
even_vals = [x for row in big_rows for x in row if x % 2 == 0]
- Side effects — if the loop body exists to call a function for its effect (print, write to file), use a regular loop. A comprehension that discards its result is misleading.
- Complex logic — if the expression or condition needs multiple lines, a loop with named variables is clearer and easier to debug.
- Large data — a comprehension builds the entire list in memory. Use a generator expression for huge datasets:
(x * 2 for x in big_range). - Debugging — you cannot add a breakpoint inside a comprehension. During debugging, a loop lets you inspect each step.
Quick Reference Table
| Pattern | Syntax | What It Does |
|---|---|---|
| Basic transform | [expr for x in iterable] | Apply expression to every item |
| Filter | [expr for x in iterable if cond] | Include only matching items |
| Transform + filter | [f(x) for x in it if cond] | Transform only passing items |
| Ternary | [a if cond else b for x in it] | Choose between two values per item |
| Flatten 2D | [x for row in matrix for x in row] | Collapse list of lists to flat list |
| Build 2D | [[expr for c in r] for r in r] | Build matrix / list of lists |
| From range | [expr for x in range(n)] | Generate numeric sequence |
| String chars | [c for c in string if cond] | Filter or extract characters |
| Generator | (expr for x in iterable) | Lazy version — saves memory |
Practice
Write a list comprehension that produces squares of numbers 1 through 5.
Where does the if filter condition go in a list comprehension?
What type of object does a list comprehension always produce?
What is the key difference between a trailing if filter and an inline if/else ternary?
Name one situation where a regular for loop is preferred over a list comprehension.
What should you use instead of a list comprehension when processing very large datasets to save memory?
Quick Quiz
What does [x * 2 for x in range(1, 4)] produce?
Which correctly filters a list to keep only values greater than 3?
What does ["yes" if x % 2 == 0 else "no" for x in range(1, 4)] produce?
What does [val for row in [[1,2],[3,4]] for val in row] produce?
Why is a generator expression preferred over a list comprehension for very large datasets?
What does [n**2 for n in range(1,11) if n % 2 == 0] produce?