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.

Toggle theme (T)