open atlas
↑ Back to track
Python for JS/TS developers PY · 01 · 03

Comprehensions and functions

Comprehensions are Pythonic map/filter; generator expressions stream lazily over huge inputs. Functions add a sharp trap — default args are evaluated once at def time — plus *args/**kwargs and LEGB closures unlike JS.

PY Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A teammate ports a JS data pipeline to Python and writes result = list(map(lambda x: x * 2, filter(lambda x: x > 0, nums))). It works, but it reads like a translation, not Python. Then a different function, def add_tag(tag, tags=[]), starts returning tags from previous calls it never received — a ghost accumulating across the whole process. Both are the same lesson: Python’s expression-level tools and its function model do not behave like JavaScript’s, and the gaps bite exactly where you stop paying attention.

Comprehensions: map and filter, fused into one expression

A list comprehension is Python’s idiomatic replacement for chaining .map() and .filter(). The shape [f(x) for x in xs if pred(x)] reads in evaluation order — source, filter, transform — and produces a list in one pass. Where JavaScript writes xs.filter(pred).map(f), Python writes the transform first and the filter last, but the data flows the same way: each element of xs is tested by pred, and the survivors are passed through f.

nums = [-2, -1, 0, 1, 2, 3]
# JS: nums.filter(x => x > 0).map(x => x * 2)
doubled = [x * 2 for x in nums if x > 0]   # [2, 4, 6]

# Same syntax, different brackets, different collection:
squares = {x: x * x for x in range(4)}      # dict: {0: 0, 1: 1, 2: 4, 3: 9}
unique  = {len(w) for w in ["a", "bb", "cc"]}  # set: {1, 2}

The {...} form gives you a dict or set comprehension depending on whether you write k: v or a bare element — there is no JS object-literal equivalent that is as direct. This is the first reason comprehensions matter: one syntax family covers map, filter, and the construction of every built-in collection.

Nested comprehensions exist — [c for row in matrix for c in row] flattens a 2D list, with the loops read left-to-right just as if you had nested for statements. But this is where readability dies fast. A comprehension with two for clauses and a conditional is at the edge of what a reader can parse at a glance; three is past it. The senior judgment call: a comprehension should fit in your head in one read. The moment it does not, write the explicit loop. Cleverness in a comprehension is a cost you pay on every future read.

Generators: the same shape, but lazy

Swap the square brackets for parentheses and [f(x) for x in xs] becomes a generator expression (f(x) for x in xs). This is not a cosmetic change. The list comprehension is eager: it builds the entire list in memory before the next line runs. The generator is lazy: it computes one value at a time, only when asked, and holds essentially constant memory regardless of how many elements flow through it.

# Eager: materializes 10 million ints — hundreds of MB resident.
total = sum([x * x for x in range(10_000_000)])

# Lazy: one value at a time, memory stays flat.
total = sum(x * x for x in range(10_000_000))

The same idea drives the yield keyword. A function containing yield is a generator function: calling it does not run the body — it returns a generator object that runs lazily, pausing at each yield and resuming on the next request. This is how you stream data that does not fit in memory.

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")   # one line resident at a time, not the whole file

for line in read_lines("huge.log"):    # constant memory over a 50 GB file
    ...

This is the real senior point of the whole first half. Reach for an eager list when you need the result more than once, need its length, or need to index into it. Reach for a lazy generator when the input is large or unbounded, you only pass over it once, and you want memory to stay flat — a streaming pipeline, a file reader, an API paginator.

Why this works

A generator is single-use: it is an iterator that exhausts as you consume it. After one full pass, it is empty — a second for loop over the same generator object yields nothing, and list(gen) after you have already iterated returns []. This surprises people coming from JS arrays, which you can iterate any number of times. If you need to traverse the data twice, either materialize it once with list(gen) or build a fresh generator. The laziness that saves memory is exactly what makes it one-shot.

Functions: arguments, and the trap that defines Python

Before you write your first Python function that takes an optional list argument, there’s one rule you must know — or you’ll spend an afternoon hunting a ghost.

def defines a function; arguments can be passed positionally or by keyword, and parameters can carry defaults: def connect(host, port=5432, *, timeout=30). The bare * marks everything after it keyword-only — callers must write timeout=10, never a bare positional — which is how you keep a call site readable as the signature grows. *args collects extra positionals into a tuple and **kwargs collects extra keywords into a dict; at the call site the same */** unpack an iterable or mapping into arguments, the Python analog of JS spread.

