Python’s argument system is one of its most powerful features — and one of the most important to understand before working with FastAPI, which relies heavily on keyword arguments, default values, and **kwargs-style patterns for dependency injection and request parameter handling. Python gives you four kinds of arguments: positional (the order matters), keyword (name matters, not position), default (optional with a fallback value), and variadic (*args and **kwargs for any number of arguments). Understanding when and how to use each — and the strict ordering rules that govern them — will make your FastAPI route handlers clean and expressive.
Positional and Keyword Arguments
def create_post(title, body, published):
return {"title": title, "body": body, "published": published}
# Positional — order matters
create_post("My Title", "Post body here", True)
# Keyword — name matters, order flexible
create_post(body="Post body here", title="My Title", published=True)
# Mixed — positional must come before keyword
create_post("My Title", published=True, body="Post body here")
# This fails — positional after keyword
create_post(title="My Title", "Post body here", True) # SyntaxError!
# Forcing keyword-only arguments (after *)
def connect(host, port, *, timeout, retries):
# timeout and retries MUST be passed as keywords
pass
connect("localhost", 5432, timeout=30, retries=3) # ✓
connect("localhost", 5432, 30, 3) # TypeError — timeout must be keyword
def get_post(post_id: int, db: Session = Depends(get_db)):, FastAPI inspects the parameter names and types to determine what comes from the URL path, query string, request body, or dependency injection. Getting comfortable with named parameters and type hints now will make FastAPI’s “magic” feel completely natural when you reach Part 3.create_user("Alice", "alice@example.com", True, "admin") with create_user(name="Alice", email="alice@example.com", is_active=True, role="admin"). The second version is immediately understandable without reading the function signature. This is especially important in test code where argument meaning must be obvious.None as the default and create a new mutable object inside the function body when needed.Default Parameter Values
def get_posts(page=1, limit=10, published=True):
print(f"Page {page}, limit {limit}, published={published}")
get_posts() # Page 1, limit 10, published=True
get_posts(2) # Page 2, limit 10, published=True
get_posts(page=3, limit=20) # Page 3, limit 20, published=True
get_posts(1, 5, False) # Page 1, limit 5, published=False
# Default with None (safe for mutable defaults)
def create_tag(name: str, posts: list = None):
if posts is None:
posts = [] # fresh list each call ✓
return {"name": name, "posts": posts}
# Parameters with defaults MUST come after those without defaults
def bad(a=1, b): # SyntaxError: non-default argument follows default argument
pass
def good(a, b=1): # ✓ non-default before default
pass
*args — Variable Positional Arguments
# *args collects extra positional arguments into a tuple
def add_all(*numbers):
print(type(numbers)) # <class 'tuple'>
return sum(numbers)
add_all(1, 2, 3) # 6
add_all(1, 2, 3, 4, 5) # 15
add_all() # 0 — empty tuple
# Mix regular and *args — regular params must come first
def log(level, *messages):
for msg in messages:
print(f"[{level}] {msg}")
log("INFO", "Server started", "Listening on port 8000")
# [INFO] Server started
# [INFO] Listening on port 8000
# Unpack a list/tuple with * when calling
numbers = [1, 2, 3, 4, 5]
print(add_all(*numbers)) # same as add_all(1, 2, 3, 4, 5)
**kwargs — Variable Keyword Arguments
# **kwargs collects extra keyword arguments into a dict
def create_user(**fields):
print(type(fields)) # <class 'dict'>
return fields
create_user(name="Alice", email="alice@example.com", role="admin")
# {"name": "Alice", "email": "alice@example.com", "role": "admin"}
# Mix regular params and **kwargs
def update_post(post_id: int, **updates):
print(f"Updating post {post_id} with: {updates}")
update_post(42, title="New Title", published=True)
# Updating post 42 with: {"title": "New Title", "published": True}
# Unpack a dict with ** when calling
data = {"name": "Alice", "email": "alice@example.com"}
create_user(**data) # same as create_user(name="Alice", email="alice@example.com")
# FastAPI pattern: forward kwargs to another function
def base_query(db, **filters):
query = db.query(Post)
for field, value in filters.items():
query = query.filter(getattr(Post, field) == value)
return query
Argument Ordering Rules
# The correct order for all argument types:
# def func(positional, *args, keyword_only, **kwargs):
def full_example(a, b, *args, keyword_only=True, **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"keyword_only={keyword_only}")
print(f"kwargs={kwargs}")
full_example(1, 2, 3, 4, keyword_only=False, x=10, y=20)
# a=1, b=2
# args=(3, 4)
# keyword_only=False
# kwargs={"x": 10, "y": 20}
# Positional-only parameters (Python 3.8+) — use /
def strict_pos(a, b, /):
# a and b can ONLY be passed positionally
pass
strict_pos(1, 2) # ✓
strict_pos(a=1, b=2) # TypeError — must be positional
Common Mistakes
Mistake 1 — Mutable default argument
❌ Wrong — shared mutable default persists between calls:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("python")) # ["python"]
print(add_tag("fastapi")) # ["python", "fastapi"] — unexpected!
✅ Correct — use None and initialise inside:
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags # ✓ fresh list every call
Mistake 2 — Wrong argument order (positional after keyword)
❌ Wrong — positional argument after keyword argument:
create_post(title="My Post", "Body text", True) # SyntaxError
✅ Correct — keyword arguments must come after positional:
create_post("My Post", "Body text", published=True) # ✓
Mistake 3 — Default before non-default parameter
❌ Wrong — default parameter before non-default:
def connect(timeout=30, host): # SyntaxError
✅ Correct — non-default parameters first:
def connect(host, timeout=30): # ✓
Quick Reference
| Type | Syntax | Receives |
|---|---|---|
| Positional | def f(a, b) |
By position |
| Default | def f(a, b=10) |
Optional, has fallback |
| Keyword-only | def f(a, *, b) |
Must use keyword |
| Variadic positional | def f(*args) |
Tuple of extra positional |
| Variadic keyword | def f(**kwargs) |
Dict of extra keyword |
| Unpack list | f(*my_list) |
Spread as positional args |
| Unpack dict | f(**my_dict) |
Spread as keyword args |