Python args, kwargs, and All Other Ways to Pass Arguments to Your Function

Expertly design your function parameters in 6 examples

Python args, kwargs, and All Other Ways to Pass Arguments to Your Function
What does the slash and asterisk do? (image by author)

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.

Understanding Python decorators: six levels of decorators from beginner to expert
How decorators work, when to use them and 6 examples in increasingly complexity

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
parameters are red, arguments are green (image by author)

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!

Args vs kwargs: which is the fastest way to call a function in Python?
A clear demonstration of the timeit module

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!

Why and how custom exceptions lead to cleaner, better code
Clean up your code by creating your own custom exceptions

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.

No Need to Ever Write SQL Again: SQLAlchemy’s ORM for Absolute Beginners
With this ORM you can create a table, insert, read, delete and update data without writing a single line of SQL

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.

Understanding Python Context-Managers for Absolute Beginners
Understand the WITH statement with lightsabers

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.

Cython for absolute beginners: 30x faster code in two simple steps
Easy Python code compilation for blazingly fast applications

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))
Destroying Duck Hunt with OpenCV — image analysis for beginners
Write code that will beat every Duck Hunt high score

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')
Multi-tasking in Python: Speed up your program 10x by executing things simultaneously
Step-by-step guide to apply threads and processes to speed up your code

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:

Happy coding!

— Mike

P.S: like what I’m doing? Follow me!

Join Medium with my referral link — Mike Huls
Read every story from Mike Huls (and thousands of other writers on Medium). Your membership fee directly supports Mike…