Three lines to build a list with a loop, or one line that says the same thing and runs faster. That trade is the whole appeal of the Python list comprehension, and it is why the syntax shows up in nearly every serious codebase. This post covers the basic form, filtering with if, nested versions, and the readability line you should not cross, all with tested examples.
“Simple is better than complex. Readability counts.”
Tim Peters, The Zen of Python
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 17 minutes
You have written this loop a hundred times: make an empty list, walk over something, append one item at a time. It works fine. But it is four or five lines of code for what is really a single thought. The Python list comprehension squeezes that whole pattern into one clean line.
Think of it like a coffee order. The long way is “take a cup, pour the coffee, add the milk, add the sugar, hand it over,” step by step. The short way is “one latte with sugar.” Same drink, one sentence. A comprehension is the short way of saying “build me this list.”
Fair warning though. Once comprehensions click, you start wanting to cram everything into one line, even the stuff that should never live on one line. The real skill is knowing when a comprehension makes the code clearer and when it just shows off. A one-liner nobody can read is worse than a plain three-line loop.
Table of Contents
The Problem: Too Much Boilerplate
Here is a pattern you have written dozens of times already. A teacher has a list of test scores and wants to add a 5 point curve to every one of them:
📄 the_old_way.py: building a list with a for loop
scores = [72, 88, 95, 61, 84, 90, 78]
# The old way: create empty list, loop, append
curved = []
for score in scores:
curved.append(score + 5)
print(curved)
▶ Output
[77, 93, 100, 66, 89, 95, 83]
Four lines to say one thing: “take each score and add 5.” The empty list, the loop, the append call, all of that is just plumbing. The only part that actually matters is score + 5. Everything else is there to keep the plumbing happy.
The Solution: List Comprehension Syntax
Here is the exact same job, the exact same result, in one line:
📄 comprehension_basic.py: same result, one line
scores = [72, 88, 95, 61, 84, 90, 78] curved = [score + 5 for score in scores] print(curved)
▶ Output
[77, 93, 100, 66, 89, 95, 83]
What happened here: The shape is [expression for item in iterable]. Read it out loud as “give me expression for each item in iterable.” Python runs the expression once for every element and gathers the results into a brand new list. No empty list to set up first, no append() call, no temporary variable. The square brackets are the giveaway: they tell you a list is coming out the other end.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram breaks the list comprehension into its core parts: the expression (what to produce), the loop variable (each item as it comes through), and the iterable (where the items come from). On top of that, an optional if at the end filters items out before the expression ever touches them. The right-hand side mirrors the equivalent for loop, so you can see that a comprehension is not magic, it is the same loop folded onto one line. Once you can spot these parts, you can read almost any comprehension you run into.
The expression slot is not limited to math. Here are a few more shapes it can take, using a small project team of four developers named Rahul, Niranjan, Viraj, and Pravin as the sample data:
📄 more_examples.py: the expression can be anything
# Squares
squares = [n ** 2 for n in range(1, 8)]
print(f"Squares: {squares}")
# Uppercase names
team = ["rahul", "niranjan", "viraj", "pravin"]
upper = [name.upper() for name in team]
print(f"Uppercase: {upper}")
# Lengths
lengths = [len(name) for name in team]
print(f"Lengths: {lengths}")
# String formatting
labels = [f"Player {i}" for i in range(1, 6)]
print(f"Labels: {labels}")
▶ Output
Squares: [1, 4, 9, 16, 25, 36, 49] Uppercase: ['RAHUL', 'NIRANJAN', 'VIRAJ', 'PRAVIN'] Lengths: [5, 8, 5, 6] Labels: ['Player 1', 'Player 2', 'Player 3', 'Player 4', 'Player 5']
What happened here: The expression on the left is just normal Python. It can be math (n ** 2), a method call (name.upper()), a function call (len(name)), or an f-string. Whatever you would put after return in a tiny function, you can put in that slot.
Comprehension with Filtering
So far the comprehension touches every item. Most of the time you want only some of them. Add an if at the end and Python keeps only the items that pass. Think of it like a bouncer at a club door: the if checks each item, and only the ones on the list get in. The last example below filters our team roster, which has picked up two more developers, Prathamesh and Aviraj:
📄 filtering.py: [expression for item in iterable if condition]
scores = [72, 88, 95, 61, 84, 90, 78]
# Only passing scores (>= 80)
passing = [s for s in scores if s >= 80]
print(f"Passing: {passing}")
# Even numbers from 1-20
evens = [n for n in range(1, 21) if n % 2 == 0]
print(f"Evens: {evens}")
# Names longer than 5 characters
team = ["Rahul", "Niranjan", "Viraj", "Prathamesh", "Aviraj"]
long_names = [name for name in team if len(name) > 5]
print(f"Long names: {long_names}")
▶ Output
Passing: [88, 95, 84, 90] Evens: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] Long names: ['Niranjan', 'Prathamesh', 'Aviraj']
What happened here: The if at the end is a filter, nothing more. Python checks the condition for each item. If it comes back True, the item goes through the expression and lands in the new list. If it comes back False, the item is quietly dropped and never shows up. Notice the order in your head: filter first, then transform.
Transform and Filter Combined
You can do both jobs at once: change each item AND drop the ones you do not want. Picture an airport. The boarding gate is the filter: you either get on the plane or you do not. The seat assignment is the transform: everyone who boards gets something, window or aisle. A comprehension can play both roles in the same line. There is one trap here that catches almost everyone, so read the explanation after the code carefully.
📄 transform_filter.py: transform and filter in the same line
scores = [72, 88, 95, 61, 84, 90, 78]
# Add 5 curve, but only keep scores that were already >= 70
curved_passing = [s + 5 for s in scores if s >= 70]
print(f"Curved passing: {curved_passing}")
# if/else INSIDE the expression (a ternary), not the same as if at the end
labels = ["PASS" if s >= 80 else "FAIL" for s in scores]
print(f"Labels: {labels}")
# Convert strings to int, but only valid ones
raw = ["42", "hello", "7", "world", "99", "3"]
numbers = [int(x) for x in raw if x.isdigit()]
print(f"Numbers: {numbers}")
▶ Output
Curved passing: [77, 93, 100, 89, 95, 83] Labels: ['FAIL', 'PASS', 'PASS', 'FAIL', 'PASS', 'PASS', 'FAIL'] Numbers: [42, 7, 99, 3]
What happened here: This is the part that trips people up. The word if can sit in two completely different spots, and they do opposite things. At the end (for s in scores if s >= 70) it is a filter: it decides which items get in at all, and there is no else. Inside the expression ("PASS" if s >= 80 else "FAIL") it is a ternary: every item gets through, and the if/else just picks what value to produce.
Filter at the end drops items. Ternary at the front keeps them all and changes them. Mix these two up and you either lose data or get a SyntaxError, which is exactly the mistake we cover later in this post.
Nested Loops in Comprehensions
You can stack more than one for in a single comprehension. This is how you flatten a list of lists or build every combination of two things, like every size of every color on a clothing rack.
📄 nested.py: more than one for clause
# Flatten a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(f"Flat: {flat}")
# All combinations
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
combos = [f"{color}-{size}" for color in colors for size in sizes]
print(f"Combos: {combos}")
# Nested with filter
pairs = [(x, y) for x in range(4) for y in range(4) if x != y]
print(f"Pairs where x != y: {pairs}")
▶ Output
Flat: [1, 2, 3, 4, 5, 6, 7, 8, 9] Combos: ['red-S', 'red-M', 'red-L', 'blue-S', 'blue-M', 'blue-L'] Pairs where x != y: [(0, 1), (0, 2), (0, 3), (1, 0), (1, 2), (1, 3), (2, 0), (2, 1), (2, 3), (3, 0), (3, 1), (3, 2)]
What happened here: Read the for clauses left to right and they line up exactly with nested loops written top to bottom. for row in matrix for num in row means “for each row, then for each number in that row.” The first for you write is the outer loop, the second is the inner one. That order feels backwards at first because the expression sits all the way on the left, but the loops still run in the order you read them.
Comprehension with Function Calls
The expression can call your own functions too. It is like a restaurant kitchen: the order slip stays one line (“one masala dosa”) while all the actual chopping and cooking happens at a separate station. The comprehension is the order slip, the function is the station doing the messy work. The second example cleans up sloppy sign-up form entries from three new users named Anvi, Anvay, and Aditi, complete with stray spaces and blank submissions:
📄 with_functions.py: calling functions inside a comprehension
def grade(score):
if score >= 90: return "A"
if score >= 80: return "B"
if score >= 70: return "C"
return "F"
scores = [95, 82, 71, 60, 88, 93]
grades = [grade(s) for s in scores]
print(f"Grades: {grades}")
# Strip and filter empty strings
raw_data = [" Anvi ", "", " Anvay", " ", "Aditi ", ""]
clean = [s.strip() for s in raw_data if s.strip()]
print(f"Clean: {clean}")
▶ Output
Grades: ['A', 'B', 'C', 'F', 'B', 'A'] Clean: ['Anvi', 'Anvay', 'Aditi']
What happened here: The grade() function holds the if-ladder, so the comprehension stays a clean one-liner: [grade(s) for s in scores]. In the second example, notice s.strip() runs twice, once in the filter to test for empty strings and once in the expression to actually clean the value. That is a little wasteful, but for short lists it reads clearly and runs fast enough that nobody will notice.
When NOT to Use Comprehensions
Comprehensions are great right up until they are not. The line between “clean one-liner” and “what on earth does this do” is roughly the width of your screen. An overloaded comprehension is like a sentence with no punctuation: technically valid, exhausting to read. When a comprehension grows a filter, a transform, and three conditions all at once, it stops being readable. Here is one that has crossed the line:
🚫 Too much going on: use a regular loop instead
# Please do not do this, nobody can read it
result = [x.strip().lower() for x in data if x.strip() and not x.startswith("#") and len(x.strip()) > 3]
✅ The same thing as a plain loop, way easier to read
# Much clearer as a loop
result = []
for x in data:
cleaned = x.strip()
if cleaned and not x.startswith("#") and len(cleaned) > 3:
result.append(cleaned.lower())
A plain loop is the better call in a few other spots too: when you need a try/except around each step, when the body does real work like printing or writing to a file, or when you want to break out early once you find what you need. Remember the one job a comprehension is built for: producing a new list. The moment your goal is something other than building a list, reach for a normal loop.
Real-World Patterns
Enough toy examples. Here are the comprehensions you will actually reach for on the job: pulling one field out of a list of records, building a quick lookup table, skipping comment lines in a config file, and generating test data. The records below belong to three students named Aditi, Aviraj, and Anvi:
📄 real_world.py: patterns you will actually use
# 1. Extract specific fields from records
students = [
{"name": "Aditi", "score": 95},
{"name": "Aviraj", "score": 82},
{"name": "Anvi", "score": 91},
]
names = [s["name"] for s in students]
print(f"Names: {names}")
# 2. Create a lookup mapping (dict comprehension preview)
scores_map = {s["name"]: s["score"] for s in students}
print(f"Scores: {scores_map}")
# 3. Process file lines (simulated)
lines = ["# comment", "name=Aditi", "age=28", "", "city=Pune"]
config = [line for line in lines if line and not line.startswith("#")]
print(f"Config lines: {config}")
# 4. Generate test data
test_emails = [f"user{i}@example.com" for i in range(1, 6)]
print(f"Emails: {test_emails}")
▶ Output
Names: ['Aditi', 'Aviraj', 'Anvi']
Scores: {'Aditi': 95, 'Aviraj': 82, 'Anvi': 91}
Config lines: ['name=Aditi', 'age=28', 'city=Pune']
Emails: ['user1@example.com', 'user2@example.com', 'user3@example.com', 'user4@example.com', 'user5@example.com']
What happened here: Pattern 1 is the one you will use most. Pulling a single field out of a list of dictionaries with [s["name"] for s in students] shows up constantly when you work with rows from a database or items from a JSON API (Application Programming Interface). Pattern 2 sneaks in a dict comprehension, which uses curly braces and a key: value expression. It is the same idea as a list comprehension, just building a dictionary instead, and it gets its own post next. The other two patterns are everyday cleanup: filtering junk lines and generating throwaway data for tests.
Common Mistakes
Mistake 1: Confusing filter-if with ternary-if
🚫 SyntaxError: a ternary in the expression needs an else
# This FAILS: an if in the expression slot must have an else result = ["PASS" if s >= 80 for s in scores] # SyntaxError: expected 'else' after 'if' expression
✅ Correct: a ternary needs both if and else
# Ternary in expression: must have else result = ["PASS" if s >= 80 else "FAIL" for s in scores] # Filter at end: no else needed result = [s for s in scores if s >= 80]
Mistake 2: Using comprehension for side effects
🚫 Bad: a comprehension built only to print
# Don't do this, it builds a throwaway list of None values [print(x) for x in items]
✅ Just use a plain loop
for x in items:
print(x)
The bad version does print each item, so it looks like it works. The catch is that print() returns None, so the comprehension quietly hands you back [None, None, None] and you build a useless list just to throw it away. If you only want the side effect (printing, writing a file, sending a request), use a loop. Save comprehensions for when you actually want the list that comes out.
Mistake 3: Nesting the comprehension too deep
Once a comprehension has more than two for clauses, it is almost always clearer as a regular loop. Three levels of nesting on one line reads like a tongue twister. The readability cliff is real, and your future self reviewing this code at 5 pm on a Friday will thank you for using a plain loop.
Best Practices
- DO use comprehensions for simple transformations:
[x.upper() for x in names] - DO use comprehensions for simple filters:
[x for x in items if x > 0] - DO break long comprehensions across multiple lines for readability
- DON’T use comprehensions for side effects (printing, file writes)
- DON’T nest more than two
forclauses in a comprehension - DON’T trade readability for cleverness. If the comprehension needs a comment to explain itself, use a loop
Conclusion
The Python list comprehension folds the make-a-list, loop, append pattern into one expression: [expression for item in iterable if condition]. It runs a little faster than the equivalent loop, because Python skips the repeated append method lookup, and it reads better for simple transforms. It is one of the most Pythonic things you can write. Just respect the readability ceiling: the moment a comprehension gets gnarly, drop back to a plain loop with a clear conscience.
Next up: Build a Number Guessing Game, your first real program that pulls together loops, conditionals, input, and randomness into something you can actually play. And if you want the full roadmap from beginner basics to AI/ML, browse every post at the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Create squares from 1 to 20 with a list comprehension.
- Exercise 2: Filter words longer than 5 characters from a sentence.
- Exercise 3: Flatten a 3-level nested list using nested comprehensions.
Frequently Asked Questions
What is a list comprehension in Python?
A Python list comprehension is a concise syntax for creating lists: [expression for item in iterable]. It replaces the pattern of creating an empty list, looping, and appending. Example: [x**2 for x in range(5)] produces [0, 1, 4, 9, 16].
Are list comprehensions faster than for loops?
Yes. List comprehensions are typically 10-30% faster than equivalent for loops because Python optimizes the internal append operation. The bytecode for a comprehension avoids the overhead of looking up and calling the append method on each iteration.
Can I use if-else in a list comprehension?
Yes, but placement matters. A ternary if-else goes in the expression and must include both parts: [x if x > 0 else 0 for x in items]. A filter if (with no else) goes at the end: [x for x in items if x > 0]. Writing an if without an else in the expression slot raises SyntaxError: expected 'else' after 'if' expression.
How do nested list comprehensions work?
Multiple for clauses read left to right, matching nested loops top to bottom. [x+y for x in [1,2] for y in [10,20]] gives [11, 21, 12, 22]. The first for is the outer loop, the second is the inner loop.
When should I NOT use a list comprehension?
Avoid comprehensions when: the logic needs try/except, you need side effects (printing, file I/O), you need to break early, or the expression is too complex to read on one line. If your comprehension needs a comment to explain it, use a regular loop instead.
What is the difference between a list comprehension and a generator expression?
A list comprehension uses square brackets [x for x in items] and creates the entire list in memory. A generator expression uses parentheses (x for x in items) and produces items lazily, one at a time. Generators use less memory for large datasets. Covered in detail in the generators tutorial.
Interview Questions on Python List Comprehension
Scenario questions, not trivia: this is the form this topic takes in a real interview.
Q: What happens to the loop variable after a list comprehension finishes?
In Python 3, including 3.14, a list comprehension runs in its own scope, so the loop variable does not leak out. After squares = [n ** 2 for n in range(5)], calling print(n) raises NameError unless n was already defined elsewhere. This is different from a regular for loop, where the variable keeps its last value after the loop ends. Interviewers ask this because Python 2 comprehensions did leak the variable, and the change trips up people with older experience.
Q: Can a list comprehension replace map() and filter()?
Yes. [f(x) for x in items] does the same job as map(f, items), and adding if cond(x) at the end covers filter(). One difference worth mentioning: in Python 3, map() and filter() return lazy iterators, while a list comprehension builds the full list immediately. For readability, most Python style guides prefer the comprehension, especially when you would otherwise need a lambda.
Q: Your comprehension calls an expensive function twice, once in the filter and once in the expression: [clean(x) for x in rows if clean(x)]. How do you fix the double call?
Use the walrus operator to compute the value once and reuse it: [y for x in rows if (y := clean(x))]. Here clean(x) runs a single time per row, its result is stored in y, tested by the filter, and then collected. If the walrus syntax feels unreadable to the team, a plain loop with a temporary variable is a perfectly good alternative. Either way, the fix is the same idea: compute once, use twice.
Q: Your script loads a 2 GB log file with lines = [line for line in f] and memory usage spikes: what do you check first?
Check whether you actually need every line in memory at the same time. A list comprehension materializes the entire result at once, so 2 GB of lines becomes 2 GB (or more) of RAM. If the code only walks through the lines once, iterate the file object directly with for line in f: or switch to a generator expression, which yields one line at a time. Keep the list comprehension only when you genuinely need random access or multiple passes over the data.
Q: What does a list comprehension return when the iterable is empty?
It returns an empty list, [], with no error. The expression and the filter never run because there are no items to feed them. This is a quietly useful property: downstream code can loop over the result or check len() without any special empty-input handling. It also means a buggy filter that rejects everything fails silently, so print the result while debugging.
Q: In a code review you find a teammate, say a developer named Anvay, has packed three for clauses and two if conditions into one comprehension. What do you suggest?
Suggest rewriting it as nested for loops, or pulling the filtering logic into a small named function so the comprehension shrinks back to one readable clause. Two for clauses is a sensible ceiling; beyond that, the left-to-right reading order stops matching how people think. The rewritten loop compiles to nearly the same work, so there is no real performance cost, only a readability win for the next person who maintains it.
Go deeper: the official Python documentation covers every edge case of this topic.
Related Posts
Previous: Python: List Methods, append to sort Every Method Explained
Next: Python: Build a Number Guessing Game, Your First Real Program
Series Home: Python + AI/ML Tutorial Series

No comment