Why is a comprehension faster than the same loop?

Yes, by a little, and for a specific reason: the loop looks up out.append and calls it on every iteration, while the comprehension appends with a single dedicated opcode, LIST_APPEND. It is a constant factor, not a change in complexity — both are O(n).

Overview

The question, and what it is testing

Yes, by a little, and for a specific reason: the loop looks up out.append and calls it on every iteration, while the comprehension appends with a single dedicated opcode, LIST_APPEND. It is a constant factor, not a change in complexity — both are O(n).

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

Python semanticsConceptualEasy

Step through it

What to watch

  • The loop's per-iteration bytecode contains LOAD_ATTR and CALL; the comprehension's does not.
  • Both forms are O(n) — the difference is per-item overhead.
  • The measured gap is modest, which is the honest answer to the question.

Say this out loud

"A bit faster. The loop does an attribute lookup and a function call per item; the comprehension uses a dedicated LIST_APPEND opcode with neither. It is a constant factor, so I would choose between them on readability, and reach for a generator expression when I do not need the list in memory - which saves the memory, not the time."

Why is a comprehension faster than the same loop?

Is a list comprehension faster than an equivalent for loop, and if so why?

Run it

Best-of-five timings, because these are noisy, and then the per-iteration bytecode of each form so the cause is visible rather than asserted.

Best of five runs, because a single measurement here is a coin flip.

1Python
Output

And the reason, which is visible rather than asserted.

2Python
Output

And the version that stores nothing - note what it does and does not buy.

3Python
Output

The difference, in bytecode

out.append(x) is three separate things: find the attribute append on the list, build a call, and make it. That happens once per item. The comprehension compiles the append into LIST_APPEND, a single instruction that pushes the value onto the list being built — no name to resolve and no call to make.

The explorer above is those two instruction sequences, taken from dis rather than described, so you can count the difference. It is a handful of opcodes per item.

Why the number is smaller than people expect

Folklore puts comprehensions at two or three times faster, and that has not been true for a while. Two things narrowed it.

The specialising interpreter added in 3.11 rewrites hot instruction sequences in place, and a repeated attribute-lookup-then-call on the same type is exactly the pattern it specialises, so the loop's overhead shrank.

And in 3.12 comprehensions were inlined (PEP 709). Before that, a list comprehension created and called a hidden function object on every evaluation — real overhead that partly cancelled the LIST_APPEND saving. Removing it made comprehensions faster in a way that has nothing to do with the append path.

So the answer to give is "a bit faster, because of a per-iteration lookup and call", not a multiplier. The editor below measures it on whichever interpreter you are running.

When the loop is the right answer anyway

A comprehension has to be one expression. The moment the body needs a statement — a try, a break, a log line, two things per item — the loop is the only option, and forcing a comprehension produces the unreadable nested version everybody has met.

The real decision is readability at a glance. One transformation and one filter reads better as a comprehension; three levels of nesting with two conditions does not, whatever it costs. And if the result is only going to be consumed once, the better answer is often neither: a generator expression skips building the list at all. That is a memory win rather than a speed one — the editor below measures the two as roughly equal in time — but constant memory against a list of a million items is a larger practical difference than the constant factor this page is about.

The follow-up: what about map and filter

map(f, xs) can beat a comprehension when f is already a function, because it avoids a Python-level call per item by doing the loop in C — but map(lambda x: x * x, xs) is usually slower than the comprehension, because the lambda reintroduces exactly the per-item Python call you were trying to avoid. So map(str, xs) is a reasonable choice and map with a lambda rarely is.

What to say out loud

A bit faster. The loop does an attribute lookup and a function call per item; the comprehension uses a dedicated LIST_APPEND opcode with neither. It is a constant factor, so I would choose between them on readability, and reach for a generator expression when I do not need the list in memory - which saves the memory, not the time.

Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.

What to notice while it runs

  • The loop's per-iteration bytecode contains LOAD_ATTR and CALL; the comprehension's does not.
  • Both forms are O(n) — the difference is per-item overhead.
  • The measured gap is modest, which is the honest answer to the question.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

