Murphy’s law 2, electric boogaloo: whatever can go wrong, will go wrong, and at the worst possible time.
Hey pips!
Positional and keyword parameters in Python can be confusing at first, but they give us a really ergonomic way to pass different kinds of arguments to a function.
To illustrate, take a look at this function call:
sorted(users, key = (lambda user: user.score), reverse = True)
We’re sorting a collection of users here, so at its core, the function is sorted(users)
. The other parameters are just configuring how that sort should be conducted.
This is probably the most common distinction between positional and keyword arguments. You pass the primary data or input positionally, but extraneous configuration, options and flags you pass with keywords.
def search(data, user, criteria = None, limit = 1):
...
Although, looking at the function definition, it’s unclear which parameters are intended to be positional vs keyworded. You can’t just assume those with default values are keyworded, since positional parameters could have defaults too.
Python as a language is all for flexibility and freedom (pretty evident in its lack of static typing), but sometimes, we may want to enforce particular parameters to be positional-only, and others to be keyword-only.
[!Tip] This is especially relevant if you’re writing code that’ll be used by others, where you’ll want to help them avoid shooting themselves in the foot.
We can achieve this with 2 symbols. A slash /
will make all parameters preceding it positional-only:
def read(book, /, pages):
...
pages
can still be positional or keyworded, but book
can now only be positional:
>>> read("The Pragmatic Programmer", pages = 101)
# `pages` can be positional or keyword
>>> read("The Pragmatic Programmer", 101)
# but `book` cannot be keyword
>>> read(book = "The Pragmatic Programmer", pages = 101)
TypeError: read() got some positional-only arguments passed as keyword arguments: 'book'
If you take a look at the definitions for methods of built-in Python classes, such as list
(you can just call help(list)
in IDLE), you’ll notice these splashed throughout.
def append(self, object, /):
...
This prevents you from passing self
as a keyword argument, although you would have to be an extraordinary level of deranged to do so.
[!Note] Of course I’ve done it before, who do you think I am.
And, more commonly, use *
to make all parameters following it keyword-only:
def review(book, *, rating, comment = None):
...
Note that book
has no restrictions so it can be positional or keyworded, but rating
and comment
here have to be keyworded:
>>> review("The Pragmatic Programmer", rating = 5)
>>> review("The Pragmatic Programmer", rating = 5, comment = "deep stuff")
>>> review("The Pragmatic Programmer", 5, "ts pmo icl")
TypeError: review() takes 1 positional argument but 3 were given
This tends to be much more useful than /
since requiring the parameters to be named explicitly helps to reduce ambiguity and overhead significantly when others use your code.
Like, what does this function do??
mark_test("Adam", 35, 4)
But with kwargs…
mark_test("Adam", page = 35, question = 4)
Much better. We can make sure this never happens by setting page
and score
as keyword-only. It’s fairly obvious what the first argument is here so there’s not much need to make that keyword-only too.
def mark_test(student, *, page, score):
...
So, when defining functions you can make it a lot safer for your users by enforcing positional-only and keyword-only parameter with /
and *
. Bear in mind – that user could be you.
Further Reading
- The PEP proposing positional-only parameters – PEP 570↗