We close the module with Python's most characteristic construct. In the previous lessons you'll have noticed a recurring pattern: creating a new list by traversing another one — the catalog prices with VAT, the uppercase titles for the labels, the books still in stock. That three-or-four-line pattern (create an empty list, traverse, transform or filter, append) is so frequent that Python dedicates a syntax of its own to it: list comprehensions, which condense it into a single readable line. Mastering when to use them — and when not to — is one of the hallmarks of "Pythonic" code.
Contents
- The pattern we're going to compress
- Basic syntax:
[expression for element in iterable] - Filtering with
ifat the end - Transforming with
if/elsein the expression - Nested comprehensions (in moderation)
- Comprehension vs. loop: a head-to-head comparison
- When NOT to use a comprehension
- A glimpse of dictionary and set comprehensions
The pattern we're going to compress
Ana wants the list of catalog prices with book VAT included. With what you know from the loops lesson, you'd do it like this:
BOOK_VAT = 0.04
prices = [12.50, 9.95, 15.90]
prices_with_vat = [] # 1. empty list to start with
for price in prices: # 2. traverse the original
prices_with_vat.append(price * (1 + BOOK_VAT)) # 3. transform and append
print(prices_with_vat)Output:
Here append() makes its appearance, the method that adds an element to the end of a list — we'll study it in detail in module 4; for now that idea is enough. The full pattern always has the same three pieces: empty list → loop → append of a transformation. Python lets you express exactly that in one line.
Basic syntax: [expression for element in iterable]
The same list of prices with VAT, as a comprehension:
BOOK_VAT = 0.04
prices = [12.50, 9.95, 15.90]
prices_with_vat = [price * (1 + BOOK_VAT) for price in prices]
print(prices_with_vat) # [13.0, 10.348, 16.536]Anatomy of the line, from the inside out:
| Piece | In the example | Role |
|---|---|---|
for element in iterable |
for price in prices |
The same loop header you already know |
expression |
price * (1 + BOOK_VAT) |
What gets stored in the new list for each element |
[ ... ] |
the square brackets | They say "build a list out of all this" |
It reads almost naturally: "a list of price * (1 + VAT) for each price in prices". The writing order is surprising at first (the expression goes before the for), but it reflects what you care about: first what you get, then where it comes from.
More quick examples with Papyrus material:
titles = ["The Odyssey", "Hamlet", "Don Quixote"]
# Uppercase titles for the shelf labels
labels = [title.upper() for title in titles]
# ['THE ODYSSEY', 'HAMLET', 'DON QUIXOTE']
# Length of each title (to work out the width of the menu table)
lengths = [len(title) for title in titles]
# [11, 6, 11]
# The first 5 receipt numbers of the day
receipts = [f"T-{n:03d}" for n in range(1, 6)]
# ['T-001', 'T-002', 'T-003', 'T-004', 'T-005'](upper() converts a string to uppercase; like lower() in the previous lesson, it's a str method we'll cover in depth in module 4.) Look at the third example: the iterable can be a range, a string, another list... anything you can traverse with for. And the expression can be as simple as the element itself ([x for x in data], which copies the list) or as elaborate as an f-string.
Filtering with if at the end
Second piece: sometimes you don't want to transform all the elements, but rather keep only some of them. Add an if after the for:
Only the elements that meet the condition make it into the new list. The Papyrus classic: which books are in stock?
titles = ["The Odyssey", "Hamlet", "Don Quixote", "Faust"]
stocks = [4, 6, 8, 0]
available = [titles[i] for i in range(len(titles)) if stocks[i] > 0]
print(available) # ['The Odyssey', 'Hamlet', 'Don Quixote']And combining transformation + filter in a single comprehension — prices with VAT, but only for the books that cost less than 15 EUR:
BOOK_VAT = 0.04
prices = [12.50, 9.95, 15.90]
cheap_with_vat = [p * (1 + BOOK_VAT) for p in prices if p < 15]
print(cheap_with_vat) # [13.0, 10.348]It reads: "the price with VAT, for each p in prices, if p < 15". The filter is applied first (does this element get in?) and the expression afterwards (what do I keep from it?). Don Quixote (15.90) doesn't pass the filter, so its VAT isn't even calculated.
In the if you can use everything from lesson 02-01: compound conditions (if p < 15 and p > 10), truthiness (if title to discard empty strings), not...
answers = ["Hamlet", "", "Faust", ""]
# Clean empty entries out of an order form
valid_orders = [a for a in answers if a]
print(valid_orders) # ['Hamlet', 'Faust']Transforming with if/else in the expression
Careful, this case gets confused with the previous one a lot. The if at the end filters (some elements don't get in). But if you want all the elements to get in, choosing between two values depending on a condition, what you need is the ternary operator from lesson 02-01, placed in the expression:
Example: the Papyrus menu listing must show all the titles, flagging the ones out of stock.
titles = ["The Odyssey", "Hamlet", "Don Quixote", "Faust"]
stocks = [4, 6, 8, 0]
listing = [
titles[i] if stocks[i] > 0 else f"{titles[i]} (OUT OF STOCK)"
for i in range(len(titles))
]
print(listing)
# ['The Odyssey', 'Hamlet', 'Don Quixote', 'Faust (OUT OF STOCK)']The two positions of the if, face to face:
| Form | Position of the if |
else? |
Effect | Size of the resulting list |
|---|---|---|---|---|
[expr for x in data if cond] |
After the for |
Not allowed | Filters elements | ≤ the original |
[a if cond else b for x in data] |
In the expression (ternary) | Mandatory | Chooses each element's value | Same as the original |
Mnemonic: if without else at the end = doorman (decides who gets in); if with else at the front = transformer (decides what each one turns into). And yes, both can be combined in the same comprehension, although readability starts to suffer:
# Member discount for the cheap ones, VAT for all, and the out-of-stock ones out
MEMBER_DISCOUNT = 0.05
prices = [12.50, 9.95, 15.90, 21.00]
stocks = [4, 6, 8, 0]
final_prices = [
round(p * (1 - MEMBER_DISCOUNT), 2) if p < 15 else round(p, 2)
for i, p in enumerate(prices)
if stocks[i] > 0
]
print(final_prices) # [11.88, 9.45, 15.9]Notice too that a long comprehension can (and should) be split across several lines, aligning the for and the if — it's still a single expression between brackets. And that enumerate() works inside comprehensions just like in ordinary loops.
Nested comprehensions (in moderation)
Just as loops nest, a comprehension allows several fors in a row. It's equivalent to nested loops read left to right (the first for is the outer loop):
# All the format-language combinations Papyrus can order from the distributor
formats = ["hardcover", "paperback"]
languages = ["es", "en"]
combinations = [f"{fmt} ({language})" for fmt in formats for language in languages]
print(combinations)
# ['hardcover (es)', 'hardcover (en)', 'paperback (es)', 'paperback (en)']Its loop equivalent, so you can see the exact correspondence:
combinations = []
for fmt in formats: # first for of the comprehension
for language in languages: # second for of the comprehension
combinations.append(f"{fmt} ({language})")Another use of nesting is a comprehension inside another, for example to generate a tabular structure (a list of lists):
# Location grid: 2 aisles x 3 shelves
locations = [[f"A{a}-S{s}" for s in range(1, 4)] for a in range(1, 3)]
print(locations)
# [['A1-S1', 'A1-S2', 'A1-S3'], ['A2-S1', 'A2-S2', 'A2-S3']]Honest warning: two fors in a comprehension is the reasonable limit. Three or more, or nesting with filters and ternaries at the same time, produces lines that nobody (not even you a month from now) deciphers at first glance. In those cases, the classic nested loops from lesson 02-02 are the better tool. Compressing isn't the goal; communicating is.
Comprehension vs. loop: a head-to-head comparison
Let's put the two versions side by side with the complete Papyrus example — final prices for Luis (a member) of the available books:
MEMBER_DISCOUNT = 0.05
BOOK_VAT = 0.04
prices = [12.50, 9.95, 15.90, 21.00]
stocks = [4, 6, 8, 0]
# ---- Loop version: 5 lines ----
luis_prices = []
for i, p in enumerate(prices):
if stocks[i] > 0:
final = p * (1 - MEMBER_DISCOUNT) * (1 + BOOK_VAT)
luis_prices.append(round(final, 2))
# ---- Comprehension version: 1 expression ----
luis_prices = [
round(p * (1 - MEMBER_DISCOUNT) * (1 + BOOK_VAT), 2)
for i, p in enumerate(prices)
if stocks[i] > 0
]
print(luis_prices) # [12.35, 9.83, 15.71] in both casesWhat does each version win?
| Criterion | Classic for loop |
Comprehension |
|---|---|---|
| Lines of code | More | Fewer |
| Reads like | A step-by-step recipe | A declaration: "I want this list" |
| Intermediate steps (debug prints, several operations) | Easy to add | Don't fit |
break/continue |
Available | Don't exist inside a comprehension |
| Side effects (printing, modifying other variables) | Natural | Contraindicated |
| Speed | Good | Slightly higher (the append is optimized internally) |
The comprehension's real advantage isn't saving keystrokes: it's declaring intent. [t.upper() for t in titles] says "a list of the titles in uppercase" at a glance; the equivalent loop forces you to read three lines to deduce the same thing. That's why, for the pattern transform/filter a collection into a new list, the comprehension is the preferred form in the Python community.
When NOT to use a comprehension
Knowing how to hold back is as important as knowing how to use them. Signs that it's time to go back to the classic loop:
- You're not building a list. If the goal is to print, accumulate a total or ask for
input(), use a loop. Writing[print(t) for t in titles]works, but it creates a useless list ofNoneand confuses the reader: comprehensions are for producing data, not for doing things. - The line doesn't fit or can't be understood. If you need more than two or three pieces (two
fors, a filter and a ternary, for instance) or you clearly exceed PEP 8's 79-99 characters even when splitting it, a loop with intermediate names will be clearer. - You need to stop halfway or skip elements with complex logic. There's no
breakorcontinuein a comprehension; if you miss them, it's a loop. - There are several dependent steps. Computing a value, checking it, adjusting it and then storing it are several statements; cramming them into one expression usually requires unreadable tricks.
- You're debugging. A loop admits intermediate
print()s to inspect values; a comprehension doesn't. Sometimes it pays to temporarily "unfold" a comprehension into a loop to investigate a problem and fold it back afterwards.
Final rule: write the comprehension, reread it as if someone else were seeing it and, if you have to think for more than a couple of seconds to understand it, turn it into a loop. Nobody has ever been reproached for a clear loop.
A glimpse of dictionary and set comprehensions
The same idea works with other Python data structures you'll study in module 4. Just as an appetizer:
titles = ["The Odyssey", "Hamlet", "Don Quixote"]
prices = [12.50, 9.95, 15.90]
# DICTIONARY comprehension (braces and a colon): title -> price
price_list = {titles[i]: prices[i] for i in range(len(titles))}
# {'The Odyssey': 12.5, 'Hamlet': 9.95, 'Don Quixote': 15.9}
# SET comprehension (braces without a colon): unique initials
initials = {title[0] for title in titles}
# {'T', 'H', 'D'}Dictionaries (key-value pairs) and sets (collections without duplicates) deserve their own lessons; when we get to them in module 4, their comprehensions will feel familiar: they're exactly this syntax with {} instead of []. For now, take away that the pattern you've learned today is general.
Common Mistakes and Tips
- Confusing the filter-
ifwith the ternaryif/else:[x for x in data if cond]filters;[a if cond else b for x in data]transforms. Writing[x for x in data if cond else y]is aSyntaxError: the trailingifdoesn't allow anelse. - Using comprehensions for their side effects (
[print(x) for x in ...]): technically valid, stylistically wrong. If you're not going to use the resulting list, it's a loop. - Shadowing variables: the comprehension's variable is local to it, but if you name it the same as another variable in the program (
price = 100and then[price for price in prices]), the code becomes confusing. Use distinct, descriptive names. - Over-nesting: two
fors is the comfortable maximum. When in doubt, unfold into loops. - Forgetting it produces a new list: the comprehension doesn't modify the original list;
pricesremains intact after[p * 2 for p in prices]. If you want to keep the result, assign it to a variable. - Tip: when a comprehension gets long, split it across several lines inside the brackets (expression /
for/if, one piece per line). Python allows it and readability improves enormously. - Tip: when reading someone else's comprehension, start with the
for(what does it traverse?), continue with theif(what does it let through?) and finish with the expression (what does it build?). That's the order in which it executes.
Exercises
Exercise 1: catalog RRP
Given prices = [12.50, 9.95, 15.90] and the constant BOOK_VAT = 0.04, use a comprehension to create the list rrp (the retail price) with each price including VAT, rounded to 2 decimals (use round(value, 2)). Then, with another comprehension, create member_rrp additionally applying MEMBER_DISCOUNT = 0.05 to the base price, only for base prices below 15 EUR (the rest must not appear in the list).
Exercise 2: window display labels
Given titles = ["The Odyssey", "Hamlet", "Don Quixote", "Faust"] and stocks = [4, 6, 8, 0], create with a single comprehension the list of window display labels: all the titles in uppercase, and the out-of-stock ones with the suffix " - COMING SOON". Expected result: ['THE ODYSSEY', 'HAMLET', 'DON QUIXOTE', 'FAUST - COMING SOON'].
Exercise 3: from comprehension to loop (and judgement)
This comprehension works, but it's hard to read:
(a) Rewrite it as a classic for loop with descriptive variable names. (b) Explain in one sentence why the loop is preferable in this case. Note: zip() pairs two lists element by element — it's studied in module 3; for this exercise it's enough to know that for t, s in zip(titles, stocks) traverses title and stock at the same time.
Solutions
Solution 1:
BOOK_VAT = 0.04
MEMBER_DISCOUNT = 0.05
prices = [12.50, 9.95, 15.90]
rrp = [round(p * (1 + BOOK_VAT), 2) for p in prices]
print(rrp) # [13.0, 10.35, 16.54]
member_rrp = [
round(p * (1 - MEMBER_DISCOUNT) * (1 + BOOK_VAT), 2)
for p in prices
if p < 15
]
print(member_rrp) # [12.35, 9.83]The first is a pure transformation (same length as the original). The second combines a filter (if p < 15, which discards Don Quixote) and a transformation (discount on the base and then VAT, the same calculation order we used in lesson 02-01).
Solution 2:
titles = ["The Odyssey", "Hamlet", "Don Quixote", "Faust"]
stocks = [4, 6, 8, 0]
labels = [
titles[i].upper() if stocks[i] > 0 else titles[i].upper() + " - COMING SOON"
for i in range(len(titles))
]
print(labels)
# ['THE ODYSSEY', 'HAMLET', 'DON QUIXOTE', 'FAUST - COMING SOON']Here the if/else goes in the expression (ternary) because all the titles must appear: we're not filtering, we're choosing each one's format. An even cleaner version, applying the suffix with the ternary: [titles[i].upper() + ("" if stocks[i] > 0 else " - COMING SOON") for i in range(len(titles))].
Solution 3:
titles = ["The Odyssey", "Hamlet", "Don Quixote", "Faust"]
stocks = [4, 6, 8, 0]
result = []
for title, stock in zip(titles, stocks):
if len(title) > 6 and stock != 1:
if stock > 3:
result.append(title.upper())
else:
result.append(title.lower())
print(result) # ['THE ODYSSEY', 'DON QUIXOTE'](b) The loop is preferable because the original comprehension piles four pieces (ternary + for + zip + compound filter) into one line: the reader has to work out what it filters and what it transforms all at once, whereas the loop separates each decision onto its own line with clear names. It's exactly the "when NOT to use a comprehension" criterion: if it takes more than a couple of seconds to understand, unfold it.
Conclusion
With list comprehensions you've come full circle in this module: [expression for x in iterable if condition] gathers into one declarative line what you learned separately — the transformation of a for loop, the filter of an if and the choice of a ternary. You know how to filter (if at the end), transform (if/else in the expression), nest with caution and, above all, recognize when a classic loop communicates better. You've also seen that the same syntax extends to dictionaries and sets, a preview of module 4.
Module 2 ends here, and Papyrus can feel it: its scripts now decide (if/elif/else), repeat (for, while), search and stop (break, loop else), converse through menu.py (while True + match) and generate price lists and labels in a single line. But all that code lives loose, repeated from script to script: the menu's search, the member price calculation, input validation... Every improvement means touching several places. The solution is to package each task under a name of its own and reuse it wherever needed: that's what functions are, the stars of module 3, where we'll reorganize the Papyrus menu piece by piece and you'll discover you've been using functions (print, len, enumerate...) since day one.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
