Python Arguments

No, not those kinds of arguments! In Python an argument:

  • is a snippet of information that is passed into a function or method.
  • can vary from a single number, right up to the state of an entire system.
  • can be positional, named or completely arbirary.

An argument differs from a parameter, although the terms are often used interchangably. A Parameter is the formal name of the variable into which an argument might be passed, whereas an argument is the actual data value that's passed in.

The different methods of passing arguments into functions in Python are subject to many uses and abuses and can easily lead to confusion. When in doubt, favour the simplest method of getting an argument from A to B the humble positional argument:

Positional Arguments

As the name might suggest, a positional argument is an argument which has a given position in the list of arguments passed into your function. A function definition with three positional arguments might look like this:

def my_function(arg1, arg2, arg3):

The only way we can call this function without causing a TypeError is to call it with all 3 of those arguments filled, like so:

my_function('one', 2, 3)

If we didn't care about the value of one of those arguments, and wanted it to use some sort of default, we'd be stuck and may end up writing something like this:

def my_function(arg1, arg2, arg3):
    if arg1 ==  None:
        arg1 = 'Default Value'

So we could call it like this:

my_function(None, 2, 3)

Messy!

Default Argument Values

We can fix the default value problem with default values.

We start by sorting our arguments by their relevance and putting the ones we're least likely to care about at the end of our definition, like so:

def my_function(important_arg, regular_arg, unimportant_arg, nerds_only):

The arguments are still positional, so we have to supply them in the right order. But what happens when we add some default values?

def my_function(important_arg, regular_arg='Some Value', unimportant_arg=False, nerds_only=3.14):
    print(important_arg, regular_arg, unimportant_arg, nerds_only)

Now we have suddenly gained the ability to ignore some of our arguments and call our function like so:

>>> my_function(1)
(1, 'Some Value', False, 3.14)

If the defaults given by the last 3 arguments suit us, we only have to pass in the one that matters.

But what happens if we really want to specify a value for unimportant_arg, but don't care about the other two? What happens if we try:

>>> my_function(1,True)
(1, True, False, 3.14)

Uh oh... we've supplied a value for important_arg and regular_arg, overwriting the wrong default value!

We're going to have to provide a value for regular_arg to make sure our positional arguments are in the right positions... or are we?

>>> my_function(1,unimportant_arg=True)
(1, 'Some Value', True, 3.14)

Whew! That's handy. Python is clever enough to figure out that we're trying to supply a value for unimportant_arg even though it's just a positional argument with a default value.

This useful mix of positional arguments and arguments with default values is how you might approach writing most of your Python functions. It's clean, simple and you can always glance over your function definitions to tell immediately:

  • What arguments your function takes
  • What order they should be in
  • What, if any, their default values are

But there are gotchas with default values...

Default Argument Values: The Gotchas

A default value for an argument is evaulated only once, along with your function definition. Python will pick through your source code, figure out what functions you've created, and create the default values for you.

If you happen to specify a mutable object as a default value ( A mutable object is one that can be modified in place, such as a list ) then every call you make to your function will re-use that same object. This leads to all sorts of fun and confusion with lists.

Look at this simple definition, it adds the string 'Oh no!' to a list and prints it.

def my_function(arg1=[]):
    arg1.append('Oh no!')
    print(arg1)

What would you expect when you run it multiple times? Let's see:

>>> my_function()
['Oh no!']
>>> my_function()
['Oh no!', 'Oh no!']
>>> my_function()
['Oh no!', 'Oh no!', 'Oh no!']

Oh no! Indeed! Instead of creating a nice empty list as our default value, our function is re-using the same object every time it's called. This leaves us with a runaway list full of junk from the last time the function was called.

The solution to this is placeholders. Rather than using a list as the default value, we'll stick with Python's good ole 'None' to tell our function that no argument has been specified. This is a good choice of placeholder, since it makes sense when you read over the code:

def my_function(arg1=None):
    if arg1 == None:
        arg1 = []
    arg1.append('Oh yes!')
    print(arg1)

You'll notice we've added a little bit of code to deal with our placeholder. If we discover that our arg1 is set to None then we can assume no value has been supplied and fill it with a nice, clean empty list. When we call the function, it now behaves as expected:

>>> my_function()
['Oh yes!']
>>> my_function()
['Oh yes!']
>>> my_function(['Donkeys!']
['Donkeys!', 'Oh yes!']

*args and **kwargs

Now we'll dive briefly into the dark and dangerous world of *args and **kwargs.

When placed in a definition, these snippets of syntax sugar will slurp up any positional or keyword arguments you might pass into your function and store them for you to investigate.

>>> def my_hungry_function(*args, **kwargs):
...    print(args, kwargs)
...
>>> my_hungry_function('so','many','arguments',that='I',lost='count!') 
(('so', 'many', 'arguments'), {'lost': 'count!', 'that': 'I'})

The trouble with *args and **kwargs is that they deal with multiple arguments at once and don't enforce any sort of rules or structure. They're useful for dealing with great long lists of arguments in complicated functions with lots of options but come at the cost of confusion.

Within your function the variable args will be a list of positional arguments, in their correct positions and kwargs will be a dictionary of keyword arguments which you can access by name.

For keyword arguments it's generally a good idea to use get with a default value, like so:

def my_function(**kwargs):
    my_arg = kwargs.get('my_arg','Default Value')
    print(my_arg)

Now you know at least something about *args and **kwargs promise me you'll use them sparingly?

That's all folks!

Search above to find more great tutorials and guides.

Plasma 2040

Swathe everything in rainbows with this all-in-one, USB-C powered controller for WS2812/Neopixel and APA102/Dotstar addressable LED strip.