Python args, kwargs, and All Other Ways to Pass Arguments to Your Function
Expertly design your function parameters in 6 examples
This article is a deep dive in designing your function parameters. We’ll find out what *args
and **kwargs
do, what the function of /
and *
is, and how to design your function parameters in the best way possible. A function with well-designed parameters is easier to understand and use by other developers. In this article we explore 6 questions that demonstrate everything you need to know to become a parameter-expert. Let’s code!
Prep: definitions and passing arguments
In this part we’ll quickly go through the terminology and all the ways Python offers to handle passing arguments to a function.
What is the difference between parameters and arguments?
Many people use these terms interchangeably but there are differences. Parameters are initialized with the values that the arguments supply:
- parameters are the names that are defined in the function definition
- arguments are the values that are passed to the function
What are the two ways I can pass arguments?
You can pass arguments positionally and by keywords. In the example below we pass the value hello
as a positional arg. The value world
is passed with a keyword; we specify that we want to pass world
to the thing
parameter.
def the_func(greeting, thing):
print(greeting + ' ' + thing)
the_func('hello', thing='world')
The difference between positional arguments and kwargs (keyword arguments) is that the order in which you pass positional arguments matter. If you call the_func('world', 'hello')
it will print world hello
. The order in which you pass kwargs doesn’t matter:
the_func('hello', 'world') # -> 'hello world'
the_func('world', 'hello') # -> 'world hello'
the_func(greeting='hello', thing='world') # -> 'hello world'
the_func(thing='world', greeting='hello') # -> 'hello world'
the_func('hello', thing='world') # -> 'hello world'
Also notice (in the last line) that you can mix and match positional and keyword arguments as long as the kwargs come after the positional ones.
Is the performance of args better than kwargs?
Check out the article below!
Designing function parameters
In this part we’ll answer 6 questions that demonstrate all the ways in which you can design your function parameters. Each answer will be accompanied by an example and a use-case if required.
1. How do I catch all uncaught positional arguments?
With *args
you can design your function in such a way that it accepts an unspecified number of parameters. As an example, take a look at the function below.
def multiply(a, b, *args):
result = a * b
for arg in args:
result = result * arg
return result
In this function we define the first two parameters (a
and b
) normally. Then we use *args
to pack all remaining arguments in a tuple. Think of the *
as eating up all unmached arguments and pushing them into a tuple-variable called ‘args’. Let’s see this in action:
multiply(1, 2) # returns 2
multiply(1, 2, 3, 4) # returns 24
The last call assigns the value 1 to parameter a
, a 2 gets assigned to b
and the arg
variable gets filled with (3, 4)
. Since this is a tuple we can loop over it in the function and use the values for multiplication!
2. How do I catch all uncaught keyword arguments?
The same trick we use in the previous part can be used to catch all remaining, unmatched keyword arguments:
def introduce(firstname, lastname, **kwargs):
introduction = f"I am {firstname} {lastname}"
for key, value in kwargs.items():
introduction += f" my {key} is {value} "
return introduction
Like with *args
, the **kwargs
keyword eats up all unmatched keyword arguments and stores them in a dictionary called kwargs
. We can then access this dictionary like in the function above.
print(introduce(firstname='mike', lastname='huls'))
# returns "I am mike huls"
print(introduce(firstname='mike', lastname='huls', age=33, website='mikehuls.com'))
# I am mike huls my age is 33 my website is mikehuls.com
With kwargs
we can add some extra arguments to the introduce
function.
3. How can I design my function to only accept keyword arguments?
When you really don’t want to mix up your parameters you can force your function to only accept keyword arguments. A perfect use-case for this could be a function that transfers money from one account to another. You really don’t want to pass the account numbers positionally because then you run the risk that a developer switch up the account numbers accidentally:
def transfer_money(*, from_account:str, to_account:str, amount:int):
print(f'Transfering ${amount} FORM {from_account} to {to_account}')
transfer_money(from_account='1234', to_account='6578', amount=9999)
# won't work: TypeError: transfer_money() takes 0 positional arguments but 1 positional argument (and 2 keyword-only arguments) were given
transfer_money('1234', to_account='6578', amount=9999)
# won't work: TypeError: transfer_money() takes 0 positional arguments but 3 were given
transfer_money('1234', '6578', 9999)
In the function above you see the *
again. I think of the asterisk as eating up all unmatched positional arguments, but whereas *args
stores all unmatched, positional arguments in the args
tuple, the bare *
just voids them.
4. How do I design my function to only accept positional arguments?
The function below is an example of a function allowing only positional arguments:
def the_func(arg1:str, arg2:str, /):
print(f'provided {arg1=}, {arg2=}')
# These work:
the_func('num1', 'num2')
the_func('num2', 'num1')
# won't work: TypeError: the_func() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'
the_func(arg1='num1', arg2='num2')
# won't work: TypeError: the_func() got some positional-only arguments passed as keyword arguments: 'arg2'
the_func('num1', arg2='num2')
The /
in the function definition forces all parameters that precede it to be positional. Sidenote: this doesn’t mean that all parameters that follow the /
must be kwarg-only; these can be positionally and with keywords.
Why would I want this? Doesn’t this decrease the readability of my code?
Good question! An example occasion could be when you define a function that is so clear that you don’t need the keyword-argument to specify what it does. For example:
def exceeds_100_bytes(x, /) -> bool:
return x.__sizeof__() > 100
exceeds_100_bytes('a')
exceeds_100_bytes({'a'})
In this example it’s pretty clear that we are checking if the memory size of 'a'
exceeds 100 bytes. I can’t really think of a better name to give the x
parameter and it’s fine to call the function without the need to specify that x=’a’
. Another function is the built-int len
function: it would be pretty awkward to call len(target_object=some_list)
.
As a little extra we can change the parameter-name since we know it doesn’t break any calls to the function: we don’t allow kwargs. In addition we can even extend this function with full backward compatibility like so. The version below will check if any provided argument exceeds 100 bytes.
def exceeds_100_bytes(*args) -> bool:
for a in args:
if (a.__sizeof__() > 100):
return True
return False
We can replace the x
by *args
because in the previous version the /
ensured that the function was called only with positional arguments.
5. Mix and match — How do I pass args that are either positional or kwargs?
As an example we’ll look to the len
function we’ve discussed earlier. This function allows only positional arguments. We’ll extend this function with by allowing the developer to choose whether or not to count duplicates. We want to the developer to pass this keyword with kwargs:
def len_new(x, /, *, no_duplicates=False):
if (no_duplicates):
return len(list(set([a for a in x])))
return len(x)
As you see we want to count the len
of the x
variable. We can only pass the argument for the x
parameter positionally since it’s preceded by a /
. The no_duplicates
parameter must be passed with a keyword since it follows the *
. Let’s call the function:
print(len_new('aabbcc')) # returns 6
print(len_new('aabbcc', no_duplicates=True)) # returns 3
print(len_new([1, 1, 2, 2, 3, 3], no_duplicates=False)) # returns 6
print(len_new([1, 1, 2, 2, 3, 3], no_duplicates=True)) # returns 3
# Won't work: TypeError: len_() got some positional-only arguments passed as keyword arguments: 'x'
print(len_new(x=[1, 1, 2, 2, 3, 3]))
# Won't work: TypeError: len_new() takes 1 positional argument but 2 were given
print(len_new([1, 1, 2, 2, 3, 3], True))
6. Mix and match — all together
The function below is pretty extreme example of how you can combine all previously discussed techniques. First, it forced the first two arguments to be passed positionaly, the next two can be passed positionally and with keywords, then two keyword-only parameters and then we catch the remaining uncaught with **kwargs
.
def the_func(pos_only1, pos_only2, /, pos_or_kw1, pos_or_kw2, *, kw1, kw2, **extra_kw):
# cannot be passed kwarg <-- | --> can be passed 2 ways | --> can only be passed by kwarg
print(f"{pos_only1=}, {pos_only2=}, {pos_or_kw1=}, {pos_or_kw2=}, {kw1=}, {kw2=}, {extra_kw=}")
You can pass this function like this:
# works (pos_or_kw1 & pow_or_k2 can be passed positionally and by kwarg)
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={}
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={}
pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2', extra_kw={'kw_extra1': 'extra_kw1'}
# doesnt work, (pos1 and pos2 cannot be passed with kwarg)
# the_func(pos_only1='pos1', pos_only2='pos2', pos_or_kw1='pk1', pos_or_kw2='pk2', kw1='kw1', kw2='kw2')
# doesnt work, (kw1 and kw2 cannot be passed positionally)
# the_func('pos1', 'pos2', 'pk1', 'pk2', 'kw1', 'kw2')
Conclusion
In this article we went through all the ways to design your function parameters and seen the way you can mix and match them so that the developer can use your function in the best way possible.
I hope this article was as clear as I hope it to be but if this is not the case please let me know what I can do to clarify further. In the meantime, check out my other articles on all kinds of programming-related topics like these:
- Git for absolute beginners: understanding Git with the help of a video game
- Create and publish your own Python package
- Create a fast auto-documented, maintainable, and easy-to-use Python API in 5 lines of code with FastAPI
Happy coding!
— Mike
P.S: like what I’m doing? Follow me!