Python for JS Devs · Fundamentals

Functions & the mutable-default trap

Keyword arguments are a first-class idea in Python — and one default-value gotcha catches every newcomer.

Lesson 05Time ~12 min Win Explain the mutable-default bug out loudPrereq Lesson 04

1 · Defaults & keyword arguments

JS got default params and named-ish args (via object destructuring) later. Python had them from the start, and callers can pass any argument by name — a huge readability win you'll see all over FastAPI.

JavaScript
function connect(host, port = 5432,
                 { ssl = false } = {}) {}

connect("db", 5432, { ssl: true });
Python
def connect(host, port=5432, ssl=False):
    ...

connect("db", ssl=True)   # pass by name, skip port

Any parameter can be passed positionally or by keyword. Passing by keyword (ssl=True) is idiomatic when it aids clarity — and FastAPI leans on it heavily.

2 · *args and **kwargs — rest & spread

JavaScript
function log(...args) {}
log(...items);

fn({ ...opts });
Python
def log(*args, **kwargs): ...
log(*items)          # spread a list into positionals
fn(**opts)           # spread a dict into keywords

*args collects extra positional args into a tuple; **kwargs collects extra keyword args into a dict. The */** also spread at the call site.

3 · The mutable-default trap ⚠️

This is the classic Python bug, and it has no JS equivalent. Default argument values are evaluated once, at function-definition time — not on each call. So a mutable default (a list/dict) is shared across every call.

def add_item(item, cart=[]):   # ⚠️ the [] is created ONCE
    cart.append(item)
    return cart

add_item("a")   # → ['a']
add_item("b")   # → ['a', 'b']  😱  same list reused!
The fix — memorize this pattern Default to None, then create the real object inside the body:
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
Why it matters for FastAPI You'll write many function signatures with defaults. This rule keeps request handlers from silently sharing state between requests.

4 · Functions are first-class (and lambdas are tiny)

handlers = {"add": add_item}     # store functions in dicts
sorted(users, key=lambda u: u["age"])  # lambda = one-expression fn

Lambdas are deliberately limited to a single expression. If you need more, use a named def — Python nudges you toward readable named functions.

🎯 Your win

In the REPL, paste the buggy add_item and call it twice to see the shared list. Then paste the fixed version and confirm each call starts fresh. Being able to say out loud "defaults evaluate once at def-time" is the win — it's a favourite interview question too.

Check yourself

Q1. When is a default argument value evaluated?
Defaults are evaluated once at definition time — which is why a mutable default is shared across calls.
Q2. The safe pattern for a list default is:
Use param=None then if param is None: param = [] inside the function.
Q3. **kwargs collects extra keyword args into a:
**kwargs is a dict; *args is a tuple of extra positionals.
Primary source Python docs — Defining Functions. The section on default values calls out this exact trap.
💬 Ask your teacher: what are keyword-only arguments (the bare * in a signature)? You'll see them in real APIs.