Python lets you call functions in two ways — by position and by name. It also lets you set default values for parameters so callers can skip them. Together these make function calls flexible and self-documenting.
Positional arguments
When you call a function with values separated by commas, they’re matched to the parameters in order. These are positional arguments:
def make_user(name: str, age: int, role: str) -> dict[str, str | int]:
return {"name": name, "age": age, "role": role}
user = make_user("Manikandan", 30, "admin")
print(user)
{'name': 'Manikandan', 'age': 30, 'role': 'admin'}
The first value goes to name, the second to age, the third to role. Order matters.
Keyword arguments
You can also pass arguments by name using key=value:
user = make_user(name="Manikandan", age=30, role="admin")
This is called a keyword argument (sometimes “kwarg” for short). The order no longer matters:
user = make_user(role="admin", name="Manikandan", age=30)
You can mix the two styles, but positional arguments must come first:
# OK
user = make_user("Manikandan", 30, role="admin")
# error — positional after keyword
user = make_user(name="Manikandan", 30, "admin")
When to use keyword arguments
Use them whenever the call would otherwise be cryptic. Compare:
# what do these arguments mean?
draw_line(0, 0, 100, 200, 3, True)
# clearer
draw_line(x1=0, y1=0, x2=100, y2=200, thickness=3, dashed=True)
The keyword form is impossible to misread. Most modern Python codebases use keyword arguments liberally for any function with more than 2–3 parameters.
Default arguments
A parameter can have a default value that’s used when the caller doesn’t supply one:
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
print(greet("Manikandan")) # Hello, Manikandan!
print(greet("Manikandan", greeting="Hi")) # Hi, Manikandan!
print(greet("Manikandan", "Howdy")) # Howdy, Manikandan!
If you don’t pass greeting, it falls back to "Hello".
Defaults must come last
Parameters with defaults must appear after parameters without:
# OK
def f(a: int, b: int = 0) -> int: ...
# SyntaxError
def f(a: int = 0, b: int) -> int: ...
Otherwise Python wouldn’t know how to interpret a single positional argument.
The mutable default trap
This is the most famous foot-gun in Python:
def add_to_list(item: int, target: list[int] = []) -> list[int]:
target.append(item)
return target
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [1, 2] ← surprise!
print(add_to_list(3)) # [1, 2, 3] ← uh oh
The default [] is created once, when the function is defined, and reused across every call. Mutable defaults — lists, dicts, sets — leak state between calls.
The fix: use None as a sentinel and create the real default inside the function:
def add_to_list(item: int, target: list[int] | None = None) -> list[int]:
if target is None:
target = []
target.append(item)
return target
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [2]
print(add_to_list(3)) # [3]
Ruff will catch and warn about mutable default arguments. Get used to writing = None as your default for any collection.
A real-world example
A function for sending email-like messages with sensible defaults:
def send_message(
to: str,
body: str,
subject: str = "(no subject)",
cc: str | None = None,
priority: str = "normal",
) -> None:
print(f"To: {to}")
if cc is not None:
print(f"Cc: {cc}")
print(f"Subject: {subject}")
print(f"Priority: {priority}")
print()
print(body)
send_message(
to="[email protected]",
body="Hello!",
priority="high",
)
Notice how the call reads almost like a form. That’s the power of keyword arguments combined with defaults.
What’s next
You can write flexible functions. Next, the way to accept an unknown number of arguments — *args and **kwargs.