PYTHON / FUNCTIONS
Keyword arguments and keyword-only parameters
Call functions by parameter name, and design signatures with a bare * so some parameters can only be passed by name.
What you will learn
- Pass arguments by name so call order stops mattering
- Use a bare * to make later parameters keyword-only
- Read TypeErrors for multiple values and missing keyword-only arguments
- Make a keyword-only parameter required by giving it no default
Understanding Keyword arguments and keyword-only parameters
When Python binds a call to a function, it works in two passes. First it takes the positional arguments and fills parameters strictly left to right; then it takes each name=value pair and drops the value into the parameter with that exact name. Because the second pass matches by name, order among keyword arguments is irrelevant: draw_box(height=4, width=3) and draw_box(width=3, height=4) produce identical bindings. The two passes must not collide, which is why a value supplied both positionally and by name raises TypeError: got multiple values for argument.
A bare * in a parameter list is not a parameter. It is a marker that says the positional pass stops here, so every parameter written after it can only be filled by the name pass. In def resize(image, *, width, height=None), image can be positional or keyword, while width and height are keyword-only. This is why passing an extra positional argument to such a function reports that it takes only one positional argument, even though the signature clearly lists three names.
Keyword-only parameters exist for readability and for the freedom to change code later. A call like open_file(path, True, False) tells the reader nothing, while open_file(path, overwrite=True, backup=False) is self-documenting, and flags are exactly the kind of parameter that is easy to swap by accident. Keyword-only also protects you as an author: since no caller can depend on the position of those parameters, you can insert a new one or reorder them without breaking anyone. A keyword-only parameter with no default is required, so it is a way to demand an explicit name at every call site.
def draw_box(width, height, *, fill="none", border=1):
return f"{width}x{height} fill={fill} border={border}"
print(draw_box(3, 4))
print(draw_box(3, height=4, fill="red"))
print(draw_box(height=4, width=3, border=2))
try:
draw_box(3, 4, "red")
except TypeError as e:
print("TypeError:", e)Positional arguments fill parameters by position and keyword arguments fill them by name, and a bare * in the signature forbids the positional route for everything after it.
Worked examples
Order-free calls and colliding arguments
Keyword arguments can appear in any order, but they must not target a parameter already filled positionally.
def slice_text(text, start=0, end=None, upper=False):
piece = text[start:end]
return piece.upper() if upper else piece
print(slice_text("keyword arguments", end=7))
print(slice_text("keyword arguments", upper=True, start=8))
print(slice_text(start=0, end=3, text="keyword"))
try:
slice_text("abc", 1, text="xyz")
except TypeError as e:
print("TypeError:", e)Example explained
Line 1end=7 skips over start entirely, so start keeps its default 0.
Line 2upper=True before start=8 is fine because keyword arguments are matched by name, not order.
Line 3The third call passes every argument by name, including text, so no positional argument is used at all.
Line 4In the last call "abc" fills text positionally and text="xyz" tries to fill it again, which is the collision Python refuses.
A required keyword-only parameter
Parameters after the bare * cannot be passed positionally, and one without a default becomes a mandatory named argument.
def resize(image, *, width, height=None):
if height is None:
height = width
return f"{image} -> {width}x{height}"
print(resize("logo.png", width=64))
print(resize("logo.png", width=64, height=32))
try:
resize("logo.png", 64)
except TypeError as e:
print("TypeError:", e)
try:
resize("logo.png")
except TypeError as e:
print("TypeError:", e)Example explained
Line 1width has no default, so every call must spell out width=... .
Line 2height=None is a keyword-only parameter with a default, so it may be omitted.
Line 3resize("logo.png", 64) fails because the * stops positional filling after image, leaving 64 nowhere to go.
Line 4The final message says 'keyword-only argument', which is Python telling you the parameter exists but only accepts the named form.
Why print's sep must be keyword-only
print accepts unlimited positional values, so its formatting options are reachable only by name.
print("a", "b", sep="-")
print("a", "b", "-")
print("progress", end="")
print("...done")Example explained
Line 1sep="-" reaches the separator parameter and replaces the default single space.
Line 2The same string passed positionally becomes a third value to print, joined by the default space.
Line 3Because print's signature is print(*objects, sep=' ', end='\n', ...), no positional slot is ever free for sep, so keyword-only is the only workable design.
Line 4end="" suppresses the newline, so the next print continues on the same line.
Keyword arguments bind by name, not by declaration order
Renaming a parameter changes the public call syntax even when the position is unchanged.
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet(name="Ada"))
def greet(who, greeting="Hello"):
return f"{greeting}, {who}!"
print(greet("Ada"))
try:
print(greet(name="Ada"))
except TypeError as e:
print("TypeError:", e)Example explained
Line 1The first call works because the parameter is literally spelled name.
Line 2After the rename, the positional call still works since position did not change.
Line 3The keyword call breaks: there is no parameter called name, so the name pass has nothing to match.
Line 4This is why parameter names are part of a function's contract once callers use the keyword form.
Important notes
A keyword argument name must be a real parameter name in that function; a typo produces 'got an unexpected keyword argument' rather than being ignored.
Only one bare * is allowed in a signature, and parameters before it can still be passed either way unless the signature restricts them further.
Common mistakes
Writing a positional argument after a keyword argument, as in greet(name="Ada", "Hi"), which is a SyntaxError raised before the file runs at all, so nothing executes.
Passing the same parameter twice, once positionally and once by name, such as slice_text("abc", 1, text="xyz"), which raises 'got multiple values for argument' instead of quietly preferring one value.
Assuming the bare * is a parameter and writing def f(a, *) or trying to pass something for it; * takes no value and must be followed by at least one parameter name, otherwise it is a SyntaxError.
Try it yourself
Change, predict, then run
Write log(message, *, level="INFO", timestamp=None) that returns a single formatted string, then call it three ways: with only a message, with level given by name, and with both options by name in reverse order. Add a try/except that calls log("boot", "DEBUG") and prints the TypeError message.
Open the Python workspaceCheck your understanding
Given def f(a, b=2, *, c=3), which call runs without error?
- f(1, 2, 3)
- f(1, c=3)
- f(c=3)
- f(a=1, 2)
Show answer
f(1, c=3) fills a positionally, lets b keep its default, and passes the keyword-only c by name. f(1, 2, 3) is the tempting choice because c is visibly the third parameter, but the bare * stops positional filling after b, so the third positional value has no slot and Python reports that f takes 2 positional arguments. f(c=3) omits the required a, and f(a=1, 2) is a SyntaxError since a positional argument cannot follow a keyword argument.