Every Python developer eventually hits the same wall. You call a function, pass in a handful of values, run the code — and something comes out wrong. You swapped width and height, or got source and destination mixed up. The output is garbage, and Python did not warn you. It just did what you told it. Keyword arguments fix that.
What Keyword Arguments Actually Are
When you call a Python function, you can pass values in two fundamentally different ways. The first is positional: you hand the function a sequence of values, and Python matches them to parameters in the order they appear in the function definition. The second is by name — keyword arguments — where you explicitly state which parameter each value belongs to.
The syntax is simple: parameter_name=value at the point of the function call. That equals sign decouples the value you are passing from the position you are passing it in. As long as the name matches a parameter in the function definition, Python routes the value correctly.
Consider this function, drawn directly from Python's official documentation and familiar to anyone who has worked through the tutorial:
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
print("-- This parrot wouldn't", action, end=' ')
print("if you put", voltage, "volts through it.")
print("-- Lovely plumage, the", type)
print("-- It's", state, "!")
This function has one required parameter (voltage) and three optional ones (state, action, and type). The optional ones have default values, which means Python uses those defaults if you do not supply those arguments.
All of the following calls are valid:
parrot(1000)
parrot(voltage=1000)
parrot(voltage=1000000, action='VOOOOOM')
parrot('a thousand', state='pushing up the daisies')
parrot('a million', 'bereft of life', 'jump')
And here are calls Python will reject:
parrot() # Missing required argument: voltage
parrot(voltage=5.0, 'dead') # Positional argument follows keyword argument
parrot(110, voltage=220) # voltage was given twice
parrot(actor='John Cleese') # Unexpected keyword argument
Python enforces a clear contract: keyword arguments cannot be followed by positional arguments in a call, you cannot supply the same parameter twice, and you cannot use a keyword that does not match any parameter the function defines.
The Order Rules
Python's grammar for function calls follows a specific precedence. Required positional parameters come first. Then default parameters. Then *args for variable positional input. Then keyword-only parameters (defined after *args). Then **kwargs for variable keyword input.
When you call a function, Python's argument-matching algorithm works like this:
- Positional arguments are assigned left to right to parameters that have not yet been filled.
- Keyword arguments are matched by name.
- If any required parameter is still unfilled after both steps, Python raises a
TypeError. - If any keyword argument does not match a parameter name (and the function does not accept
**kwargs), Python raises aTypeError.
This process happens at the CPython level through the function's code object. Understanding this helps explain why keyword arguments have essentially no runtime overhead compared to positional arguments — the matching logic runs once at call time regardless of approach.
Default Values: Where Things Get Tricky
One of the most common Python gotchas involves mutable default values. Consider this seemingly innocent function:
def add_item(item, collection=[]):
collection.append(item)
return collection
Every call to add_item that does not pass a collection argument shares the same list object. Default values in Python are evaluated once, at function definition time — not each time the function is called. So that [] is created once and reused permanently.
Never use mutable objects (lists, dicts, sets) as default parameter values. They are shared across all calls that use the default, which leads to subtle bugs that are hard to track down.
The correct pattern is:
def add_item(item, collection=None):
if collection is None:
collection = []
collection.append(item)
return collection
When your function accepts optional parameters that might be mutable, always default to None and initialize inside the function body.
Real-World Application: Data Pipelines
Here is a function you might actually write in a data engineering context. Notice how keyword arguments make both the definition and the call self-documenting:
def load_dataset(
filepath,
delimiter=',',
encoding='utf-8',
skip_rows=0,
max_rows=None,
parse_dates=False,
na_values=None
):
import csv
results = []
with open(filepath, encoding=encoding, newline='') as f:
reader = csv.DictReader(f, delimiter=delimiter)
for i, row in enumerate(reader):
if i < skip_rows:
continue
if max_rows is not None and i >= skip_rows + max_rows:
break
results.append(row)
return results
When you call this in production, the call reads like a specification:
records = load_dataset(
'/data/sales_q4.csv',
delimiter='\t',
encoding='latin-1',
skip_rows=2,
max_rows=10000
)
Compare that to the positional equivalent:
records = load_dataset('/data/sales_q4.csv', '\t', 'latin-1', 2, 10000, False, None)
This is not a subtle difference. The positional version is a maintenance hazard: if someone adds a new parameter before max_rows, every existing positional call silently breaks or produces wrong results. The keyword version is structurally immune to that class of bug.
Real-World Application: API Wrappers
Keyword arguments are practically mandatory when wrapping HTTP APIs. Here is a simplified but realistic pattern:
def send_request(
endpoint,
method='GET',
payload=None,
headers=None,
timeout=30,
retries=3,
verify_ssl=True,
auth_token=None
):
import urllib.request
import json
url = f"https://api.example.com/{endpoint}"
req_headers = {'Content-Type': 'application/json'}
if auth_token:
req_headers['Authorization'] = f'Bearer {auth_token}'
if headers:
req_headers.update(headers)
data = json.dumps(payload).encode() if payload else None
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
return urllib.request.urlopen(req, timeout=timeout)
Callers can now express exactly what they need without having to specify everything:
# Simple read — only the required argument
response = send_request('users/42')
# Authenticated POST with a custom timeout
response = send_request(
'orders',
method='POST',
payload={'product_id': 99, 'quantity': 2},
auth_token='abc123xyz',
timeout=10
)
Design your API wrapper functions with keyword arguments from the start. The function signature can evolve — new parameters can be added with defaults — without breaking any existing callers, as long as the existing parameter names remain stable.
Real-World Application: Machine Learning Configuration
Data science code is notorious for long parameter lists. Keyword arguments are the standard way configuration flows through training loops, preprocessing functions, and evaluation pipelines.
def train_model(
X_train,
y_train,
learning_rate=0.001,
epochs=100,
batch_size=32,
dropout_rate=0.3,
optimizer='adam',
loss_function='categorical_crossentropy',
validation_split=0.2,
early_stopping=True,
patience=10,
verbose=1
):
config = {
'lr': learning_rate,
'epochs': epochs,
'batch': batch_size,
'dropout': dropout_rate
}
print(f"Training with config: {config}")
# ... training logic
When researchers run experiments, they tweak specific parameters while keeping everything else at baseline:
# Baseline run — all defaults apply
train_model(X_train, y_train)
# Higher learning rate experiment
train_model(X_train, y_train, learning_rate=0.01, epochs=200)
# Lower dropout, no early stopping
train_model(X_train, y_train, dropout_rate=0.1, early_stopping=False)
This pattern makes experiment logs meaningful. When you save the arguments used in a run, you have a complete, readable record of exactly what changed between experiments.
The **kwargs Pattern: Variable Keyword Arguments
Sometimes you want to accept keyword arguments without declaring them explicitly in the function signature. The **kwargs syntax handles this by collecting all unmatched keyword arguments into a dictionary.
def create_html_element(tag, content='', **kwargs):
attributes = ' '.join(f'{k}="{v}"' for k, v in kwargs.items())
if attributes:
return f'<{tag} {attributes}>{content}</{tag}>'
return f'<{tag}>{content}</{tag}>'
# Pass any HTML attributes you need
print(create_html_element('a', 'Click here', href='https://pythoncodecrack.com', target='_blank'))
# <a href="https://pythoncodecrack.com" target="_blank">Click here</a>
print(create_html_element('input', type='text', id='username', placeholder='Enter name'))
# <input type="text" id="username" placeholder="Enter name"></input>
Inside the function, kwargs is a regular Python dictionary. One particularly useful pattern is forwarding keyword arguments to another function:
import json
def pretty_print_json(data, **kwargs):
"""Wrapper around json.dumps that always uses pretty formatting."""
defaults = {'indent': 4, 'sort_keys': True}
defaults.update(kwargs)
print(json.dumps(data, **defaults))
pretty_print_json({'name': 'Alice', 'age': 30})
pretty_print_json({'name': 'Alice', 'age': 30}, indent=2, ensure_ascii=False)
The **defaults syntax at the call site unpacks the dictionary as keyword arguments — the same unpacking operator applied in reverse. Instead of collecting keyword arguments into a dict, it expands a dict into keyword arguments.
Keyword-Only Arguments
Python 3 introduced a powerful refinement: keyword-only parameters. These can only be supplied as keyword arguments, never positionally. You define them by placing them after a bare * in the function signature:
def process_file(filepath, *, encoding='utf-8', overwrite=False, verbose=False):
print(f"Processing {filepath} with encoding={encoding}, overwrite={overwrite}")
The bare * tells Python: everything after this point is keyword-only. This call will fail:
process_file('data.csv', 'latin-1', True) # TypeError
This call is required:
process_file('data.csv', encoding='latin-1', overwrite=True)
This is a deliberate API design choice. It forces callers to be explicit about optional behavior, reducing bugs in long-lived codebases where someone might not remember what the third positional argument does six months later. This pattern appears throughout the Python standard library — sorted() requires key and reverse to be passed as keywords, and print() requires end, sep, file, and flush to be keyword arguments. This is not an accident. It is a readability constraint baked into the language by design.
Common Mistakes and How to Avoid Them
Once you switch to keyword arguments in a function call, every subsequent argument in that call must also be a keyword argument. func(a=1, 2) is a syntax error — Python catches this at parse time before the code ever runs.
Passing a value both positionally and by name for the same parameter raises a TypeError. If voltage is the first parameter, then parrot(10, voltage=10) will always fail with a "got multiple values" error.
If you misspell a keyword argument and the function does not accept **kwargs, Python raises TypeError: unexpected keyword argument. This is actually helpful — it surfaces typos immediately rather than silently using a wrong default.
Since Python 3.6, **kwargs preserves the order in which keyword arguments were passed, as specified by PEP 468. In Python 3.7, insertion-order preservation became a guaranteed part of the language specification for all dict objects — not just an implementation detail of CPython. Arguments arrive in the order the caller specified them. This is reliable in any supported Python version, but do not write functions that depend on the order of **kwargs entries as part of their API contract — that ordering reflects the call site, not any semantic promise your function makes.
The complete correct order for function parameters is: standard positional parameters, parameters with defaults, *args, keyword-only parameters, then **kwargs. Getting this order wrong produces a SyntaxError at parse time — Python will not attempt to run code with parameters in the wrong order.
def complex_function(required, optional='default', *args, kw_only=True, **kwargs):
print(f"required: {required}")
print(f"optional: {optional}")
print(f"args: {args}")
print(f"kw_only: {kw_only}")
print(f"kwargs: {kwargs}")
complex_function('hello', 'world', 1, 2, 3, kw_only=False, extra='data')
# required: hello
# optional: world
# args: (1, 2, 3)
# kw_only: False
# kwargs: {'extra': 'data'}
The Other Side of the Coin: Positional-Only Parameters
Keyword-only parameters (the * separator) force callers to name arguments. Python 3.8 introduced the mirror image: positional-only parameters (the / separator, standardized in PEP 570), which prohibit callers from using a keyword at all.
def move(x, y, /, speed=1.0):
# x and y are positional-only — you cannot call move(x=3, y=4)
print(f"Moving to ({x}, {y}) at speed {speed}")
This matters more than it first appears. When you mark a parameter positional-only, you decouple its internal name from your public API. You can rename x to coord_x internally without breaking any caller, because no caller is allowed to write move(x=3). The Python standard library uses this extensively — len(obj, /), pow(base, exp, mod=None, /), and range() are all positional-only, which is why len(obj=mylist) raises a TypeError.
The full, complete parameter ordering rule — which the official Python documentation describes as the grammar for a function definition — is therefore:
def function(
positional_only, # before /
/,
positional_or_keyword, # ordinary parameters
*args, # variable positional
keyword_only, # after *
**kwargs # variable keyword
):
pass
Understanding this taxonomy in full means you can read any function signature in the standard library or in popular third-party packages and immediately know exactly what calling conventions are allowed. The / and * sentinels are the grammar of intentional API design, not obscure syntax for advanced programmers.
Use / when a parameter's name has no semantic meaning to callers (e.g., coordinate inputs, math operands). Use * when the parameter name is the whole point — configuration options, flags, and anything that reads as a sentence at the call site. The two are not in competition; they solve different expressivity problems.
Typing **kwargs: From Flexible to Precise
One legitimate criticism of **kwargs is that it erases type information. A function signature of def configure(**kwargs) tells type checkers and IDEs nothing about what keys are valid or what types their values should be. This is where PEP 692 (accepted in Python 3.12) becomes useful.
from typing import TypedDict, Unpack
class TrainConfig(TypedDict):
learning_rate: float
epochs: int
batch_size: int
def train_model(X_train, y_train, **kwargs: Unpack[TrainConfig]) -> None:
config = dict(kwargs)
print(f"Training with: {config}")
The Unpack[TrainConfig] annotation instructs a type checker like mypy or Pyright that the keyword arguments collectively must conform to the TrainConfig TypedDict structure. Valid key names and their expected types are now statically verifiable without losing the flexibility of **kwargs at runtime. This resolves the long-standing trade-off between flexibility and safety in function interfaces.
For production codebases running Python 3.12 and above, this pattern is the considered approach for functions that genuinely need open-ended keyword arguments but also benefit from tooling support. It threads the needle between the rigid explicit signature and the opaque **kwargs catchall.
What Callers Actually Experience
A gap that documentation rarely addresses: keyword arguments change the experience of calling code, not just of writing functions. When a developer is reading a call site six months later — in a code review, a stack trace, or a log statement — the presence or absence of keyword arguments shapes how quickly they can reconstruct intent.
Consider this call to a real-world function from the requests library, a widely used HTTP client for Python:
import requests
# Opaque — what does True mean here?
response = requests.get('https://api.example.com/data', None, True, None, None, 30, None, False)
# Self-documenting — intent is clear without reading the source
response = requests.get(
'https://api.example.com/data',
verify=True,
timeout=30,
allow_redirects=False
)
The keyword version is not just easier to read — it is harder to get wrong. Transposing timeout and verify in the keyword call produces a TypeError at the type-checking stage or a clear runtime error. Transposing them positionally produces silent wrong behavior: an integer where a boolean is expected, or vice versa. The cost of the extra characters is paid once; the readability dividend compounds indefinitely.
This is also why the Python documentation's guidance on the subject is worth internalizing: the official tutorial notes that keyword form "allows the arguments to be passed in any order," and the PEP 736 authors wrote that keyword arguments "increase readability and minimise the risk of inadvertent transposition." That PEP, currently in draft, proposes shorthand syntax f(x=) for the common pattern where a variable name matches the parameter name — a sign that the Python community is actively investing in making keyword-style calling even less verbose to write.
Key Takeaways
- Keyword arguments eliminate positional guessing. Naming your arguments at the call site means the code is self-documenting and immune to bugs caused by parameter reordering in the function definition.
- Never use mutable objects as default values. Default to
Noneand initialize mutable defaults inside the function body to avoid the shared-state gotcha. - Use
**kwargsto build extensible interfaces. Collecting arbitrary keyword arguments into a dictionary lets your functions evolve without breaking existing callers, and makes forwarding arguments to other functions clean and readable. - Keyword-only parameters are a design tool. The bare
*in a function signature forces callers to be explicit about optional behavior, which pays off in any codebase with more than one contributor or a lifespan of more than a few weeks. - Positional-only parameters are the complement. The
/separator, available since Python 3.8 via PEP 570, decouples a parameter's internal name from its public API. Use it when a parameter name carries no meaning to callers, or when you need freedom to rename internals without breaking the interface. - Type your kwargs when it matters.
Unpack[TypedDict](Python 3.12, PEP 692) gives you statically-verifiable keyword arguments without forcing an explicit signature. It is the precise tool for functions that need both flexibility and type safety. - Readability compounds over time. A call like
connect(host='db.internal', port=5432, ssl=True)communicates intent to every reader of that code, forever — in code review, in debugging, and when returning to a codebase months later.
Keyword arguments are one of those Python features that seem minor until you appreciate the full shape of what they enable: optional parameters with sensible defaults, self-documenting call sites, forward-compatible APIs, and fine-grained control over what callers must specify explicitly versus what they can leave to the function. The parrot example from Python's official documentation is deliberately whimsical, but the mechanics it demonstrates appear in every serious Python codebase. Add the / and * separators, and the picture becomes complete: Python gives you the tools to specify precisely which parameters can be named, which must be named, and which must never be named. Understanding that spectrum at a deep level — and knowing which PEPs established each piece of the grammar — pays dividends whether you are writing a small utility script, consuming a major library, or designing an interface that other developers will depend on for years.