Python Lesson 13: Turn 4 Lines Into 1 With Comprehensions
📄 Full written solution: https://interview-kit-fe.vercel.app/python-lesson-13-comprehensions 💻 Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/python-lesson-13-comprehensions Chapters: 0:00 Turn 4 Lines Of Python Into 1 0:13 The everyday picture 0:29 The old way first 0:47 List comprehension 1:01 Naming the parts 1:16 Add a condition 1:32 if in front is different 1:50 Set and dict versions 2:08 Nesting: a grid 2:26 See the nesting run 2:41 Common mistakes 2:57 When NOT to use one 3:17 Recap Learn Python comprehensions the clear way. We start with the old for-loop version, then collapse 4 lines into 1 and name every part so it never looks like magic. You'll add conditions two ways (filter at the end vs. if/else in front), build set and dict comprehensions, and nest them into a grid. Finish with the common mistakes and the times you should NOT reach for a comprehension at all. #Python #LearnPython #Comprehensions #Coding #PythonForBeginners Watch next: - Lesson: How Do Apps Survive a Million Users Without Crashing?: https://youtu.be/wHWdfeklLG4 - How Do Apps Survive a Million Users Without Crashing? #shorts: https://youtu.be/HqAIQt9EtZg - Lesson 10: Why Is Dictionary Lookup Basically Instant?: https://youtu.be/zZhAaPjLOcA
8. Introduction
Every Python developer hits the same wall early on. You write a small loop that builds a list — three or four honest lines — and then you see a senior engineer collapse the exact same logic into a single line that reads almost like an English sentence. It feels like a magic trick, and magic tricks are uncomfortable when you're the one who has to maintain the code.
Comprehensions are not magic. They are one of the most quietly important tools in Python because they show up everywhere: in data processing, in interview answers, in library source code, and in code review comments that say "this could be a comprehension." Interviewers like them because they test something deeper than syntax — they test whether you can express a transformation clearly. Can you take a collection, change each item, optionally filter some out, and collect the result, without drowning in boilerplate?
More importantly, comprehensions teach a habit that carries into serious software engineering: separating what you want from how you loop to get it. That mental split makes you faster at reading unfamiliar Python and calmer when you write your own. By the end of this article, you'll read comprehensions at a glance and — just as valuably — you'll know the exact moments when reaching for one is the wrong call.
9. Problem Overview
Here's the recurring task, stated plainly:
You have a collection of items. You want to produce a new collection where each item has been transformed in some way, and possibly where some items have been dropped based on a condition. Build that new collection cleanly.
The classic, universal way to do this is a loop: create an empty container, walk through the source one item at a time, transform each item, and append the result. It works flawlessly. The problem isn't correctness — it's noise. You spend three lines of ceremony (empty list, for, append) to express one simple idea.
A comprehension is Python's built-in shorthand for exactly this pattern. It lets you write the transformation as a single expression that says, in order: what you want, then where it comes from, then which items qualify. The same shape builds lists, sets, and dictionaries — only the brackets and punctuation change.
Think of a factory conveyor belt. Raw items roll in one end, each one gets a quick stamp, finished items pile up at the other end. That's a comprehension: take each item, change it, collect the results.
10. Example
Let's make it concrete with the most common case: squaring a list of numbers.
Input: [1, 2, 3, 4, 5]
Goal: produce each number multiplied by itself.
Output: [1, 4, 9, 16, 25]
Every output element is simply n * n. The 1 becomes 1, the 2 becomes 4, the 3 becomes 9, and so on. Nothing is added or removed — it's a pure one-to-one transform.
Now a filtered example. From the same input, keep only the even numbers:
Input: [1, 2, 3, 4, 5]
Output: [2, 4]
Here 1, 3, and 5 are dropped entirely because they fail the "is even" test. The output is shorter than the input — that's the signature of a filter.
And a choosing example. Label each number "even" or "odd":
Input: [1, 2, 3]
Output: ["odd", "even", "odd"]
Notice the output is the same length as the input. Nothing was dropped — every item was mapped to one of two values. That distinction (filter vs. choose) is the single most important idea in this whole article.
11. Intuition
An experienced engineer doesn't memorize comprehension syntax as a formula. They think in terms of a pipeline and ask three questions in order:
- What do I want each output item to look like? — the expression.
- Where do the input items come from? — the loop.
- Do I want all of them, or only some? — the filter.
Start from the loop you already know how to write:
squares = []
for n in [1, 2, 3, 4, 5]:
squares.append(n * n)
Now watch the pieces migrate. The thing being appended (n * n) moves to the front. The loop header (for n in [1, 2, 3, 4, 5]) moves to the back. The empty list and the .append() call disappear entirely, because the surrounding brackets handle collection for you:
squares = [n * n for n in [1, 2, 3, 4, 5]]
Read it out loud: "give me n times n, for every n in this list." The front says what, the back says where from. Once you see that swap, you genuinely cannot unsee it.
The reason this reads so naturally is that it matches how we describe the task in plain language. We don't say "create an empty list, then iterate, then append." We say "I want the squares of these numbers." Comprehensions let the code match the sentence.
12. Brute Force Solution
Idea: the explicit loop. Build an empty list and append transformed items one at a time.
Algorithm:
- Create an empty list
result. - Loop over each item in the source.
- Transform the item.
- Append the transformed value to
result. - Return
result.
def squares_loop(numbers):
result = []
for n in numbers:
result.append(n * n)
return result
Advantages:
- Maximally readable for newcomers — nothing is hidden.
- Trivial to extend when logic gets complicated (multiple steps, logging, branching).
- Easy to drop a breakpoint or
printinside for debugging.
Disadvantages:
- Verbose for simple transforms — three lines of scaffolding for one idea.
- Slightly slower than a comprehension because each
.appendis a separate method lookup and call. - The intent ("I want squares") is buried under mechanics ("empty list, loop, append").
Time complexity: O(n) — we touch each of the n items once. Space complexity: O(n) — we build a new list of n items.
13. Optimal Solution
The comprehension keeps the same O(n) work but removes the ceremony and runs a touch faster. Let's build up every variation.
Basic transform:
squares = [n * n for n in numbers]
Filter — if at the back. A bouncer at the door. The if decides which items are even allowed onto the belt. It keeps or drops — no else.
evens = [n for n in numbers if n % 2 == 0]
Choose — if/else at the front. When the if sits in front of the for, it isn't filtering — it's picking between two values for every item, so it must have an else.
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
Here is the distinction as a table — internalize this and comprehensions stop tripping you up:
Position of if |
Job | Needs else? |
Output length |
|---|---|---|---|
Back (after for) |
Filter — keep or drop items | No | Can be shorter |
Front (before for) |
Choose — pick a value per item | Yes | Same as input |
Set comprehension — curly braces, and duplicates vanish automatically:
unique_letters = {ch for ch in "banana"} # {'b', 'a', 'n'}
"banana" collapses to just b, a, and n because a set can't hold duplicates.
Dict comprehension — curly braces plus a key: value pair:
squares_map = {n: n * n for n in numbers} # {1: 1, 2: 4, 3: 9, ...}
Nested comprehension — chain two loops, read left to right, exactly like stacked for loops. The first loop is the outer (slow) one; the second spins fastest:
grid = [(x, y) for x in range(3) for y in range(3)]
# (0,0), (0,1), (0,2), (1,0), (1,1), ...
Picture a grid: x fixes the row, y slides across the columns. Finish the row, drop to the next x, slide across again. That's why the outer loop is written first — it's the slow hand.
14. Python Code
"""Comprehension patterns: transform, filter, choose, set, dict, and nested."""
def squares(numbers):
"""Return each number multiplied by itself."""
return [n * n for n in numbers]
def evens(numbers):
"""Keep only even numbers (filter: `if` at the back, no else)."""
return [n for n in numbers if n % 2 == 0]
def parity_labels(numbers):
"""Label each number 'even' or 'odd' (choose: if/else at the front)."""
return ["even" if n % 2 == 0 else "odd" for n in numbers]
def unique_chars(text):
"""Collect distinct characters using a set comprehension."""
return {ch for ch in text}
def square_map(numbers):
"""Map each number to its square using a dict comprehension."""
return {n: n * n for n in numbers}
def coordinate_grid(size):
"""Every (x, y) pair; outer loop first, inner loop spins fastest."""
return [(x, y) for x in range(size) for y in range(size)]
if __name__ == "__main__":
assert squares([1, 2, 3, 4, 5]) == [1, 4, 9, 16, 25]
assert evens([1, 2, 3, 4, 5]) == [2, 4]
assert parity_labels([1, 2, 3]) == ["odd", "even", "odd"]
assert unique_chars("banana") == {"b", "a", "n"}
assert square_map([1, 2, 3]) == {1: 1, 2: 4, 3: 9}
assert coordinate_grid(2) == [(0, 0), (0, 1), (1, 0), (1, 1)]
print("All comprehension examples pass.")
15. Code Walkthrough
squares—[n * n for n in numbers]is the pure transform. The expressionn * nis at the front; the loop feeds onenat a time from the back. No filtering.evens— the trailingif n % 2 == 0is a filter.n % 2is the remainder after dividing by two; when it's0, the number is even and survives. Odd numbers are dropped, so the output can be shorter than the input.parity_labels— the leading"even" if n % 2 == 0 else "odd"is a choice, not a filter. Every input produces exactly one label, so theelseis mandatory and the output length always matches the input.unique_chars— swapping[]for{}yields a set, which silently discards duplicate characters. This is the laziest way to dedupe.square_map— addingn:before the value turns braces into a dictionary. The part before the colon is the key, the part after is the value.coordinate_grid— twoforclauses read top-to-bottom as nested loops:xis the outer/slow loop,yis the inner/fast loop.__main__block — eachassertis a tiny, runnable proof that a function behaves. Run the file and it either prints success or blows up on the exact broken line.
16. Dry Run
Let's trace evens([1, 2, 3, 4, 5]) step by step.
| Step | n |
n % 2 |
== 0? |
Action | result so far |
|---|---|---|---|---|---|
| 1 | 1 | 1 | No | drop | [] |
| 2 | 2 | 0 | Yes | keep | [2] |
| 3 | 3 | 1 | No | drop | [2] |
| 4 | 4 | 0 | Yes | keep | [2, 4] |
| 5 | 5 | 1 | No | drop | [2, 4] |
Final result: [2, 4]. The comprehension walked all five numbers, tested each against the filter, and only let the even ones through — exactly like a bouncer checking IDs at the door.
17. Complexity Analysis
Time: O(n). A comprehension visits each of the n input items exactly once, performing constant-time work (a multiply, a modulo, an append handled internally) per item. Adding a filter doesn't change the class — you still inspect every item to decide whether it passes. A nested comprehension over two ranges of size m is O(m²), because you produce every pair.
Space: O(n). You allocate a brand-new list (or set, or dict) holding up to n results. This is the same as the loop version — comprehensions save lines, not memory. If you don't need to store all results at once, a generator expression ((n * n for n in numbers)) drops the space to O(1) by producing items lazily on demand.
Both bounds are tight because the transformation is unavoidable per-item work: you cannot build n results without touching n inputs and storing n outputs.
18. Alternative Solutions
The main alternative is the generator expression — identical syntax, but with round parentheses instead of square brackets:
total = sum(n * n for n in numbers) # no intermediate list built
A list comprehension builds the whole list in memory first; a generator yields one value at a time. When you're feeding the result straight into sum(), max(), any(), or a for loop — and never need the full list — the generator is strictly better on memory.
map and filter are the functional-style alternatives:
squares = list(map(lambda n: n * n, numbers))
evens = list(filter(lambda n: n % 2 == 0, numbers))
| Approach | Readability | Memory | When to prefer |
|---|---|---|---|
| List comprehension | High | O(n) | You need the full list |
| Generator expression | High | O(1) | You'll consume it once |
map / filter |
Medium | Lazy | Passing an existing named function |
Plain for loop |
Highest for complex logic | O(n) | Branching, side effects, debugging |
Most Pythonistas reach for the comprehension or generator; map/filter shine mainly when you already have a named function to pass.
19. Edge Cases
- Empty input:
[n * n for n in []]returns[]. Comprehensions handle empty collections gracefully — the loop simply never runs. - Duplicates in a set comprehension: they're silently collapsed.
{ch for ch in "aaa"}is{'a'}. That's a feature, not a bug — but don't use a set if you need to count occurrences. - Duplicate keys in a dict comprehension: the last value wins.
{k: v for k, v in [("a", 1), ("a", 2)]}gives{"a": 2}. Silent data loss if you didn't expect collisions. - Single-element input: works exactly as expected; nothing special.
- Very large input: a list comprehension materializes everything in memory. For millions of items you'd feel it — switch to a generator expression.
20. Common Mistakes
- Filtering with
ifin front offor.[n for n in numbers if n % 2 == 0]filters;["x" if cond else "y" for n in numbers]chooses. Putting a bare frontifwithoutelseis a syntax error — the front form demands anelse. - Cramming too much logic in. Three loops and two conditions deep, nobody can read it — including you at 3am. If you have to squint, unfold it back into a normal loop.
- Building a list only to loop over it once. Use a generator expression instead and save the memory.
- Expecting a set comprehension to preserve order or duplicates. Sets are unordered and deduplicated by design.
- Reference/scope surprises with nested loops. Getting the outer/inner loop order backward flips your grid. Remember: first
for= outer/slow, secondfor= inner/fast. - Reaching for a comprehension when there are side effects. If the "body" needs to print, log, write a file, or raise — write a loop. Comprehensions are for building values, not doing things.
21. Interview Questions
- What's the difference between a list comprehension and a generator expression? (Eager list in memory vs. lazy one-at-a-time; brackets vs. parentheses; O(n) vs. O(1) space.)
- When would you not use a comprehension? (Complex branching logic, side effects, or when a loop reads more clearly.)
- Explain the difference between a leading
if/elseand a trailingif. (Choose a value vs. filter items.) - Rewrite this nested loop as a comprehension — and then argue whether you should.
- How do you deduplicate a list while transforming it in one line? (Set comprehension.)
- What happens with duplicate keys in a dict comprehension? (Last value wins.)
22. Similar Problems
- LeetCode 1108 — Defanging an IP Address: a pure per-character transform, ideal comprehension practice.
- LeetCode 1929 — Concatenation of Array: building a new list from an existing one, the comprehension bread-and-butter.
- LeetCode 1512 — Number of Good Pairs: naturally expressed with a nested comprehension counting pairs.
- LeetCode 771 — Jewels and Stones: a set comprehension for the jewels plus a filtered count is the clean solution.
- LeetCode 1720 — Decode XORed Array: a one-line transform from a running value, reinforcing "expression first."
They're related because each is fundamentally take a collection, transform and/or filter it, collect the result — the exact shape comprehensions were built for.
23. Key Takeaways
- Write what you want first, then the loop that feeds it.
- An
ifat the back filters (keep or drop, noelse). Anifat the front chooses a value (needselse). - Change the brackets and punctuation:
[]list,{}set,{k: v}dict. - Nest loops left to right — outer loop first, inner loop spins fastest.
- Comprehensions are underrated for small transforms and overrated for everything else. One clean step → comprehension. Messy, branching logic → loop.
24. Watch the Video
Reading about the four-lines-into-one swap is one thing; seeing each piece slide into place makes it click for good. Watch the full walkthrough — including the live filter-vs-choose demo and the nested-grid animation — here: {LINK}
25. About the Series
This is Lesson 13 of Fun with Learning Technology — a hands-on Python series that takes one small, real concept per episode and makes it genuinely stick. No fluff, no memorization drills: just the intuition an experienced engineer actually uses, explained the way you'd want a mentor to explain it. Each lesson builds toward reading and writing Python without hesitation.
26. Call To Action
If this made comprehensions finally click, like the video on YouTube and subscribe so the next lesson finds you. Get the written deep-dives — like this one — by subscribing to the Substack newsletter. Got a case where you're unsure whether to use a comprehension or a loop? Drop it in the comments and I'll weigh in. And if a teammate is still writing four lines where one would do, share this with them.
The solution
# clear one-step transform - comprehension wins
doubled = [n * 2 for n in nums]
# lots of branching - a loop reads better
for n in nums:
...Ready to try it yourself? Solve Python problems with instant feedback in the practice sandbox.
Practice now →


