Yesterday batch_restock had *product_updates collecting all positional args. I want to push on something you said — that parameters after *args have to be named. What exactly happens in memory when the caller passes move_stock("SKU-001", 50, "Phoenix", "Dallas")?
Without a signature, Python would just match positionally — first arg to first param, second to second, and so on. If the signature is move_stock(sku, quantity, from_warehouse, to_warehouse), then it works. But I have to trust that whoever calls it gets the warehouse order right.
And that's the failure mode. move_stock("SKU-001", 50, "Phoenix", "Dallas") versus move_stock("SKU-001", 50, "Dallas", "Phoenix") — both calls look valid. Both parse without error. One ships to the wrong destination. The function cannot tell them apart.
So you need the warehouses to be required by name — from_warehouse="Phoenix", to_warehouse="Dallas" — so the call site is self-documenting and the direction can't be flipped silently.
Exactly. And Python 3.8 added direct syntax for this. Two separator markers: / marks the end of the positional-only zone, * marks the beginning of the keyword-only zone.
def move_stock(sku: str, quantity: int, /, *, from_warehouse: str, to_warehouse: str) -> dict:
return {
"sku": sku,
"quantity": quantity,
"from": from_warehouse,
"to": to_warehouse,
}A bare / and a bare * in the middle of the parameter list — not attached to any parameter name. That looks like a typo.
Separators, not parameters. The / says: everything to my left is positional-only — the caller must pass it by position, may not use a keyword. The * says: everything to my right is keyword-only — the caller must use a keyword name, may not pass it positionally.
So sku and quantity are before / — positional-only. from_warehouse and to_warehouse are after * — keyword-only. There's nothing in between / and * in this signature, so no middle-ground parameters.
Right. And Python enforces both at runtime:
move_stock("SKU-1001", 50, from_warehouse="Phoenix", to_warehouse="Dallas") # correct
move_stock(sku="SKU-1001", quantity=50, from_warehouse="Phoenix", to_warehouse="Dallas")
# TypeError: got some positional-only arguments passed as keyword arguments: 'sku, quantity'
move_stock("SKU-1001", 50, "Phoenix", "Dallas")
# TypeError: takes 2 positional arguments but 4 were givenHard enforcement. Not a linting suggestion — a TypeError at runtime. There's no workaround?
No workaround. The signature is the contract and Python enforces it. Think about the warehouse loading docks. The pallets dock has a forklift and a weight minimum — you physically cannot bring a small parcel delivery van up to it. The parcels dock has a size limit and a hand trolley — you can't back up a freight truck there. The dock design enforces what it accepts. / and * are the dock design.
So / is the pallets dock rule — "this goes in by position, no address labels needed, the order is obvious." And * is the parcels dock rule — "every package needs an explicit address label, direction matters too much to guess."
That's the mental model. Now — why do sku and quantity belong in the positional-only zone? Why not leave them as normal parameters?
Because the order is semantically unambiguous. SKU is the identity of the product. Quantity is how many. Nobody would flip those. Adding sku= and quantity= labels at every call site would be noise, not clarity.
And the warehouse strings are the opposite — they're both str, they look identical structurally. If you pass them positionally, the chance of getting the direction backwards is roughly fifty percent. The keyword enforces the direction in the signature itself.
I had a function in Track 1 with start_date and end_date both as plain positional strings. I flipped them at least once. The report came back empty and I spent an hour figuring out why.
Keyword-only markers would have caught that at runtime the moment you tried to pass them in the wrong order without naming them. Five seconds instead of an hour.
Do I need both markers together? What if I only want keyword-only without the positional-only restriction?
Use just *. A bare * in the signature marks the keyword-only zone without positional-only. You've already seen this implicitly — anything after *args is keyword-only. A bare * does the same enforcement without capturing any extra positional arguments:
def reserve_stock(sku: str, *, warehouse: str, days: int = 7) -> dict:
return {"sku": sku, "warehouse": warehouse, "reserved_days": days}
reserve_stock("SKU-001", warehouse="Phoenix") # correct — days defaults
reserve_stock("SKU-001", warehouse="Dallas", days=14) # both keyword args
reserve_stock("SKU-001", "Phoenix", 14) # TypeError — keyword-onlyAnd sorted() uses this — sorted(iterable, *, key=None, reverse=False). The bare * is why I can't do sorted(products, len) — key is keyword-only.
You've been following that rule without knowing it existed. The standard library uses / and * throughout. Now you can read those signatures and know exactly what's required.
When do I reach for this versus just leaving everything as normal parameters?
Two situations. First: parameters that are stable and positional by obvious convention — like SKU and quantity — where keyword labels would be noise. Use / to lock in that positional contract. Second: parameters that look similar enough at the call site to cause direction errors — like two warehouse strings. Use * to require naming. Most functions need neither. Precision tools, not defaults.
Write move_stock. I've got the shape.
Go. Tomorrow is lambda — the temporary label printer. You've already used it without understanding it. I want that to change.
Python 3.8 introduced two positional markers in function signatures: / ends the positional-only zone, * begins the keyword-only zone. Both are separators — they are not parameter names.
def move_stock(sku: str, quantity: int, /, *, from_warehouse: str, to_warehouse: str) -> dict:
return {"sku": sku, "quantity": quantity, "from": from_warehouse, "to": to_warehouse}
# Positional-only — sku and quantity must be passed by position
move_stock("SKU-1001", 50, from_warehouse="Phoenix", to_warehouse="Dallas") # correct
# TypeError: cannot pass positional-only args as keywords
move_stock(sku="SKU-1001", quantity=50, from_warehouse="Phoenix", to_warehouse="Dallas")
# TypeError: warehouse args cannot be passed positionally
move_stock("SKU-1001", 50, "Phoenix", "Dallas")Zone summary:
| Zone | Marker | How caller must pass |
|---|---|---|
Before / | positional-only | Position only — keyword label forbidden |
Between / and * | normal | Either position or keyword |
After * | keyword-only | Keyword only — positional forbidden |
Bare * without /: When you only want keyword-only enforcement (no positional-only restriction), use a bare * alone. Any parameter after it must be named at the call site.
def reserve_stock(sku: str, *, warehouse: str, days: int = 7) -> dict:
return {"sku": sku, "warehouse": warehouse, "reserved_days": days}
reserve_stock("SKU-001", warehouse="Phoenix") # correct
reserve_stock("SKU-001", "Phoenix") # TypeError — keyword-onlyThe standard library uses both markers extensively. sorted(iterable, *, key=None, reverse=False) is the canonical bare-* example — it prevents sorted(products, len) from silently passing len as the key.
Pitfall 1: Forgetting that / and * are separators, not parameters. Newcomers read def f(a, /, b, *, c) and expect / and * to capture arguments. They don't — they only enforce call-site rules for the parameters around them.
Pitfall 2: Overusing these markers. Most functions need neither. Use / when the parameter order is semantically obvious and keyword labels add noise. Use * when two or more parameters have the same type and a caller could silently flip their values. Applying them everywhere produces unreadable signatures.
Pitfall 3: Confusing bare * with *args. A bare * in a signature does not collect extra positional arguments — it only marks the keyword-only zone. *args collects all remaining positional arguments into a tuple. They look similar but behave differently.
def f(a, *, b): # bare * — b must be keyword, no extra positional args collected
pass
def g(a, *args, b): # *args — extra positionals collected; b must be keyword
pass/ and * also interact with *args and **kwargs in the same signature — advanced APIs combine all four for maximum call-site control. The CPython source uses / heavily in built-in functions like len(), range(), and isinstance() — their signatures in help() show / at the end of parameters that are positional-only by C implementation necessity. Reading those signatures fluently is a skill that separates intermediate from advanced Python developers.
Sign up to write and run code in this lesson.
Yesterday batch_restock had *product_updates collecting all positional args. I want to push on something you said — that parameters after *args have to be named. What exactly happens in memory when the caller passes move_stock("SKU-001", 50, "Phoenix", "Dallas")?
Without a signature, Python would just match positionally — first arg to first param, second to second, and so on. If the signature is move_stock(sku, quantity, from_warehouse, to_warehouse), then it works. But I have to trust that whoever calls it gets the warehouse order right.
And that's the failure mode. move_stock("SKU-001", 50, "Phoenix", "Dallas") versus move_stock("SKU-001", 50, "Dallas", "Phoenix") — both calls look valid. Both parse without error. One ships to the wrong destination. The function cannot tell them apart.
So you need the warehouses to be required by name — from_warehouse="Phoenix", to_warehouse="Dallas" — so the call site is self-documenting and the direction can't be flipped silently.
Exactly. And Python 3.8 added direct syntax for this. Two separator markers: / marks the end of the positional-only zone, * marks the beginning of the keyword-only zone.
def move_stock(sku: str, quantity: int, /, *, from_warehouse: str, to_warehouse: str) -> dict:
return {
"sku": sku,
"quantity": quantity,
"from": from_warehouse,
"to": to_warehouse,
}A bare / and a bare * in the middle of the parameter list — not attached to any parameter name. That looks like a typo.
Separators, not parameters. The / says: everything to my left is positional-only — the caller must pass it by position, may not use a keyword. The * says: everything to my right is keyword-only — the caller must use a keyword name, may not pass it positionally.
So sku and quantity are before / — positional-only. from_warehouse and to_warehouse are after * — keyword-only. There's nothing in between / and * in this signature, so no middle-ground parameters.
Right. And Python enforces both at runtime:
move_stock("SKU-1001", 50, from_warehouse="Phoenix", to_warehouse="Dallas") # correct
move_stock(sku="SKU-1001", quantity=50, from_warehouse="Phoenix", to_warehouse="Dallas")
# TypeError: got some positional-only arguments passed as keyword arguments: 'sku, quantity'
move_stock("SKU-1001", 50, "Phoenix", "Dallas")
# TypeError: takes 2 positional arguments but 4 were givenHard enforcement. Not a linting suggestion — a TypeError at runtime. There's no workaround?
No workaround. The signature is the contract and Python enforces it. Think about the warehouse loading docks. The pallets dock has a forklift and a weight minimum — you physically cannot bring a small parcel delivery van up to it. The parcels dock has a size limit and a hand trolley — you can't back up a freight truck there. The dock design enforces what it accepts. / and * are the dock design.
So / is the pallets dock rule — "this goes in by position, no address labels needed, the order is obvious." And * is the parcels dock rule — "every package needs an explicit address label, direction matters too much to guess."
That's the mental model. Now — why do sku and quantity belong in the positional-only zone? Why not leave them as normal parameters?
Because the order is semantically unambiguous. SKU is the identity of the product. Quantity is how many. Nobody would flip those. Adding sku= and quantity= labels at every call site would be noise, not clarity.
And the warehouse strings are the opposite — they're both str, they look identical structurally. If you pass them positionally, the chance of getting the direction backwards is roughly fifty percent. The keyword enforces the direction in the signature itself.
I had a function in Track 1 with start_date and end_date both as plain positional strings. I flipped them at least once. The report came back empty and I spent an hour figuring out why.
Keyword-only markers would have caught that at runtime the moment you tried to pass them in the wrong order without naming them. Five seconds instead of an hour.
Do I need both markers together? What if I only want keyword-only without the positional-only restriction?
Use just *. A bare * in the signature marks the keyword-only zone without positional-only. You've already seen this implicitly — anything after *args is keyword-only. A bare * does the same enforcement without capturing any extra positional arguments:
def reserve_stock(sku: str, *, warehouse: str, days: int = 7) -> dict:
return {"sku": sku, "warehouse": warehouse, "reserved_days": days}
reserve_stock("SKU-001", warehouse="Phoenix") # correct — days defaults
reserve_stock("SKU-001", warehouse="Dallas", days=14) # both keyword args
reserve_stock("SKU-001", "Phoenix", 14) # TypeError — keyword-onlyAnd sorted() uses this — sorted(iterable, *, key=None, reverse=False). The bare * is why I can't do sorted(products, len) — key is keyword-only.
You've been following that rule without knowing it existed. The standard library uses / and * throughout. Now you can read those signatures and know exactly what's required.
When do I reach for this versus just leaving everything as normal parameters?
Two situations. First: parameters that are stable and positional by obvious convention — like SKU and quantity — where keyword labels would be noise. Use / to lock in that positional contract. Second: parameters that look similar enough at the call site to cause direction errors — like two warehouse strings. Use * to require naming. Most functions need neither. Precision tools, not defaults.
Write move_stock. I've got the shape.
Go. Tomorrow is lambda — the temporary label printer. You've already used it without understanding it. I want that to change.
Python 3.8 introduced two positional markers in function signatures: / ends the positional-only zone, * begins the keyword-only zone. Both are separators — they are not parameter names.
def move_stock(sku: str, quantity: int, /, *, from_warehouse: str, to_warehouse: str) -> dict:
return {"sku": sku, "quantity": quantity, "from": from_warehouse, "to": to_warehouse}
# Positional-only — sku and quantity must be passed by position
move_stock("SKU-1001", 50, from_warehouse="Phoenix", to_warehouse="Dallas") # correct
# TypeError: cannot pass positional-only args as keywords
move_stock(sku="SKU-1001", quantity=50, from_warehouse="Phoenix", to_warehouse="Dallas")
# TypeError: warehouse args cannot be passed positionally
move_stock("SKU-1001", 50, "Phoenix", "Dallas")Zone summary:
| Zone | Marker | How caller must pass |
|---|---|---|
Before / | positional-only | Position only — keyword label forbidden |
Between / and * | normal | Either position or keyword |
After * | keyword-only | Keyword only — positional forbidden |
Bare * without /: When you only want keyword-only enforcement (no positional-only restriction), use a bare * alone. Any parameter after it must be named at the call site.
def reserve_stock(sku: str, *, warehouse: str, days: int = 7) -> dict:
return {"sku": sku, "warehouse": warehouse, "reserved_days": days}
reserve_stock("SKU-001", warehouse="Phoenix") # correct
reserve_stock("SKU-001", "Phoenix") # TypeError — keyword-onlyThe standard library uses both markers extensively. sorted(iterable, *, key=None, reverse=False) is the canonical bare-* example — it prevents sorted(products, len) from silently passing len as the key.
Pitfall 1: Forgetting that / and * are separators, not parameters. Newcomers read def f(a, /, b, *, c) and expect / and * to capture arguments. They don't — they only enforce call-site rules for the parameters around them.
Pitfall 2: Overusing these markers. Most functions need neither. Use / when the parameter order is semantically obvious and keyword labels add noise. Use * when two or more parameters have the same type and a caller could silently flip their values. Applying them everywhere produces unreadable signatures.
Pitfall 3: Confusing bare * with *args. A bare * in a signature does not collect extra positional arguments — it only marks the keyword-only zone. *args collects all remaining positional arguments into a tuple. They look similar but behave differently.
def f(a, *, b): # bare * — b must be keyword, no extra positional args collected
pass
def g(a, *args, b): # *args — extra positionals collected; b must be keyword
pass/ and * also interact with *args and **kwargs in the same signature — advanced APIs combine all four for maximum call-site control. The CPython source uses / heavily in built-in functions like len(), range(), and isinstance() — their signatures in help() show / at the end of parameters that are positional-only by C implementation necessity. Reading those signatures fluently is a skill that separates intermediate from advanced Python developers.