The measurement is noisy. The same code timed twice can differ by half, which is why the editor takes the best of five runs - and why quoting a single-run figure in an interview is a claim you cannot defend.

A comprehension over a huge input still builds the whole list. The speed argument is per item; the memory cost is the whole thing, and for a single pass a generator expression removes it.

Readability wins at the point where you have to re-read it. Two for clauses and a condition is usually the limit, and volunteering that boundary is a better answer than defending comprehensions in general.

The follow-ups interviewers ask

"Do dict and set comprehensions work the same way?" Yes, with MAP_ADD and SET_ADD in place of LIST_APPEND. Same saving, same reasoning.

"In a nested comprehension, what order do the for clauses run in?" Left to right, outermost first - the same order you would write the nested loops. [x for row in grid for x in row] flattens; swapping the clauses is a NameError, which is a quick way to check yourself.

"Does a comprehension leak its loop variable?" Not in Python 3 - it has its own scope, so i does not survive. It did leak in Python 2, and the fix is worth knowing because PEP 709 in 3.12 inlined comprehensions again without reintroducing the leak.

Common wrong answers

"Comprehensions are three times faster." Not on a modern interpreter. The specialising interpreter in 3.11 narrowed the gap and the honest figure is a small constant factor - which the editor above measures on whatever version you are running.

"They run in C, so they are parallel." Neither half is true. It is the same bytecode interpreter, one item at a time.

"Always prefer a comprehension." Not when the body needs a statement, and not when the nesting makes it unreadable. Volunteering that limit is usually what the question is actually probing.

Recap in one screen

  • The loop's per-iteration bytecode contains LOAD_ATTR and CALL; the comprehension's does not.
  • Both forms are O(n) - the difference is per-item overhead.
  • The measured gap is modest, which is the honest answer to the question.
  • The one-line answer: Yes, by a little, and for a specific reason: the loop looks up out.
  • Worth trying: Hoist the lookup out of the loop with append = out.append and re-measure. This is classic advice and the result may surprise you on a modern interpreter - measure rather than assume.
  • Worth trying: Compare map(str, range(N)) against [str(i) for i in range(N)], then against map(lambda i: str(i), ...). The lambda undoes the advantage.

How the code works

Best-of-five timings, because these are noisy, and then the per-iteration bytecode of each form so the cause is visible rather than asserted.

How the code works

  1. best_of(fn, n, rounds=5)Timing the same code twice here can differ by 50%, so a single measurement would be a coin flip. Taking the minimum of several runs reports the least-interrupted one.
  2. body(with_loop)The instructions between FOR_ITER and the jump back — that is, one iteration. LOAD_ATTR and CALL are the two the comprehension does not have.
  3. LIST_APPENDOne opcode that appends to the list under construction. There is no name lookup because the list is on the interpreter stack, not in a variable.
  4. sum(i * i for i in range(N))No brackets and no list, so the memory is constant rather than proportional to N. The time is about the same or slightly worse — each item costs a __next__ call, which is roughly what the list build was costing. Reach for it to avoid holding the data, not to go faster.

Change one thing

  • Hoist the lookup out of the loop with append = out.append and re-measure. This is classic advice and the result may surprise you on a modern interpreter — measure rather than assume.
  • Compare map(str, range(N)) against [str(i) for i in range(N)], then against map(lambda i: str(i), ...). The lambda undoes the advantage.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why is a comprehension faster than an append loop?

  2. How does the speed-up scale with n?

  3. When must you use a loop instead?

  4. If the result is consumed once, a generator expression saves you:

Cheat sheet

Why is a comprehension faster than the same loop?

Yes, by a little, and for a specific reason: the loop looks up out.append and calls it on every iteration, while the comprehension appends with a single dedicated opcode, LIST_APPEND. It is a constant factor, not a change in complexity — both are O(n).

INTERVIEW · vizlearn.in/interview/why-a-comprehension-is-faster.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.