A default argument is a value Python supplies when the caller doesn't pass one. They make function signatures shorter at the call site, give you a sensible fallback for optional knobs, and let you add new parameters to a function without breaking existing callers. They also come with one famous trap, the mutable default argument, that has bitten almost every Python programmer at least once. This lesson covers how defaults work, when they're evaluated, the trap and its fix, and how to pick good default values.
A default value is set in the function signature with name=value. If the caller passes that argument, the value they pass wins. If they don't, the default is used.
The first call omits quantity, so Python uses the default 1. The second call passes 3, which overrides the default for that call only. The default itself isn't changed by the override; the next call without a quantity argument would still see 1.
Parameters without defaults are required: if the caller omits them, Python raises TypeError. Parameters with defaults are optional. The two kinds can sit in the same signature, but Python requires the required ones to come first.
item has no default, so leaving it out is an error. quantity has a default, so leaving it out is fine.
If you try to put a required parameter after an optional one in the signature, Python rejects the function definition outright:
The rule is straightforward: all required parameters first, then all optional ones. Mixing them in the wrong order is a SyntaxError, not a runtime error, which means Python catches it the moment it tries to compile the file.
You can have many optional parameters, each with its own default. They follow the same rule among themselves: order doesn't matter for evaluation, just for the syntax.
Three calls, three different ways of using the defaults. Alice's call takes both defaults. Bob's call overrides discount but keeps currency. Carol's call overrides both. The function doesn't know or care which arguments came from defaults and which came from the caller. By the time the body runs, every parameter just has a value.
When a function has several optional parameters, you often want to override just one of them while keeping the rest at their defaults. Positional calls don't help here, because positional arguments fill the slots left-to-right. You'd have to repeat all the earlier defaults just to reach the one you care about.
Keyword arguments solve this. Pass the parameter by name and Python skips the rest.
Without keyword arguments, you'd have to write place_order("alice", [29.99, 14.99], "USD", 0.0, True). That works, but it forces the caller to know and repeat the defaults for currency and discount just to reach gift_wrap. The keyword form lets the caller say exactly what they want to change and ignore everything else.
Keyword arguments also make calls more readable. A call like place_order("alice", [29.99], 0.1) is ambiguous unless you remember the signature: does that 0.1 mean a currency code (no, that's a string) or a discount? place_order("alice", [29.99], discount=0.1) answers the question at the call site.
You can mix positional and keyword arguments in the same call. The rule is: all positional arguments come first, then all keyword arguments. The positional ones fill parameters left-to-right; the keyword ones fill parameters by name.
Alice's call uses two positional arguments (customer, items) and two keyword arguments (discount, gift_wrap), letting currency stay at its default. Bob's call uses three positional arguments and one keyword argument, letting discount stay at its default.
Trying to pass a positional argument after a keyword argument is a SyntaxError:
Once you start using names in a call, every following argument also has to use a name. The rule keeps Python's argument-binding logic simple: positional first, keyword after.
Passing the same parameter both positionally and by keyword is also an error, but a different one (raised at call time, not parse time):
The third positional argument already filled currency. The currency="EUR" keyword then tries to fill it again, and Python refuses. Each parameter gets exactly one value per call.
Here's the part that surprises people. Default values are evaluated once, when Python runs the def statement, not each time the function is called. The result is stored on the function object and reused for every call that omits that argument.
For immutable defaults like numbers and strings, this is invisible. 1 is 1 no matter when you ask. But the rule applies to every default, including expressions you might think run on each call.
(The exact timestamp depends on when you run the code, but all three lines will show the same timestamp.)
That looks wrong at first. The function clearly calls time.time() somewhere, and time.time() returns the current wall-clock time, so different calls should show different times. The catch is that time.time() runs once, when Python compiles the def and builds the function object. The value it returns at that moment is baked into the function as the default for when. Every call that omits when gets that same frozen timestamp.
A counter makes the point even more clearly:
make_default is called exactly once, when the def place_order(...) line runs. It returns 1, and 1 becomes the default for order_id. Every later call without order_id reuses that same 1. The counter is never bumped past 1 because make_default never runs again.
The diagram shows the lifecycle. The cyan node is the def statement Python encounters at module load time. The orange step evaluates each default expression in the signature. The teal step stores the resulting values on the function object (you can inspect them later via place_order.__defaults__). After that, all three calls reuse those frozen values; none of them re-run the original expression.
You can inspect the stored defaults directly:
__defaults__ is a tuple of the default values, in the order they appear in the signature among parameters that have defaults. It's set once and stays put unless you (or some buggy code) reassign it.
For immutable defaults, "evaluated once" is harmless because there's nothing to mutate. The number 1 is the same number forever. The same is true for strings, tuples of immutables, None, and the boolean values. The trap shows up when the default is mutable.
This is the bug. The single most famous Python footgun. The pattern is so common that every Python style guide warns about it, and it's still on every interview short-list.
A default like [] or {} looks like "give me an empty list each time" or "give me an empty dict each time". It isn't. Because defaults are evaluated once, the [] is one list object, created when the def runs, and shared by every call that uses the default.
Each call uses the default cart, but the default is the same list, so each call's append mutates the list that earlier calls already added to. The "fresh empty cart" the signature seems to promise never appears; what you get is one shared cart that grows across every call.
This breaks the whole illusion of "each customer gets their own cart". If add_to_cart is called by three different customers without passing a cart, the second customer sees the first customer's item, and the third sees both. In a real system, that's a data leak between users.
The mechanics are easier to picture as before-and-after:
The left half is the bug. One list is built at def time and reused on every call, so all three calls share state. The right half is the fix. None is the default; each call checks for None and builds a fresh list when it sees one. There's no shared state because each call allocates its own list.
You can prove the sharing by reading __defaults__ between calls:
The default itself is changing, because the calls are mutating it. The function object's __defaults__ tuple still holds the same list; the list is what's growing.
Dictionaries have the same problem:
Same pattern. One dict, shared, growing. A function that's supposed to "make a fresh log entry per call" is actually appending to a global log nobody declared.
Sets have the same problem too. Anything mutable does.
The rule that keeps you safe: never put a mutable object as a default value. Not [], not {}, not set(), not an instance of any class whose methods can change its state.
None Sentinel FixThe fix is a small two-line dance: use None as the default, then check for None inside the body and build a fresh mutable object when you see one.
Three independent carts. The default None is immutable (in fact None is a singleton; there's literally only one None in a Python program), so there's nothing to share or mutate. Each call that omits cart sees None, hits the if cart is None: branch, builds a brand-new list, and works on that list. Calls that pass a cart skip the if and operate on the cart the caller provided.
The check is is None, not == None. Use is when comparing against None because is checks object identity and there's only one None object in the language. == would invite trouble if some object's __eq__ was overridden to return True for None, which would mean a non-None object could sneak past your check.
This pattern is sometimes called the sentinel pattern. None is the sentinel value, a special marker that means "nothing was passed". Inside the body, you turn the sentinel into the real default.
The same pattern works for any mutable default:
Three independent logs, exactly what the original signature seemed to promise. The body is one line longer, but the function is correct.
For functions that accept an existing collection or build a new one, the sentinel pattern also makes "I want to start fresh" and "I want to extend yours" both work cleanly with the same parameter:
Alice's three calls thread the same cart through. Bob's call gets a fresh one. The two carts don't see each other, because the function only ever reuses a cart when the caller explicitly hands it in.
Cost: The if cart is None: cart = [] line runs on every call, but is comparisons against None are cheap, and allocating a small empty list is also cheap. Don't avoid the pattern for performance; the per-call overhead is well under a microsecond.
There's a second sentinel pattern worth knowing: distinguishing "the caller passed None" from "the caller passed nothing". For most code, treating None as "nothing was passed" is fine. When you need the finer distinction, define your own sentinel:
The third call explicitly passed None as the locale, and the function preserves that intent rather than treating it as "no locale given". The _MISSING object is a private sentinel that no caller could ever construct, so the is _MISSING check is reliable.
You don't need this finer distinction most of the time. Reach for it only when None has a meaning in your domain that's different from "nothing was passed". For the typical case, None as the sentinel is the standard Python idiom.
Once you know the rule (immutable defaults are safe, mutable defaults are not), choosing good defaults becomes a small checklist. The full list of safe choices is short:
| Default value | Type | Safe as default? |
|---|---|---|
None | NoneType | Yes |
Numbers (0, 1, 3.14) | int, float | Yes |
Booleans (True, False) | bool | Yes |
Strings ("", "USD") | str | Yes |
Tuples of immutables ((), (1, 2)) | tuple | Yes |
Frozensets (frozenset()) | frozenset | Yes |
Lists ([], [1, 2]) | list | No |
Dicts ({}) | dict | No |
Sets (set()) | set | No |
| Any object whose state can change | mutable class | No |
The "safe" rows are all immutable types. Once they exist, nothing the function does can modify them. The "no" rows are all mutable: a method or operation on them can change their contents in place, which is exactly what causes the sharing bug.
A few concrete patterns show up over and over.
Counts and amounts default to `0`:
0 and 0.0 are perfect "no effect" defaults for additive and multiplicative operations: discount of 0.0 means no discount, tax of 0.0 means no tax. The function works without thinking about whether the caller passed those arguments.
Flags default to the cheap or off state:
False is the right default for "opt-in" features. Customers who don't ask for gift wrap shouldn't get it for free, so the default has to be off.
Strings default to a fixed code or marker:
Strings are immutable, so they're safe to put directly in the signature. Pick a string that makes sense as "default behavior" rather than a magic value that needs special handling.
Tuples of immutables work for fixed groups:
A tuple of strings is immutable: no method on it can change its contents. That makes it safe to use as a default, where a list with the same contents would be a bug. If you need the "default group" pattern and don't want the sentinel dance, a tuple of immutables is the cleanest way to express it.
Frozensets work the same way:
A frozenset is the immutable cousin of set. It supports the same membership checks but can't be added to or removed from, which makes it safe as a default and slightly cheaper to look up than a tuple for the "is this in the allowed group?" question.
For mutable collections, use the sentinel pattern:
Anywhere you'd be tempted to write cart=[] or log={}, write None and translate it inside the body. The translation is one line, and it gets rid of an entire category of bug.
A small style note: don't use None as a default just because it feels neutral. If the function genuinely needs an integer or a string, pick an integer or string default. None is the right default when the parameter is genuinely optional and the function knows how to handle "nothing supplied". For values that are always required to be present in some form, an explicit immutable default is clearer.
10 quizzes