What do Python's built-in map() and filter() return, and how do you get a list?
answer
- Not a list in Python 3
- Lazy, computed on demand
- Read it once, then it is spent
- len() and indexing both fail
- list(), tuple(), or a for loop
basics
~20 sBoth return a lazy, one-shot iterator object, not a list. Nothing is computed until you iterate. Wrap the result in list() or tuple(), or feed it straight to a for loop, sum(), any() or all().
solid answer
~40 sIn Python 3, `map(func, iterable)` returns a `map` object and `filter(pred, iterable)` returns a `filter` object. Both are **iterators**: they hold the source and the callable, compute one result per `__next__` call, and are exhausted after a single full pass. That makes them cheap over large inputs and composable without building intermediate lists, but it also means `len()` and indexing raise `TypeError`, a second `list()` on the same object returns `[]`, and printing one shows a repr rather than the values. Materialize with `list()`, `tuple()` or `set()`, or consume directly in a `for` loop or an aggregate such as `sum()`. In Python 2 both returned real lists; Python 3.0 made them lazy, and that is still the behaviour on 3.14.
code
pycon · 7 lines>>> m = map(str.upper, ["a", "b"])
>>> type(m).__name__
'map'
>>> list(m)
['A', 'B']
>>> list(m)
[]go deeper
Recall the one-line fact: map() and filter() give back lazy iterators, so wrap them in list() when you want to print, index, count or reuse the results.
Explain the iterator protocol behind it - iter/next, StopIteration, and why one full pass exhausts the object - and show the two-consumption bug rather than just naming it.
Show the judgment about where to materialise: keep pipelines lazy over large or streaming inputs, but convert to a list at the boundary where the result becomes shared data, and note that exceptions surface at the consumer.
Own the API-boundary call: returning an iterator from a public function makes callers responsible for the one-shot semantics, so decide deliberately whether your library hands back a stream or a materialised collection, and document it.
### Two different things: an iterable and an iterator An **iterable** is anything you can start a loop over — a list, a tuple, a string, a dict. An **iterator** is the cursor that walks it: an object with `__next__()` that yields the next value and finally raises `StopIteration`. Every iterator is also an iterable (its `__iter__` returns itself), which is why a `map` object works in a `for` loop and confuses people into thinking it is a collection. `map(func, iterable)` and `filter(predicate, iterable)` both return iterators. `map` stores the callable and a cursor over the source; each time you ask for the next item it pulls one source element, calls the function on it, and hands back the result. `filter` does the same but pulls until the predicate is truthy, skipping the rest. **No function call happens at the moment you write `map(...)`** — it happens later, once per item, as you consume. ### What laziness buys you Memory is the obvious win. Reading a million-line log and transforming each line with a list comprehension builds a million-element list; `map` over the same file holds one item at a time, so peak memory does not grow with the input. Composition is the second win: `filter(pred, map(func, source))` is a two-stage pipeline that still touches each element once and still holds one element at a time. The third is early exit — `next(filter(pred, items))` finds the first match and stops, without transforming the tail. ### What laziness costs you The cost is that a `map` or `filter` object behaves nothing like the list a beginner expects: * `len(m)` raises `TypeError` — an iterator does not know how many items it will produce, and asking would require consuming it. * `m[0]` raises `TypeError` — there is no indexing protocol, only forward stepping. * `print(m)` prints something like `<map object at 0x...>`, not the values. Wrap it in `list()` to look at it. * **It is one-shot.** After a full pass the underlying cursor is at the end. A second `list(m)` returns `[]`, a second `for` loop body never runs, and a second `sum(m)` is `0`. This is the single most common real bug: code logs `len(list(results))`, then loops over `results` again and silently processes nothing. * An empty result and an exhausted iterator are indistinguishable from the outside, which makes that bug quiet rather than loud. If you need the values more than once, materialize once (`items = list(map(func, source))`) and reuse the list. If you need laziness *and* repetition, keep the source and rebuild the pipeline, or store the source in a list and map over it each time. ### Errors surface late Because the function is not called until consumption, a callable that raises does so from inside `list()`, `sum()` or the `for` statement — not from the line that constructed the `map`. Tracebacks point at the consumer, and a wrapped `try` around only the construction line catches nothing. This is worth knowing before you spend twenty minutes reading the wrong line. ### `filter` and its `None` shortcut `filter(None, iterable)` is a documented special case: passing `None` as the predicate keeps the items that are **truthy**, dropping `0`, `""`, `[]`, `{}`, `None` and `False`. It is not "filter by nothing" and it is not a bug — it is the idiom for dropping blanks. Beyond that special case, `filter` takes exactly one predicate and one iterable. ### The version story In Python 2, `map` and `filter` returned lists, and `map` even padded shorter inputs with `None`. Python 3.0 turned both into lazy iterators as part of the same sweep that made `range`, `dict.keys()` and `zip` lazy. That is still the behaviour on Python 3.14 — no release since has changed it. Interviewers ask this partly because it is the cleanest way to check whether a candidate has internalised the iterator protocol or is still thinking in list-shaped Python 2 habits. ### Practical rule Treat a `map` or `filter` result as a *stream you may read once*. Either consume it in the same expression that created it, or convert it to a list at the boundary where it stops being a pipeline and starts being data.
- Why does len() fail on a map object when len() works on the list you mapped over?`len()` calls `__len__`, which an iterator does not implement, so it raises `TypeError`. An iterator cannot know its length without consuming itself, and consuming it would destroy the values it is meant to hand you. If you need a count, materialise first with `list()` and take the length of that, or count as you consume.
- What does filter(None, items) do, and how is it different from filter(bool, items)?Both keep the truthy items and drop `0`, `""`, `[]`, `{}`, `None` and `False`. Passing `None` is a documented special case handled in C, so it skips a Python-level call per element and is marginally faster; `filter(bool, items)` calls `bool` on every item. Behaviourally they are the same, and `filter(None, ...)` is the conventional spelling.
- If a function passed to map() raises, which line does the traceback point at?The line that **consumes** the iterator — the `list()`, `sum()` or `for` statement — not the line that built the `map` object, because the callable is not invoked until consumption. Wrapping only the construction line in `try` catches nothing. Put the guard around the consumption, or materialise eagerly if you want failures to surface where the pipeline is defined.
A list is a printed page you can reread; a map object is a ticker tape being typed as you read it. Look away and let it run to the end, and the tape is spent — the values were never stored.
saying these in an interview costs you the question
- Saying map() returns a list in Python 3
- Calling len() or indexing a map or filter object
- Assuming the function runs when map() is written
- Iterating the same map object twice and expecting values
- Thinking filter(None, xs) filters nothing out
- Printing a map object and reporting it as empty