Warming up the neural circuits...
By the end of this chapter you will:
Signatures are the user interface of your code. A well-designed function signature tells callers exactly what to pass and what they'll get back. A confusing signature causes bugs. Master the parameter model, and you'll write functions that are easy to use correctly and hard to use incorrectly.
def greet(name, greeting):
print(f"{greeting}, {name}!")
# Positional (order matters)
greet("Alice", "Hello") # Hello, Alice!
# Keyword (order doesn't matter)
greet(greeting="Hi", name="Bob") # Hi, Bob!
# Mixed (positional first, then keyword)
greet("Charlie", greeting="Hey") # Hey, Charlie!| Type | Syntax | Order matters? |
|---|---|---|
| Positional | greet("Alice", "Hello") | ✅ Yes |
| Keyword | greet(name="Alice", greeting="Hello") | ❌ No |
| Mixed | greet("Alice", greeting="Hello") | Positional first |
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice!
greet("Bob", "
def sum_all(*args):
return sum(args)
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) #
def create_user(name, *, age, email):
print(f"{name}, {age}, {email}")
create_user("Alice", age=30, email
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) #
def func(pos_only, /, normal, *args, keyword_only, **kwargs):
↑ ↑ ↑ ↑ ↑
Must be Can be Extra Must be Extra
positional either positional keyword keyword| Mistake | Why it's wrong | Fix |
|---|---|---|
def f(items=[]) | Mutable default shared across calls | def f(items=None): items = items or [] |
def f(x, y=10, z) | Default before non-default | def f(x, z, y=10) |
global x inside function | Modifies module-level variable | Use nonlocal for enclosing scope |
*args and **kwargs everywhere | Hides signature | Use explicit parameters |
x = "global"; def f(): x = "local"; print(x); f()nonlocal to modify a variable in the enclosing scope.Beginner:
"What's the difference between *args and **kwargs?"
*argscaptures extra positional arguments as a tuple.**kwargscaptures extra keyword arguments as a dict. Use*argswhen you don't know how many positional args will be passed; use**kwargsfor optional keyword settings.
"Why is def f(items=[]) dangerous?"
The default list is created once when the function is defined, not each time it's called. All calls share the same list, so modifications persist. Fix: use
items=Noneand create a new list inside the function.
Senior:
"What's the LEGB rule?"
Python looks up variables in order: Local (current function), Enclosing (outer function), Global (module level), Built-in (Python built-ins). If not found in any scope, raises NameError.
*args captures extra positional as tuple. **kwargs captures extra keyword as dict.nonlocal to modify enclosing scope, global to modify module scope.def func(pos_only, /, normal, *args, keyword_only, **kwargs):
├── Positional → order matters
├── Keyword → name=value
├── Default → def f(x=10)
├── *args → tuple of extra positional
├── **kwargs → dict of extra keyword
└── LEGB → Local, Enclosing, Global, Built-inWhat does *args capture?
Key: Defaults are evaluated once, when the function is defined. Mutable defaults (lists, dicts) are shared across calls.
| Pattern | Syntax | Captures |
|---|---|---|
*args | Extra positional | Tuple |
**kwargs | Extra keyword | Dict |
Warning: Use *args and **kwargs sparingly — they hide the function's signature.
Use case: Prevent positional mistakes. create_user("Alice", 30, "email") is confusing — create_user("Alice", age=30, email="email") is clear.
| Scope | Where | Example |
|---|---|---|
| Local | Inside current function | x = "local" |
| Enclosing | Inside outer function | x = "enclosing" |
| Global | Module level | x = "global" |
| Built-in | Python built-ins | print, len |
Modifying outer scope:
def counter():
count = 0
def increment():
nonlocal count # modify enclosing scope
count += 1
return count
return increment
c = counter()
print(c()) # 1
print(c()) # 2When positional arguments would be confusing. For example,
create_user("Alice", 30, "email")— which is age and which is email?create_user("Alice", age=30, email="email")is clear. Also useful for boolean flags:func(data, verbose=True)vsfunc(data, True).