Tuples and comprehensions: the Pythonic idioms
You can already write small programs. This is the lesson where your code starts to look like the code professionals write. Everything here does jobs you could already do the long way with a loop — but Python gives you tighter, clearer tools for the most common jobs, and once you can read them you can read almost any real codebase. We will cover four of them: tuples, list comprehensions, dict comprehensions, and the enumerate/zip pair. None of these are exotic. They are the everyday grammar of Python.
Tuples: a list that can't be changed
A tuple is an ordered collection of values, exactly like a list — except a tuple is immutable, meaning it cannot be changed after you make it. You write a tuple with parentheses (or even just commas) instead of square brackets:
point = (3, 4) # a tuple of two numbers
print(point[0]) # 3 — index it exactly like a list
print(point[1]) # 4
print(len(point)) # 2
So far it behaves just like a list. The difference shows up the moment you try to change it:
nums = [1, 2, 3] # a LIST (square brackets)
nums[0] = 99 # fine — lists are mutable
print(nums) # [99, 2, 3]
pair = (1, 2, 3) # a TUPLE (parentheses)
pair[0] = 99 # TypeError: 'tuple' object does not support item assignment
:::note Mutable vs. immutable
Mutable means "changeable in place" — a list lets you reassign an element, append, or remove. Immutable means "fixed once created" — a tuple, like a string, does not. If you need a different tuple, you build a brand-new one. This is the same property strings have: "cat"[0] = "h" fails for the same reason (1,2)[0] = 9 does.
:::
Why would you ever want a collection you can't change? Three honest reasons:
- It signals intent. When a function returns
(latitude, longitude), the tuple says "these two belong together as one fixed thing." A reader knows nothing will be appended or reordered. - It is safer. A value that can't change can't be changed by accident in some far-off corner of your program. Bugs from "something quietly mutated my data" simply can't happen.
- It can be a dictionary key. Remember from the dictionaries lesson that keys must be immutable. A tuple
(3, 4)can be a key; a list[3, 4]cannot.
A tiny gotcha worth memorizing: a one-element tuple needs a trailing comma. The comma — not the parentheses — is what makes a tuple.
not_a_tuple = (5) # just the number 5 in parentheses
real_tuple = (5,) # a tuple holding one value
print(type(not_a_tuple)) # <class 'int'>
print(type(real_tuple)) # <class 'tuple'>
Unpacking: pulling a tuple apart in one line
The feature that makes tuples a joy is unpacking: assigning several names at once from one tuple, by position.
point = (3, 4)
x, y = point # x gets 3, y gets 4 — unpacked by position
print(x) # 3
print(y) # 4
This is everywhere in real Python. The classic example is the one-line variable swap — no temporary variable needed:
a = 1
b = 2
a, b = b, a # build the tuple (b, a) on the right, unpack into a, b
print(a, b) # 2 1
And a function can return several values at once by returning a tuple — the caller unpacks them:
def min_and_max(nums):
return min(nums), max(nums) # returns the tuple (smallest, largest)
low, high = min_and_max([4, 1, 7, 3])
print(low) # 1
print(high) # 7
That "return several things, unpack them" pattern is how a huge amount of Python is structured. You will see it constantly in the next guides.
List comprehensions: build a list in one expression
Here is a job you have done many times: start with an empty list, loop over something, and append a transformed value each time.
# the long way
squares = []
for n in range(5):
squares.append(n * n)
print(squares) # [0, 1, 4, 9, 16]
A list comprehension does exactly that in a single, readable line. The shape is [EXPRESSION for ITEM in ITERABLE] — read it left to right as "give me EXPRESSION, for each ITEM in ITERABLE":
squares = [n * n for n in range(5)]
print(squares) # [0, 1, 4, 9, 16] — identical result, one line
Both versions produce the same list. The comprehension just packs the "make an empty list, loop, append" ritual into one expression. Read it as a sentence: "n times n, for each n in range 5."
You can add a filter with an if at the end — only items that pass the condition make it in:
evens = [n for n in range(10) if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8] — kept only the even numbers
The expression can be anything, including a method call:
words = ["hi", "there", "friend"]
shouted = [w.upper() for w in words]
print(shouted) # ['HI', 'THERE', 'FRIEND']
lengths = [len(w) for w in words]
print(lengths) # [2, 5, 6]
:::tip How to read any comprehension
Find the for first — that tells you what you're looping over. The part before for is what each item becomes. The part after if (if present) decides which items survive. So [w.upper() for w in words if len(w) > 2] reads: "loop words, keep the ones longer than 2 letters, and turn each into uppercase."
:::
:::warning Don't over-pack
Comprehensions shine for "transform and/or filter a collection." If you find yourself stuffing nested loops and multiple conditions into one, a plain for loop is clearer — and clarity wins. The goal is readable code, not the shortest possible code.
:::
Dict comprehensions: the same idea for dictionaries
The same compact syntax builds a dictionary. The only change is curly braces and a key: value pair before the for:
nums = [1, 2, 3, 4]
squares = {n: n * n for n in nums}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16}
Read it: "for each n, make an entry whose key is n and whose value is n * n." Filters work here too:
prices = {"pen": 2, "book": 12, "pencil": 1}
cheap = {name: p for name, p in prices.items() if p < 5}
print(cheap) # {'pen': 2, 'pencil': 1}
Notice prices.items() hands back each key/value as a tuple, which we unpack into name, p right in the for — tuples and comprehensions working together.
enumerate and zip: looping, the Pythonic way
Two tiny built-ins clean up loops you write all the time.
enumerate gives you the index and the item as you loop — so you never have to manage a counter by hand:
colors = ["red", "green", "blue"]
# the clumsy way
for i in range(len(colors)):
print(i, colors[i])
# the Pythonic way — enumerate hands you (index, item) each time
for i, color in enumerate(colors):
print(i, color)
# 0 red
# 1 green
# 2 blue
enumerate(colors) produces (0, "red"), (1, "green"), (2, "blue") — tuples again, unpacked into i, color. You can even pick a starting number: enumerate(colors, start=1) begins at 1.
zip walks two (or more) collections in lockstep, handing you one item from each, paired up:
names = ["Ada", "Lin", "Sam"]
scores = [90, 75, 88]
for name, score in zip(names, scores):
print(name, "scored", score)
# Ada scored 90
# Lin scored 75
# Sam scored 88
zip stops at the shortest collection, so mismatched lengths won't crash — the extras are simply ignored. A common use is building a dict from two parallel lists:
report = {name: score for name, score in zip(names, scores)}
print(report) # {'Ada': 90, 'Lin': 75, 'Sam': 88}
That single line — dict comprehension + zip + tuple unpacking — is about as Pythonic as code gets, and you just read it fluently.
Why it matters
These four idioms are not advanced tricks; they are the default way experienced Pythonistas write. The next guides — web, AI, security — are full of them: [item["id"] for item in response] to pull fields out of API data, for i, row in enumerate(rows) to number output, dict(zip(headers, values)) to assemble records. You don't need to prefer them yet, but you must be able to read them without slowing down. Now you can.
Where this leads: comprehensions and unpacking are how you'll reshape the structured data that every API and library hands back. See Where this leads next.
Practice
Write a comprehension-powered function and run it live:
Write even_squares(nums) that returns a new list containing the square of every EVEN number in nums (drop the odd ones). Use a list comprehension with a filter. An empty list (or one with no evens) returns [].
Checkpoint
Tuples and comprehensions
Pass to unlock the Next button belowNext: Modules & imports →