Here is the answer that saves you an hour: Python has no tuple comprehension. Writing (x * 2 for x in numbers) looks like it should produce a tuple, but it produces a generator. To get a tuple, wrap a comprehension in the tuple() function. This guide explains why the language works that way, and why the generator you accidentally created is often what you wanted all along.
What the parentheses actually do#
numbers = [1, 2, 3, 4]
squares_list = [n ** 2 for n in numbers] # list comprehension
squares_set = {n ** 2 for n in numbers} # set comprehension
squares_dict = {n: n ** 2 for n in numbers} # dict comprehension
squares_gen = (n ** 2 for n in numbers) # generator expression
print(type(squares_list)) # <class 'list'>
print(type(squares_set)) # <class 'set'>
print(type(squares_dict)) # <class 'dict'>
print(type(squares_gen)) # <class 'generator'> <- not a tuple
Square brackets and curly braces were free to be used for comprehensions. Parentheses were not — they were already doing several jobs (grouping expressions, calling functions, defining tuples), and Python’s designers gave them to generator expressions instead, because a lazy sequence is more useful more often than an immutable one.
How to actually build a tuple#
numbers = [1, 2, 3, 4]
squares = tuple(n ** 2 for n in numbers)
print(squares) # (1, 4, 9, 16)
print(type(squares)) # <class 'tuple'>
Note there is only one set of brackets. tuple(...) is a function call, and the generator expression sits inside it as the argument. You do not need the extra parentheses:
tuple(n ** 2 for n in numbers) # fine
tuple((n ** 2 for n in numbers)) # also fine, but redundant
tuple([n ** 2 for n in numbers]) # works, builds a throwaway list first
The third version is slightly wasteful: it constructs a complete list in memory, then copies it into a tuple, then throws the list away. Prefer the first.
Filtering and conditionals work the same#
numbers = range(1, 11)
evens = tuple(n for n in numbers if n % 2 == 0)
print(evens) # (2, 4, 6, 8, 10)
labels = tuple("even" if n % 2 == 0 else "odd" for n in range(1, 6))
print(labels) # ('odd', 'even', 'odd', 'even', 'odd')
The if at the end filters; the if/else at the front chooses a value. That distinction confuses people in every comprehension, not just this one.
The bug this causes#
Because a generator is not a tuple, code that assumed a tuple starts behaving strangely:
gen = (n ** 2 for n in [1, 2, 3])
print(len(gen))
TypeError: object of type 'generator' has no len()
Worse is the version that does not raise anything:
gen = (n ** 2 for n in [1, 2, 3])
print(sum(gen)) # 14
print(sum(gen)) # 0 <- not a bug in sum
print(list(gen)) # []
A generator produces its values once. After the first sum walked through it, there is nothing left. No error, just a zero, which is exactly the kind of bug that survives code review.
When you want the generator instead#
Generators are lazy: they compute each value only when asked. That makes them the right choice for large or infinite sequences, and for feeding functions that consume values one at a time.
import sys
as_tuple = tuple(n for n in range(1_000_000))
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_tuple)) # roughly 8 MB
print(sys.getsizeof(as_gen)) # around 200 bytes
The generator has not computed anything yet. It holds only the instructions. That is why this pattern is efficient:
# reads one line at a time, never loads the whole file
with open("big.log") as f:
error_count = sum(1 for line in f if "ERROR" in line)
Wrapping that in tuple() would pull the entire file into memory to count it, which defeats the point.
Tuple or list?#
Once you have decided you want a concrete sequence rather than a generator, the tuple-versus-list question is about intent:
| Tuple | List | |
|---|---|---|
| Can be changed after creation | No | Yes |
| Usable as a dictionary key | Yes | No |
| Usable in a set | Yes | No |
| Slightly smaller and faster | Yes | — |
| Signals “fixed record” | Yes | — |
The dictionary-key point is the practical one. This is legal because the coordinates are tuples:
grid = {}
grid[(0, 0)] = "start"
grid[(3, 4)] = "treasure"
corners = tuple((x, y) for x in (0, 9) for y in (0, 9))
print(corners) # ((0, 0), (0, 9), (9, 0), (9, 9))
Questions people ask#
Why does Python not have a tuple comprehension?
Parentheses were already taken by generator expressions, which arrived later than list comprehensions and were judged more broadly useful. Since tuple(...) converts a generator in one short call, adding dedicated syntax would buy very little.
Is tuple(generator) slower than a list comprehension?
Marginally, because of the extra function call, but the difference is small enough to ignore in almost all code. Choose based on whether you want immutability, not on micro-benchmarks.
Can I use a generator expression as a function argument without extra brackets?
Yes, when it is the only argument: sum(n for n in nums) is valid. With more than one argument you need the parentheses: max((n for n in nums), default=0).
How do I check whether something is a generator?
Use import types then isinstance(obj, types.GeneratorType). In everyday code, calling type(obj) in a quick print is usually enough to settle it.
Where to go next#
- Python lists explained — including list comprehensions in full.
- Python dictionaries explained — where tuple keys come in.
- Python for loops explained — the loop every comprehension is shorthand for.