def log(level, *args, **kwargs):
    print(level, args, kwargs)

log("INFO", "started", port=8080)   # INFO ('started',) {'port': 8080}

Now the trap. A default argument is evaluated exactly once, when the def statement runs — not on each call. If the default is a mutable object, every call that omits that argument shares the same object, and mutations persist across calls.

def add_tag(tag, tags=[]):     # the [] is created ONCE, at def time
    tags.append(tag)
    return tags

add_tag("a")   # ['a']
add_tag("b")   # ['a', 'b']  ← the same list, surviving across calls

This is not a bug in Python; it falls directly out of “defaults are evaluated at definition time.” JavaScript re-evaluates default parameters on every call, so function f(x = []) is fresh each time — the exact opposite. The idiomatic Python fix is the sentinel:

def add_tag(tag, tags=None):
    if tags is None:
        tags = []          # a fresh list on every call that omits it
    tags.append(tag)
    return tags

Lambdas, closures, and LEGB scope

lambda makes an anonymous function, but it is deliberately limited: a single expression, no statements, no annotations. It is not the JS arrow function — there is no multi-line lambda. Use it for a throwaway key function (sorted(words, key=lambda w: len(w))) and reach for a named def the instant it wants a second line.

Functions are first-class — passed, returned, stored — and an inner function that references an outer function’s variable forms a closure, capturing that variable by reference. Name resolution follows LEGB: Python looks in the Local scope, then any Enclosing function scopes, then the Global (module) scope, then Built-ins. Crucially, assigning to a name inside a function makes it local for the whole function unless you say otherwise — global rebinds a module-level name, and nonlocal rebinds a name in the nearest enclosing function. JS resolves scope lexically too, but let/const give you per-block bindings, whereas Python’s scope unit is the function, and reassignment-implies-local is a sharper edge.

def make_counter():
    count = 0
    def tick():
        nonlocal count    # without this, count += 1 raises UnboundLocalError
        count += 1
        return count
    return tick
JavaScript idiomPython idiomNote
xs.map(f)[f(x) for x in xs]Comprehension, one pass
xs.filter(p)[x for x in xs if p(x)]Filter is the trailing if
x => x * 2lambda x: x * 2Single expression only; else def
f(…arr) / {…obj}f(*seq) / f(**mapping)Spread → unpacking
function(…args)def f(*args, **kwargs)Rest params → *args/**kwargs
generators / yield(f(x) for x in xs) / yieldSame lazy idea, parens not brackets
function f(x = []) fresh each calldef f(x=None) + sentinelPython default evaluated once at def
Quiz

What does add_tag('a'); add_tag('b') print if defined as def add_tag(tag, tags=[]): tags.append(tag); return tags?

Quiz

g = (x*x for x in range(3)). After total = sum(g), what does list(g) return?

Pick the best fit

A pipeline must transform a 50 GB log file line by line and write a summary, on a box with 2 GB RAM. Pick the construct.

Recall before you leave
  1. 01
    When should you choose a generator expression over a list comprehension, and what is the cost?
  2. 02
    Why does def f(x=[]) accumulate state across calls, and what is the idiomatic fix?
Recap

Comprehensions are Python’s fused map-and-filter: [f(x) for x in xs if pred(x)] replaces a .filter().map() chain in one readable pass, and the {...} form builds dicts and sets too — but a comprehension that no longer fits in your head in one read should become an explicit loop. Swapping the brackets for parentheses turns it into a lazy generator expression, the same lazy machinery as yield: one value at a time, constant memory, single-use. Collect with a list when you need the result repeatedly, its length, or indexing; stream with a generator when the input is large or one-pass. On the function side, *args/**kwargs collect and unpack arguments, the bare * makes parameters keyword-only, and lambda is a single-expression throwaway — reach for def the moment it grows. The defining trap: default arguments are evaluated once at def time, so a mutable default like [] is shared across calls; use None plus a sentinel. Finally, name resolution is LEGB, scope is per-function not per-block, and assignment makes a name local unless you reach for global or nonlocal to rebind an outer one. Now when you see a function signature with x=None checked right inside the body, or a generator expression passed directly to sum(), you’ll recognize the deliberate pattern — not an oversight.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.