Positional & Keyword Arguments
Configure required parameters, named keyword calls, default values, and avoid the mutable default trap.
01 / Mental Model: Mail Slots, Named Tags & The Mutable Trap
Think of a function's parameter signature as a contract specifying incoming mail delivery slots. Positional arguments are like packages delivered strictly by slot order (slot 1, slot 2). Keyword arguments are packages addressed with explicit recipient tags (e.g. to: 'port'), allowing them to arrive in any sequence. Default values provide pre-filled package contents if a slot is left vacant.
Function Signature: def configure(host, port=80, *, ssl=True):
▲ ▲ ▲
│ │ └─ Keyword-Only (must be named)
│ └─ Optional with Default (80)
└─ Required Positional
Call 1: configure('api.internal') => host='api.internal', port=80, ssl=True
Call 2: configure('api.internal', 443) => host='api.internal', port=443, ssl=True
Call 3: configure('api.internal', ssl=False) => host='api.internal', port=80, ssl=False
Call 4: configure(port=8080, host='10.0.0.1') => host='10.0.0.1', port=8080, ssl=TrueAt the call site, positional arguments must strictly precede any keyword arguments. Keyword arguments can be reordered freely.
02 / Syntax Anatomy: Positional-Only, Defaults, *args & **kwargs
# [1] [2] [3] [4] [5] [6] [7]
def request(verb, /, timeout=30, *headers, auth, debug=False, **extra):
"""Standard HTTP request dispatcher with structured signature."""Positional-only parameter (preceding /). Cannot be called as verb='GET'.
PEP 570 positional boundary. All prior parameters are positional-only.
Standard parameter with default value stored in __defaults__.
Variadic positional arguments gathered into an immutable tuple.
Required keyword-only argument (following *headers). Must be named.
Keyword-only with default stored in __kwdefaults__.
Variadic keyword arguments collected into a mutable dict.
03 / Under the Hood: CPython Argument Binding Protocol (Objects/call.c)
CPython inspects co_argcount and maps positional arguments sequentially into the newly provisioned frame's fastlocals array (fastlocals[0 .. N-1]). Positional-only parameters (co_posonlyargcount) are validated to ensure no keyword name attempts to bind to them.
fastlocals[0] = evaluated_arg_0 fastlocals[1] = evaluated_arg_1
04 / Comparison Matrix: Parameter Flavors & Call Strategies
| Parameter Type | Matching Mechanism | Call Syntax | Order Dependency | Primary Advantage |
|---|---|---|---|---|
| Positional | Index order in argument list | fn(1, 2) | Strictly ordered | Fastest CPU execution; concise |
| Keyword | Parameter name lookup | fn(b=2, a=1) | Order-independent | Self-documenting; safe refactoring |
05 / Syntax Reference: Parameter Signatures & Constraints
| Parameter Type | Syntax | Definition Order | Call Syntax | Notes |
|---|---|---|---|---|
| Positional-Only | def f(a, b, /): | First, before '/' | f(1, 2) (cannot use a=1) | Introduced in Python 3.8 (PEP 570); callers cannot specify argument name. |
| Standard Positional / Keyword | def f(a, b=0): | After '/', before '*' | f(1, 2) or f(a=1, b=2) | Most common parameter type; default parameters must follow non-defaults. |
| Variadic Positional (*args) | def f(*args): | After standard parameters, before keyword-only | f(1, 2, 3, 4) | Gathers surplus positional arguments into an immutable tuple. |
| Keyword-Only | def f(*, key=val): or def f(*args, key=val): | After '*' or '*args', before '**kwargs' | f(key=val) (cannot pass positionally) | Prevents callers from relying on argument position; stored in __kwdefaults__. |
| Variadic Keyword (**kwargs) | def f(**kwargs): | Must be the very last parameter | f(x=1, y=2, z=3) | Gathers all surplus keyword arguments into a mutable dictionary. |
06 / Core Code Walkthroughs: 7 Argument Design Patterns
Positional vs. Keyword Arguments and Mixing Rules
Calling functions using positional ordering, named keywords, and adhering to Python's ordering constraints.
def configure_server(host, port, ssl_enabled, timeout=30):
return f'Connecting to {host}:{port} (SSL: {ssl_enabled}, Timeout: {timeout}s)'
# 1. Pure positional call (matched by order)
res1 = configure_server('10.0.0.1', 8080, True)
print('Positional:', res1)
# 2. Pure keyword call (order does not matter!)
res2 = configure_server(port=443, ssl_enabled=True, host='api.service.io', timeout=60)
print('Keyword:', res2)
# 3. Mixed call: positional arguments MUST precede keyword arguments
res3 = configure_server('127.0.0.1', 3000, ssl_enabled=False)
print('Mixed:', res3)
07 / Interactive Playground: Keyword Arguments & Sentinel Isolation
Run and modify the code below to see the difference between shared mutable default arguments and fresh sentinel allocations.
# 1. Test Sentinel Pattern vs Shared Default
def register_student(name, grades=None):
if grades is None:
grades = []
grades.append(100)
return {"name": name, "grades": grades}
s1 = register_student("Zara")
s2 = register_student("Kabir")
print("Student 1:", s1)
print("Student 2 (cleanly isolated!):", s2)
# 2. Test Argument Unpacking (*args, **kwargs)
def send_notification(user, message, *, priority="normal", **metadata):
return {
"to": user,
"body": message,
"priority": priority,
"meta": metadata
}
user_data = {"priority": "urgent", "channel": "sms", "retry": True}
notif = send_notification("alice@py.io", "Server down", **user_data)
print("Notification payload:", notif)08 / Common Pitfalls & Anti-Patterns
🟢 Beginner Pitfall: The Mutable Default Argument Trap
Default parameter values are evaluated once when the function is defined, not on each function call. Using a mutable object like a list or dictionary as a default causes that object to be shared across all calls that omit the argument, leaking state across calls.
def record_event(event_id, event_log=[]):
event_log.append(event_id)
return event_log
print(record_event(1)) # [1]
print(record_event(2)) # [1, 2] (Shared state bug!)
def record_event(event_id, event_log=None):
if event_log is None:
event_log = []
event_log.append(event_id)
return event_log
print(record_event(1)) # [1]
print(record_event(2)) # [2] (Cleanly isolated!)
🟢 Beginner Pitfall: Placing Positional Arguments After Keyword Arguments
At the call site, all positional arguments must precede any keyword arguments. Placing a positional argument after a keyword argument is a compile-time SyntaxError: positional argument follows keyword argument.
def send_email(to, subject, body):
return f'{to}: {subject}'
# SyntaxError: positional argument follows keyword argument
# send_email(to='user@corp.io', 'Urgent', 'Meeting at 2pm')
def send_email(to, subject, body):
return f'{to}: {subject}'
# Correct: all positional first, or all keyword
send_email('user@corp.io', 'Urgent', body='Meeting at 2pm')
🟡 Intermediate Pitfall: Passing Multiple Values for the Same Parameter
If you provide a value positionally for a parameter, and also provide an explicit keyword argument for the same parameter name, Python raises TypeError: function() got multiple values for argument.
def greet(name, greeting='Hello'):
return f'{greeting}, {name}'
try:
# 'Alice' fills name positionally; name='Bob' attempts to fill it again!
greet('Alice', name='Bob')
except TypeError as e:
print('Multiple values error:', e)
def greet(name, greeting='Hello'):
return f'{greeting}, {name}'
print(greet('Alice', greeting='Hi')) # Distinct parameters
🟡 Intermediate Pitfall: Defining Default Parameters Before Non-Default Parameters
In a function definition, non-default parameters cannot follow default parameters. If a parameter with a default value were defined before a required parameter, Python would not know how to match positional arguments.
# SyntaxError: non-default argument follows default argument # def query(timeout=30, sql): # pass
# Correct: required non-default parameters come first
def query(sql, timeout=30):
pass
09 / Edge Cases: Empty Variadics, Non-String Kwargs & Bare Asterisks
Empty *args and **kwargs Collections
When no surplus positional or keyword arguments are supplied, *args evaluates to an empty tuple () and **kwargs evaluates to an empty dictionary {}. They are never None.
def inspect_variadic(*args, **kwargs):
print('args is tuple?', isinstance(args, tuple), 'len:', len(args))
print('kwargs is dict?', isinstance(kwargs, dict), 'len:', len(kwargs))
inspect_variadic()
Unpacking Non-String Keys in **kwargs
When unpacking a dictionary with **kwargs, all dictionary keys must be strings. If any dictionary key is an integer or non-string, Python raises TypeError: function() keywords must be strings.
def test_func(**kwargs):
pass
invalid_dict = {1: 'one', 2: 'two'}
try:
test_func(**invalid_dict)
except TypeError as exc:
print('Kwargs error:', exc)
Forwarding Exact Signatures with *args and **kwargs
When building wrapper functions or decorators, forwarding *args and **kwargs transparently preserves all positional and keyword arguments to the wrapped function.
def target(x, y, multiplier=1):
return (x + y) * multiplier
def wrapper(*args, **kwargs):
print('Wrapper intercepted call')
return target(*args, **kwargs)
print('Result:', wrapper(3, 4, multiplier=2))
Bare Asterisk (*) for Pure Keyword-Only Arguments
A bare asterisk (*) in the parameter list does not bind to a variable; it acts as a delimiter indicating that all following parameters MUST be passed as keyword arguments.
def create_table(name, *, primary_key='id', auto_increment=True):
return f'Table {name} (PK: {primary_key})'
print(create_table('users', primary_key='uuid'))
10 / Memory & Performance Trade-Offs
Positional vs. Keyword Resolution Speed
Positional arguments map directly to fastlocals array offsets in O(1) time without name resolution. Keyword arguments require hashing and comparing parameter names, incurring slight overhead in high-frequency loops.
Variadic Allocation Overhead (*args & **kwargs)
*args allocates a new tuple and **kwargs allocates a new dictionary on each call. If performance is paramount in tight inner loops, explicit named parameters avoid these heap allocations.
Default Values Storage Efficiency
Function default values reside in a single tuple (__defaults__) allocated once per function definition, consuming negligible memory regardless of how many times the function is invoked.
PEP 570 Positional-Only Throughput
Positional-only parameters (marked with /) allow Python's internal C vectorcall protocol to bypass keyword dictionary inspection completely, yielding fastest possible invocation throughput.
11 / Step-by-Step Problem Solving: Designing Clean Public APIs
Never place parameters with defaults before required parameters. Anchor your function's core inputs (e.g. url, query) as leading positional arguments.
def fetch_resource(endpoint: str, key: str):
pass12 / Real-World Applications & Production Architecture
Configurable HTTP Client Requests (requests / httpx)
Modern HTTP client libraries define signatures like get(url, params=None, headers=None, **kwargs). Surplus keyword arguments (**kwargs) are passed through to underlying connection and timeout handlers transparently.
Flexible Logging and Analytics Trackers
Event tracking SDKs (e.g. telemetry trackers) define track(event_name, *tags, **metadata), allowing callers to record arbitrary metric tags and contextual payload properties without changing SDK method signatures.
Parallel Batch Computation with zip() and map()
Scientific and financial computation engines combine zip() and map() to stream multi-feature vectors into model scoring functions with zero-overhead argument unpacking.
13 / Interactive Quiz Booster (17 Questions)
def welcome(name, greeting='Hello'):
print(greeting, name)
welcome('Kai')def show(item, *extras):
print(extras)
show('book', 'pen', 'card')def introduce(name, age):
print(name, age)
introduce(age=12, name='Mina')def add_item(item, items=[]):
items.append(item)
return items
print(add_item('tea'))
print(add_item('coffee'))14 / Guided Practice Labs & Test Suites
Server Configuration Builder with Options
Write a function make_server(host='127.0.0.1', port=8000, **options) that returns a dictionary containing 'host', 'port', and any additional keyword options passed into the function.
def make_server(host='127.0.0.1', port=8000, **options):
# Return dictionary with host, port, and options
pass
assert make_server() == {'host': '127.0.0.1', 'port': 8000}
assert make_server(port=9000, ssl=True) == {'host': '127.0.0.1', 'port': 9000, 'ssl': True}
assert make_server('0.0.0.0', 5000) == {'host': '0.0.0.0', 'port': 5000}
Safe Accumulator with Sentinel Default
Write a function safe_append(element, collection=None) that safely appends element to collection. If collection is omitted or None, allocate a new list so that subsequent calls do not share state. Return the updated list.
def safe_append(element, collection=None):
# Safely append without mutable default trap
pass
a = safe_append('x')
b = safe_append('y')
assert a == ['x'] and b == ['y']
shared = [1, 2]
assert safe_append(3, shared) == [1, 2, 3]
assert shared == [1, 2, 3]
Multi-Sequence Dot Product via zip() and Unpacking
Write a function dot_product(vec_a, vec_b) that computes the dot product of two numerical vectors (sum of products of corresponding elements) using zip() and argument unpacking. If lengths differ, raise a ValueError.
def dot_product(vec_a, vec_b):
# Compute dot product or raise ValueError if lengths differ
pass
assert dot_product([1, 2, 3], [4, 5, 6]) == 32
assert dot_product([2, 0], [0, 5]) == 0
try:
dot_product([1], [1, 2])
assert False, 'Should raise ValueError'
except ValueError:
pass
15 / Debugging Scenarios: Production Post-Mortems
State Leakage Across API Requests in Flask/FastAPI Endpoint
Every new user registration received the validation errors of all previously registered users.
The validation helper was declared as def validate_user(data, errors=[]):. The default errors list persisted across HTTP requests.
Change signature to def validate_user(data, errors=None): with if errors is None: errors = [] to allocate a fresh list per request.
TypeError: got multiple values for argument 'format'
A utility function render_doc('report.pdf', 'pdf', format='a4') failed with a confusing TypeError.
The second parameter was named format, so passing 'pdf' filled format positionally, and format='a4' attempted to fill format again by keyword.
Use distinct parameter names (e.g. extension and page_format) or pass all arguments with matching keywords.
16 / Best Practices, Key Takeaways & Quick Cheatsheet
Engineering Best Practices
Core Mental Pillars
Quick Reference Cheatsheet
fn(a=1, b=2)Matches arguments by explicit parameter name in any order.
def f(items=None):Prevents shared memory state bug across multiple calls.
fn(*coords)Unpacks elements from list/tuple into positional slots.
fn(**config)Unpacks key-value pairs into matching named parameters.
Frequently Asked Questions
Worked example
A cinema booking: seat number is required, but the snack bundle is optional and can be passed by keyword for clarity.
def book(name, show, snack=None):
line = f"{name}: {show}"
if snack:
line += f" + {snack}"
return line
print(book("Rafi", "7pm", snack="popcorn"))
How it works, step by step
- Positional arguments fill parameters in order: name then show.
- snack=... was passed by KEYWORD, so its label makes the call self-documenting.
- The default None lets callers skip it.
Positional & Keyword Arguments practice
Write a function named repeat with parameters word and times=2. Print word repeated times times. Call repeat("go", 3).
Expected output: gogogo
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Positional & Keyword Arguments quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.