You made it through week one. The warehouse product specs are tuples now, the duplicate categories are sets, and you've got named tuples doing the heavy lifting. So what's bothering you this week?
The three functions. I have format_report, format_report_with_header, and format_report_with_header_and_padding. They do basically the same thing and every time I fix a bug I have to fix it in three places.
That smell has a name. And it means your function signature doesn't have enough vocabulary yet. Default arguments let one function handle all three variations. When the caller doesn't pass a header, the default kicks in.
So instead of three functions I'd have one with header=None and padding=0?
Exactly. And then we push further. What if the warehouse wants to restock a variable number of SKUs in one call — sometimes two, sometimes twenty? You don't know in advance how many to expect.
Can't I just pass a list?
You can. But *args lets callers pass individual arguments and Python bundles them into a tuple automatically. It's the difference between restock(["SKU-001", "SKU-002"]) and restock("SKU-001", "SKU-002"). The second one reads like spoken English. Here's the pattern:
def restock(*skus, quantity=1):
for sku in skus:
print(f"Restocking {quantity} of {sku}")
restock("SKU-001", "SKU-002", "SKU-003", quantity=10)Fine. I see why that's cleaner. But lambda? I'm not convinced lambda is ever the right choice when you can just write a named function.
Write that grievance down. We'll revisit it when you see lambda inside sorted(). By Thursday you might soften slightly.
You know how to write a function. You've been doing it since Track 1 week two. But there's a gap between the functions beginners write and the functions that appear in production Python code — and that gap is almost entirely about how functions receive their inputs.
Default arguments are the most immediately useful concept this week. A function with default values is a contract: here's what I'll do if you don't specify. The formatter that takes header=None and padding=0 does not need three versions. It has one definition and the caller supplies only what differs from the standard behavior. The practical catch is mutable defaults — passing a list as a default value creates a shared object that persists across calls, a bug that trips up even experienced Python developers.
*args and **kwargs expand what a function can accept. *args collects any number of positional arguments into a tuple — your function doesn't need to know in advance how many items will arrive. **kwargs collects named arguments into a dictionary — each keyword becomes a key. Combining them gives you maximum flexibility: fixed required arguments, then a variable pile of positional extras, then a variable collection of named options.
Positional-only and keyword-only parameters put constraints on callers. Marking a parameter positional-only (with /) means callers cannot use it as a keyword argument. Marking one keyword-only (with *) means callers must name it explicitly. These are not just style rules — they're enforced by Python and let you design function signatures that are impossible to call incorrectly.
Lambda functions are small anonymous functions defined in a single expression. They are not replacements for def — they exist specifically for cases where you need a tiny throwaway function and giving it a name would be noise. The canonical use case is key= arguments in sorted(), min(), and max(). Sorting a list of product tuples by price is one line with a lambda, and the lambda disappears the moment sorting is done.
Docstrings and argument unpacking close the week. Unpacking a list or dict into a function call with * and ** is the mirror image of *args/**kwargs — you collect on the way in and unpack on the way out. Docstrings make your warehouse functions readable to the next person, including you in three months.
Sign up to save your notes.
You made it through week one. The warehouse product specs are tuples now, the duplicate categories are sets, and you've got named tuples doing the heavy lifting. So what's bothering you this week?
The three functions. I have format_report, format_report_with_header, and format_report_with_header_and_padding. They do basically the same thing and every time I fix a bug I have to fix it in three places.
That smell has a name. And it means your function signature doesn't have enough vocabulary yet. Default arguments let one function handle all three variations. When the caller doesn't pass a header, the default kicks in.
So instead of three functions I'd have one with header=None and padding=0?
Exactly. And then we push further. What if the warehouse wants to restock a variable number of SKUs in one call — sometimes two, sometimes twenty? You don't know in advance how many to expect.
Can't I just pass a list?
You can. But *args lets callers pass individual arguments and Python bundles them into a tuple automatically. It's the difference between restock(["SKU-001", "SKU-002"]) and restock("SKU-001", "SKU-002"). The second one reads like spoken English. Here's the pattern:
def restock(*skus, quantity=1):
for sku in skus:
print(f"Restocking {quantity} of {sku}")
restock("SKU-001", "SKU-002", "SKU-003", quantity=10)Fine. I see why that's cleaner. But lambda? I'm not convinced lambda is ever the right choice when you can just write a named function.
Write that grievance down. We'll revisit it when you see lambda inside sorted(). By Thursday you might soften slightly.
You know how to write a function. You've been doing it since Track 1 week two. But there's a gap between the functions beginners write and the functions that appear in production Python code — and that gap is almost entirely about how functions receive their inputs.
Default arguments are the most immediately useful concept this week. A function with default values is a contract: here's what I'll do if you don't specify. The formatter that takes header=None and padding=0 does not need three versions. It has one definition and the caller supplies only what differs from the standard behavior. The practical catch is mutable defaults — passing a list as a default value creates a shared object that persists across calls, a bug that trips up even experienced Python developers.
*args and **kwargs expand what a function can accept. *args collects any number of positional arguments into a tuple — your function doesn't need to know in advance how many items will arrive. **kwargs collects named arguments into a dictionary — each keyword becomes a key. Combining them gives you maximum flexibility: fixed required arguments, then a variable pile of positional extras, then a variable collection of named options.
Positional-only and keyword-only parameters put constraints on callers. Marking a parameter positional-only (with /) means callers cannot use it as a keyword argument. Marking one keyword-only (with *) means callers must name it explicitly. These are not just style rules — they're enforced by Python and let you design function signatures that are impossible to call incorrectly.
Lambda functions are small anonymous functions defined in a single expression. They are not replacements for def — they exist specifically for cases where you need a tiny throwaway function and giving it a name would be noise. The canonical use case is key= arguments in sorted(), min(), and max(). Sorting a list of product tuples by price is one line with a lambda, and the lambda disappears the moment sorting is done.
Docstrings and argument unpacking close the week. Unpacking a list or dict into a function call with * and ** is the mirror image of *args/**kwargs — you collect on the way in and unpack on the way out. Docstrings make your warehouse functions readable to the next person, including you in three months.