Now we have learned the basics of Python programming: variables, collections and loops. If you have followed the examples and the tutorial you might feel that sometimes we use too much code and it can be leveraged a bit. But maybe you do not know how to do it.
Most of the time the solution is to introduce functions. We have encountered functions of other modules which we used just like isupper() of strings or choice() of the random module.
If you have learned mathematics then you know about functions already. Mathematicians can do very nasty things with them but programmers too. The aim of a function is to produce a result which is determined only by the parameters passed to the function.
In programming language we can look at a function as a black box where we send in a defined number of parameters (0 and more) and the function returns a result. In Python a function always returns a result even if you do not explicitly write or see a return statement. This means if you know another programming language such as C++ or Java then you know about void functions. In Python there is no such type — but we will see this later in this article when I tell you about return values.
Defining a function
A function is created with the def statement. The general syntax looks like this:
def function_name(parameter list):
function_body of statements
The parameter list contains zero or more elements. If you call a function then you say arguments instead of parameters but this is terminology, in my eyes it is ok if you just say parameters even if you call a function. These parameters are mandatory if you do not declare them optional. We will take a look at optional parameters in the next section.
Every time a function is called the statements in its body are executed. Naturally you can use the pass statement in the function’s body to do nothing — but in this case the pass statement is executed too.
As you might know: a function has to have at least one statement in its body. Without it you get an error:
>>> def no_body_function():
...
File "<stdin>", line 2
^
IndentationError: expected an indented block
Well, this error message is not the most speaking but in this case the compiler is missing an indented block — at least one statement for the function’s body.
So let’s write a simple function which is an exchange calculator. It gets two parameters: the value and the exchange rate and it returns the changed value (value multiplied by exchange rate).
def exchange(value, rate):
return value*rate
So every time you define a function make sure you have an indented body. If you follow along this article in the interactive interpreter of Python the definition would look like this:
>>> def exchange(value, rate):
... return value*rate
...
And as I mentioned previously you can have functions without return statements too. However most of the time you won’t use such functions but for the sake of brevity let’s see an example here too:
>>> def no_return():
... print("This function has no return statement")
...
Calling functions
I think this is easy. You already know how to call functions but let’s quickly go through it. If you have a function definition then you can call it with passing the right arguments as parameters and you are good to go.
And as I told you before we already called functions. The very first and basic function we called was print(). You can call it without any parameters and in this case it prints a new-line character to the output (an empty line). Alternatively we can pass an arbitrary number of parameters, each separated with a comma (,) and they will be printed to the output too.
Now let’s call the two functions we defined in the previous section.
>>> no_return()
This function has no return statement
>>> exchange(123, 1.12)
137.76000000000002
As you can see there is nothing complex in calling functions.
Return values
Previously I said that functions return values — even if you do not explicitly write a return statement. Now it is time to verify my statement so I will show you that even the no_return() function returns a value, and this value is None.
To see the return statement of the function let’s simply wrap the function call into a print() function call.
>>> print(no_return())
This function has no return statement
None
>>> print(exchange(123,1.12))
137.76000000000002
Here you can see that even a function without return statements returns a None. This means in such cases you have to be careful how you use the return value because with a None you can do almost nothing just use it in a boolean expression — with care of course.
>>> result_value = no_return()
This function has no return statement
>>> result_value + 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>> if result_value == False:
... print("Nothing useful...")
... else:
... print("Wow, we have got True back!")
...
Wow, we have got True back!
As you can see in the example above you cannot use None in mathematical operations for example and None is not False.
To fix the second part of the example we could change the code like this:
>>> if not result_value:
... print("Nothing useful...")
... else:
... print("Wow, we have got True back!")
...
Nothing useful...
The same goes for using only return without any value. It results in the same None after returning than no return statement at all. Why should be this good? For example you want to terminate your function if a condition evaluates to true and want to return nothing. Naturally you can use return None but the more pythonic solution would be to simply use return.
Optional parameters
You can create functions with optional parameters. This means that these parameters do not have to be passed to the function. In this case their default value is used — and sometimes a block of statements is skipped if the optional parameter gets its default value.
Optional parameters have to follow the mandatory parameters and they have to have a default value. This value is used when you call the function and you do not provide this argument.
As an example let’s take the previously introduces exchange function. As a quick reminder here is the definition:
def exchange(value, rate):
return value * rate
If we try to call this exchange function with only 1 parameter (with the value) then we get an error from the interpreter:
>>> exchange(42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: exchange() missing 1 required positional argument: 'rate'
Now let’s make the rate variable optional and set it’s default value to 1 to be able to call this function for the same currency without any exchange.
So the solution is to have a default value for the rate parameter in the function and set this value to 1.
To see how this works I changed the code a bit to display the current exchange rate too:
>>> def exchange(value, rate=1):
... print('Current exchange rate is', rate)
... return value * rate
Now the rate parameter is optional, we can call this function with or without a rate:
>>> exchange(124,0.78)
Current exchange rate is 0.78
96.72
>>> exchange(325,1)
Current exchange rate is 1
325
>>> exchange(42)
Current exchange rate is 1
42
Ordering of the optional and required parameters do matter. For example if we change the order and add rate=1 as the first argument before value we will get an error:
>>> def exchange(rate=1, value):
... return value * rate
...
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
If you think about it you will get the idea why it is this way: what if we provide one argument. Would it be the optional one or the required one? Well, the interpreter could not tell and maybe you would end up with the wrong result.
Keyword arguments
It can happen that you will encounter the term “keyword arguments” when learning Python. They are in fact the same as optional arguments: they have a name and a default value. And this name is the keyword and you can use it to assign a new value to this parameter.
Let’s look at the previous example again: rate is the keyword argument of the function. Because exchange has only one optional argument you can call it in two flavors with both parameters:
>>> exchange(42, 1.25)
52.5
>>> exchange(42, rate=1.25)
52.5
The second case is where we use keyword arguments.
Now let’s take another example where we have multiple optional parameters so you see how it really works with keyword arguments.
The example will be very basic: we define a function which takes four parameters a, b, c, d and executes the following calculation: a + b – c + d. And to have it work it requires only 2 parameters, two are optional.
>>> def sub_sum(a, b, c=0, d=0):
... return a + b - c + d
...
>>> sub_sum(12,33)
45
Now we can optionally pass values for the variables c and d. If we already know that providing a value for c has two flavors.
>>> sub_sum(12,33,0,10)
55
>>> sub_sum(12,33,d=10)
55
As you can see, you do not have to provide all the values, it is enough to assign the value for d when calling the function. This is why they are called “keyword arguments”. And you might get the idea: there are functions which have a lot of arguments and most of the time you only need their default values. So you do not pass them along (so you do not have to know what the default value is) and you can fine-tune the function call with a single parameter which is somewhere in the list by using its keyword.
Taking this concept a bit further we can call functions with this keyword syntax in a way you cannot imagine in other languages: you are free to order the values as you want until you provide all the required arguments with their names.
>>> sub_sum(b=12,a=33,d=10)
55
>>> sub_sum(d=10, 8, 11)
File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
As you can see you cannot omit the names of required arguments if you mess around with the order. In this case it is required to have them named to let the interpreter know that you want to set those values.
Traps with keyword arguments
Above we have seen one way of using keyword arguments. However every coin has two sides. Let’s dig a bit deeper when those default values are assigned. It is done when the function is created (so when the interpreter parses the function definition) and not when the function is invoked. This means we do not see any difference as long we use immutable types for named parameters / keyword arguments.
However problems can arise when we use mutable variables, for example a list:
>>> def append_if_short(s, lst=[]):
... if len(s) < 4:
... lst.append(s)
... return lst
...
In the example above we append the parameter s to the parameter lst if the length of s is at most 3. This seems fine if we pass both parameters to the function. But let’s call this function some times…
>>> append_if_short("one")
['one']
>>> append_if_short("two")
['one', 'two']
>>> append_if_short("three")
['one', 'two']
>>> append_if_short("four")
['one', 'two']
>>> append_if_short("five")
['one', 'two']
>>> append_if_short("six")
['one', 'two', 'six']
As you can see this results in an unexpected behavior. We pass in one string and get back a list with more elements than expected.
However this is not the full truth. We can make things ever worse:
>>> def append_if_short(s, lst=[]):
... if len(s) < 4:
... lst.append(s)
... return lst
...
>>> result = append_if_short("one")
>>> result
['one']
>>> result.append('five')
>>> append_if_short('two')
['one', 'five', 'two']
In the example above we have added an element to the list which us clearly longer than 3 characters — which again can lead to unexpected behavior.
To fix this let’s change the function definition:
>>> def append_if_short(s, lst=None):
... if lst is None:
... lst = []
... if len(s) < 4:
... lst.append(s)
... return lst
...
>>> result = append_if_short("one")
>>> result
['one']
>>> result.append('five')
>>> append_if_short('two')
['two']
Docstrings
Sometimes (I hope every time) you will feel the urge to document your functions. This you can do with simple comments put around the function definition.
However there is a common practice which you should follow: docstrings. These are simple documentation strings placed right after the function definition. They have the special three quotation format because they are multiline strings describing your function.
Sometimes documentation is longer than the function itself. A convention is to make the first line of the docstring a brief one-line description, then have a blank line followed by a full description, and then some examples as they would appear if typed in the interactive interpreter.
So let’s use this guidance and add documentation to our append_if_short function.
def append_if_short(s, lst=None):
""" Returns a list containing s if the length of s is smaller than 4.
s is any string, lst is an optional list-type parameter. If lst is not provided a new lst gets a new empty list assigned. If len(s) < 4 then s is appended to lst and lst is returned.
>>> append_if_short('one')
['one']
>>> append_if_short('two', ['one'])
['one', 'two']
>>> append_if_short('three', [])
[]
>>> append_if_short('four')
[]
"""
if lst is None:
lst = []
if len(s) < 4:
lst.append(s)
return lst