Python Basics: Making Function Options Clear with Keyword-Only Arguments
Calls with several positional options can be difficult to read. A keyword-only parameter requires the caller to name the option, making intent visible at the call site. Use a bare asterisk in the function definition to mark the boundary.
These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the demonstrated results; run the commands normally, without Python’s -O option.
Prepare a scratch directory
You need Bash, a text editor, Python 3 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8. The setup stops the shell if creating or entering the scratch directory fails.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Give optional behavior an explicit name
Save this as format_label.py:
def format_label(text, *, uppercase=False, prefix=""):
label = text.upper() if uppercase else text
return prefix + label
plain = format_label("draft")
marked = format_label("draft", uppercase=True, prefix="Status: ")
assert plain == "draft"
assert marked == "Status: DRAFT"
print(plain)
print(marked)
try:
format_label("draft", True)
except TypeError:
print("Positional option rejected")
else:
raise AssertionError("Expected TypeError")Run it:
python3 format_label.pyOutput:
draft
Status: DRAFT
Positional option rejectedtext can be positional or named, but uppercase and prefix must be named. Their defaults make them optional. The deliberate bad call demonstrates the boundary without depending on the exact wording of a Python error message.
2. Require an option even when it is keyword-only
Save this as limit_results.py:
def first_items(items, *, limit):
if limit < 0:
raise ValueError("limit must be non-negative")
return items[:limit]
selected = first_items(["alpha", "beta", "gamma"], limit=2)
assert selected == ["alpha", "beta"]
assert first_items(["alpha"], limit=0) == []
print(selected)
try:
first_items(["alpha"])
except TypeError:
print("Missing limit rejected")
else:
raise AssertionError("Expected TypeError")Run it:
python3 limit_results.pyOutput:
['alpha', 'beta']
Missing limit rejectedA parameter without a default is required, even after the asterisk. This interface forces each caller to choose a limit. The example expects an integer limit and a sliceable sequence; keyword-only syntax controls how arguments are supplied, not their data types.
3. Combine variable positional values with a named option
Save this as join_labels.py:
def join_labels(*labels, separator=", "):
return separator.join(labels)
default = join_labels("draft", "review", "publish")
custom = join_labels("draft", "review", separator=" -> ")
assert default == "draft, review, publish"
assert custom == "draft -> review"
assert join_labels() == ""
print(default)
print(custom)Run it:
python3 join_labels.pyOutput:
draft, review, publish
draft -> reviewAfter *labels collects the positional values into a tuple, separator is keyword-only automatically. You do not add another bare asterisk. The values are strings here because str.join expects strings; this function does not convert other types.
When to use this
Use named options when their meaning would be unclear from their position. Avoid changing an established positional parameter to keyword-only without considering existing callers: that is an interface change. The official special-parameters tutorial distinguishes positional-only, positional-or-keyword, and keyword-only parameters.
Clean up
In the same terminal, remove only the three example files, return to your original directory, and remove the empty scratch directory:
rm -- format_label.py limit_results.py join_labels.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.