skip to content

Why is `sum(x for x in nums)` legal but `max(x for x in nums, default=0)` a SyntaxError?

level: middleimportance: nice to knowfreq 25%

answer

  1. A grammar rule, not a runtime one
  2. The call's parentheses can do double duty
  3. Only when it is the sole argument
  4. Keyword arguments count as another argument

basics

~20 s

Python lets a generator expression borrow the call's own parentheses only when it is the sole argument. Add any second argument, positional or keyword, and the parser cannot tell where the expression ends, so it demands explicit parentheses.

solid answer

~40 s

The grammar allows a bare, unparenthesised generator expression in a call **only** when it is that call's single argument - `sum(x * x for x in nums)` reads unambiguously because the call's parentheses delimit it. The moment another argument appears, a comma could belong either to the argument list or to the expression, so CPython rejects it at compile time with a SyntaxError saying the generator expression must be parenthesized. A keyword argument counts: `max(x for x in nums, default=0)` fails exactly like a second positional one would. The fix is to give the expression its own parentheses - `max((x for x in nums), default=0)`. Square brackets never have this problem, because a list comprehension is self-delimiting, so `sorted([x for x in nums], reverse=True)` is always fine.

code

python · 5 lines
python
nums = [3, 1, 4, 1, 5]

print(sum(x * x for x in nums))            # bare form: sole argument
print(max((x for x in nums), default=0))   # parentheses required
print(sorted((x for x in nums), reverse=True))

go deeper

for a junior

Learn the shorthand and the safe habit: sum(x for x in nums) is fine because the expression is the only argument, and adding your own parentheses is never wrong when a call takes anything else too.

for a middle

Explain why the grammar forbids it - a comma would be ambiguous with no closing token to end the expression - and that a keyword argument counts. Show the fix and note that square brackets are self-delimiting.

for a senior

Move past the syntax to the choice: pass the lazy form for large sources or early-exit consumers, materialize for small ones, and know str.join() builds a list internally so the eager form costs nothing there.

for a principal

Set a house style. Either always parenthesize or reserve the bare form for single-argument aggregates, and make it a lint rule rather than a review comment, so the compile-time failure never reaches a branch that others depend on.

### What the grammar actually permits When PEP 289 introduced generator expressions in Python 2.4, it also introduced a small syntactic convenience: if the expression is the only argument to a call, you may leave off its parentheses and let the call's own parentheses do the delimiting. So `sum(x * x for x in nums)` is shorthand for `sum((x * x for x in nums))`, and the shorthand is the idiomatic form - it is the reason the aggregate builtins read so cleanly over a lazy source. The permission is narrow on purpose. In `f(x for x in nums, key)` a reader - and a parser - cannot tell whether the comma separates two arguments or belongs inside the expression, and generator expressions have no closing token of their own to resolve it. Rather than invent a disambiguation rule, the language simply forbids the bare form whenever the call has more than one argument. CPython reports this at compile time, not at run time: it is a `SyntaxError` whose message tells you the generator expression must be parenthesized, and it fires before any of your code executes. ### The rule in practice A keyword argument is still an argument. `max(x for x in nums, default=0)`, `sorted(x for x in nums, reverse=True)` and `str.join` style calls with extra parameters all fail the same way. A single argument that happens to be followed by a trailing comma is also not the sole-argument case any more. And the rule is about the *call site*, not about the function: nothing about `max()` is special, only the shape of its argument list. The fix is mechanical - wrap the expression: `max((x for x in nums), default=0)`. Many teams simply always parenthesize, which is never wrong and avoids the question entirely; others prefer the bare form for the single-argument aggregates because it reads better. Either convention is defensible; mixing them without noticing is what produces the surprised SyntaxError. Square brackets do not have the problem. `[x for x in nums]` is a list comprehension, self-delimiting by its own brackets, so it can appear anywhere in an argument list next to anything else. That asymmetry is often the first hint people get that the parentheses in the lazy form are part of the *expression*, not decoration around it - and it is why `(x for x in nums)` needs those parentheses when it appears outside a call at all, for instance on the right of an assignment. ### The related question interviewers really want Once the syntax is settled, the interesting half is when to pass the lazy form at all. For a large source, `sum(x * x for x in nums)` avoids allocating a whole intermediate list, which is real memory and real allocator work. For a small source, the list comprehension is often marginally *faster*, because iterating a list costs less per element than resuming a generator frame each time, and since Python 3.12 (PEP 709) list comprehensions no longer pay the function-call overhead they used to. The generator's advantage is memory and early exit, not raw per-element speed. There is one well-known case where the list form is the better default: `str.join()`. It needs the total length before it can allocate the result string, so when handed an arbitrary iterator it materializes it into a list internally anyway. Passing a generator expression there buys no memory saving - the list gets built regardless - and adds a layer of generator machinery, so `"".join([str(n) for n in nums])` is usually as fast or faster than the lazy form. Knowing that specific exception is a good signal that a candidate has actually measured rather than absorbed a slogan. ### Two error shapes not to confuse This is a compile-time `SyntaxError`, so it is caught when the module is imported, not when the line runs; you will see it in a linter and in a failed import, never as a runtime failure deep in a request. That distinguishes it from the *other* common generator-in-a-call mistake, which is passing a generator object where a sequence is required - that one is a perfectly valid parse and fails later with a `TypeError` about the object not supporting length or indexing, or worse, does not fail at all and quietly consumes the stream.

  • Is `sum(x for x in nums)` faster than `sum([x for x in nums])`?
    Not per element. The list version avoids resuming a generator frame for every item and, since 3.12, avoids comprehension call overhead too, so on small inputs it often edges ahead. The generator expression wins on memory for large sources and on wasted work when the consumer can stop early - which `sum()` never does.
  • Why is a list comprehension usually preferred inside `str.join()`?
    `str.join()` needs the total length before allocating the result, so it materializes any iterator into a list internally. Passing a generator expression therefore saves no memory and adds a layer of machinery, making the list comprehension typically as fast or faster. It is the standard counterexample to 'always pass a generator'.
  • Does the same delimiting rule apply to a list comprehension in a call?
    No. Square brackets close the comprehension themselves, so `sorted([x for x in nums], reverse=True)` parses fine anywhere in an argument list. Only the parenthesised lazy form can borrow the call's parentheses, and only when it is alone.

saying these in an interview costs you the question

  • Claims the parentheses may be dropped anywhere in a call
  • Says a keyword argument does not count as a second argument
  • Reads the failure as a runtime TypeError, not a SyntaxError
  • Thinks a list comprehension needs the same delimiting rule
  • Believes the bare form builds a tuple before the call

context