print("Hello")Hello
Functions encapsulate code that performs a specific task, or in other words, functions group lines of code that belong together. They promote reusability and prevent code duplication. For example, if you need to perform repeated calculations consisting of several lines of code, the corresponding code might be outsourced into a function. We will see examples of Python functions soon, but first we will discuss how to call and define functions.
We have already mentioned that we call an existing function with its name followed by a pair of parentheses. Inside the parentheses, a function can accept arguments, which are specific values we provide to customize its behavior.
For example, we already saw the built-in functions print and type. Here’s how we call these functions:
print("Hello")Hello
type("Hello")str
Notice that both functions are called with one argument (the string "Hello" in this case). However, we can also call a function without any argument:
print()
If and how many arguments a function accepts depends entirely on the specific function. The print function, for example, can accept any number of arguments, whereas the len function (which we will cover in a later chapter) accepts exactly one argument.
No matter how many arguments a function takes (this really depends on the specific function), we need to supply the pair of parentheses in order to call the function.
Calling a function instructs Python to run all lines of code belonging to that function. In the examples we’ve just shown, we do not even know which lines of code get executed when we run the print or type functions. However, this is not important as long as the functions do what they are supposed to do. This is what encapsulation actually means: a function encapsulates its code, and we can call a function without ever knowing its implementation details.
We are not restricted to calling existing functions. In fact, we can create our own functions, and they behave just like built-in functions. Here’s how we define a function in Python (using pseudo-code):
def function_name(<arg1>, <arg2>, ...):
<do something>
...
<optionally return something>
Let’s break this down. A function definition starts with a function header introduced with the keyword def. Then we specify the function name (keeping in mind the naming rules we’ve already discussed), followed by a pair of parentheses. If our function requires additional information to do its job, we specify its parameters inside the parentheses (separated by commas). Each parameter gets its own name, and the function replaces all parameters with specific values that are provided when the function is called (these specific values are called arguments). Finally, the function header needs to be concluded with a : symbol.
Next, we indent all lines that belong to the function body. That way, Python knows which lines to execute when we call the function. A function body can consist of one or several lines of code. If the function has parameters, their specific values are available within the function body (that is, parameter names get replaced with specific argument values). Optionally, a function can return a value (for example, the result of a computation). This return value can be evaluated and used just like any expression.
Let’s review some examples of simple functions. The following function is named test1, has no parameters, and its body consists of two lines of code. When called, the function internally assigns the name s to the string "Hello World!", which it then prints to the screen. This function does not return a value.
def test1():
s = "Hello world!"
print(s)When typing this function definition in the interactive interpreter, the prompt changes from >>> to ... after the first line, because Python knows that the function definition is incomplete. As with the regular prompt, do not type the ... characters when you define this function in the REPL.
Notice that running these three lines of code in Python does not actually run the function – they merely define the function so that Python now knows that a function test1 exists. We still have to call this function in order to run it:
test1()Hello world!
As a side note, the name s used inside the function body does not exist outside the function, because a function body defines its own scope. Everything defined inside a function only exists within this function.
Let’s modify the first example so that it returns a value. Here’s our new function test2:
def test2():
s = "Hello world!"
return sThis function contains a return statement, which in this case means that the function returns s (which is equal to the string "Hello world!"). After defining this function, we can call it:
test2()'Hello world!'
Since we are using Python in interactive mode, the returned value is automatically printed on the screen. However, we can now also continue to work with the returned value, for example by giving it a name:
h = test2()Now the result of calling test2 (its return value) is accessible with the name h, which refers to the string 'Hello world!':
h'Hello world!'
type(h)str
Make sure that you understand the difference in behavior between the two functions test1 and test2! The first function prints a string to the screen (and returns nothing), whereas the second function returns a string.
Let’s define an even more sophisticated function, this time we throw two parameters x and y into the mix. The function should return the sum of these two parameters, which is why we aptly name it add:
def add(x, y):
return x + yOnce the add function is defined, we have to call it with two arguments:
add(3, 7)10
When the function is called, the arguments (the values 3 and 7 in this particular example) are used in place of the parameters x and y inside the function body. That’s why the function returns 3 + 7 (evaluating to 10).
Notice that we need to supply exactly two arguments when we call add. If we don’t, Python will throw an error:
add(5)--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[13], line 1 ----> 1 add(5) TypeError: add() missing 1 required positional argument: 'y'
Because this function returns a value, Python will reduce a function call like add(5, 5) to its return value 10. Using this knowledge, we can compose more complicated expressions as follows:
add(add(2, add(5, 7)), 9)23
Working its way from inside out, Python replaces each function call with its returned value until the result cannot be reduced further. Here’s a breakdown of the steps involved in the previous example:
add(add(2, add(5, 7)), 9) # add(5, 7) is evaluated to 12
add(add(2, 12), 9) # add(2, 12) is evaluated to 14
add(14, 9) # add(14, 9) is evaluated to 23
2323
Of course, we can also assign a name to the returned value if we want to use it later on:
a = add(add(2, add(5, 7)), 9) # 23
a - 203
Python functions have an extremely useful feature. When defining a function, parameters can get default values, so-called default arguments. This means that all parameters with default values are optional when the function is called – values for these optional parameters do not need to be passed.
Here’s our add function definition from before, but this time the second parameter gets a default value of 1:
def add(x, y=1):
return x + yNow we can also call add with just one argument (x), because if we do not supply a value for y, Python will use its default value of 1:
add(5)6
Note that we can still call the function with two arguments if we want to override the default value for y:
add(5, 3)8
Normally, Python assigns arguments passed to a function call by position. That is, if we call add(5, 3), the first parameter x gets the first value 5, and the second parameter y gets the second value 3. Specifying arguments in a function call by position is referred to as positional arguments.
However, this can quickly get unwieldy if a function has many parameters. Consider the following function definition:
def many_args(a, b, c=0, d=1, e=0, f=5, g=5, h=0, i=-1):
passThe pass statement is a placeholder that literally does nothing. Since Python requires at least one indented line of code in a function body, we have to use pass here in order to define a function that does not do anything.
Parameters a and b are mandatory, whereas the remaining seven parameters have default values (and are therefore optional). Let’s assume we want to call the function with arguments a=10 and b=5, and we want only one of the remaining seven parameters to differ from their default value – say, we only want h=-5. Using positional arguments, we need to include arguments for parameters that we do not even want to change (because we need to get to the eigth parameter):
many_args(10, 5, 0, 1, 0, 5, 5, -5)This is where keyword arguments come to the rescue. Whenever we call a function, we can always explicitly include the parameter name in addition to its specific value like this:
many_args(a=10, b=5, h=-5)That way, arguments that keep their default values do not need to be passed to the function. In addition, keyword arguments make it obvious which arguments we are actually passing.
We can even mix positional and keyword arguments to optimize conciseness and readability:
many_args(10, 5, h=-5)The first two arguments are matched by position, whereas the third argument is matched by name.
Here is another function to further illustrate the concept (taken directly from the official Python tutorial):
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
print("-- This parrot wouldn't", action, end=' ')
print("if you put", voltage, "volts through it.")
print("-- Lovely plumage, the", type)
print("-- It's", state + "!")The following function calls are valid (outputs omitted):
# 1 positional argument
parrot(1000)
# 1 keyword argument
parrot(voltage=1000)
# 2 keyword arguments
parrot(voltage=1000000, action='VOOOOOM')
# 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)
# 3 positional arguments
parrot('a million', 'bereft of life', 'jump')
# 1 positional, 1 keyword argument
parrot('a thousand', state='pushing up the daisies')In contrast, all of these function calls produce an error:
parrot()--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[25], line 1 ----> 1 parrot() TypeError: parrot() missing 1 required positional argument: 'voltage'
parrot(voltage=5.0, 'dead')Cell In[26], line 1 parrot(voltage=5.0, 'dead') ^ SyntaxError: positional argument follows keyword argument
parrot(110, voltage=220)--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[27], line 1 ----> 1 parrot(110, voltage=220) TypeError: parrot() got multiple values for argument 'voltage'
parrot(230, actor='John Cleese')--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[28], line 1 ----> 1 parrot(230, actor='John Cleese') TypeError: parrot() got an unexpected keyword argument 'actor'. Did you mean 'action'?
It is instructive to inspect how Python runs code. In general, Python executes code line by line, starting at the very top (the first line).
Whenever Python comes across a line containing a function header, it takes note that this functions exists, but it doesn’t run the function body – this only happens when a function is called (as opposed to defined). Therefore, Python skips the function body and resumes at the first line after the function body.
Consider the following example:
Let’s see how Python runs this script step by step:
a = 5.print("Hello"). This function is a built-in function, so we don’t know which code gets executed when we call this function (it is defined somewhere else outside our script).test function in line 4, taking note that this function requires two arguments x and y when called.print("World") on line 9.test function with arguments 5 and 6 on line 10. This means that it jumps to the function header in line 4 and assigns concrete values x=5 and y=6 to both parameters.x + 2 and re-assigns the name x to this result (after this line, x is equal to 7).y by 3, so y is now 3.x * y, which equals 7 * 3 or 21.z to the return value of the function, so z gets the value 21.print(z) on line 11, which prints 21 to the screen.You can use the debug facilities of IDLE to step through the code. First, click anywhere in the first line to create a breakpoint (the line will be highlighted in yellow). Then, click on the “Debug” > “Debugger” (this menu is only available in the IDLE shell and not in the IDLE editor window). A new window titled “Debug Control” will open, and you can run this file as usual. Python will stop at the first breakpoint and you can step through the code line by line. The “Debug Control” window provides buttons to step into, over, or out of functions, as well as to continue running the code until the next breakpoint is reached. This window also shows the current state of all local variables (objects that you have given names in your code).

In the previous example, the function test had two parameters x and y, which are only accessible within the function. These names do not exist outside the function, so they are bound to the local scope of that function.
Some additional examples further illustrate the scoping rules of Python. Consider the following function definition and subsequent function call:
def test():
s = 15 # s only exists in the function body
print(s)
test()15
Since s only exists in the function body (in its local scope), we get an error if we try to access it outside the function:
print(s)--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[30], line 1 ----> 1 print(s) NameError: name 's' is not defined
Interestingly, we can access names from higher-level scopes without any problem. For example, we can use names defined in the global scope (outside of any function) inside a function body (but not vice versa):
s = 15 # global scope
def test():
print(s) # s from global scope
test()
print(s)15
15
In Python, local names can shadow global names. In the following example, the local name s inside the function body shadows the global name s. This means that inside the function, a different (local) name s is used. Outside the function, only the global s exists.
s = 15
def test():
s = 12 # local s shadows global s
print(s)
test()
print(s)12
15
Now this is where things can get tricky. Inside functions, we can access global names, but we are only allowed to read their values and not modify them unless we take special measures. If a function contains an assignment to a name, this name is automatically treated as a local name. The following example will throw an error because the function is trying to change the local name before it is defined:
s = 15
def test():
print(s) # local s does not exist yet, so we can't print it
s = 12 # here we define our local s which shadows the global s
print(s)Calling that function results in an error:
test()--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) Cell In[34], line 1 ----> 1 test() Cell In[33], line 4, in test() 3 def test(): ----> 4 print(s) # local s does not exist yet, so we can't print it 5 s = 12 # here we define our local s which shadows the global s 6 print(s) UnboundLocalError: cannot access local variable 's' where it is not associated with a value
If we really meant to access the global s, we need to tell Python by using the global statement:
s = 15
def test():
global s # we want to access the global s
print(s)
s = 12 # modifies global s
print(s)
test()
print(s)15
12
12
The solution to the previous example with the global statement is not ideal and should be avoided whenever possible. If you need to access a name from the global scope, it is better to pass this value to the function as an argument:
s = 15
def test(s):
print(s)
s = 12
print(s)
print(s)
test(s)
print(s)15
15
12
15
Note that this does not change the global s, because the function argument s merely shadows it.
If we also want to modify a value from the global scope, we could have the function return a value, which we can re-assign in global scope:
s = 15
def test(s):
print(s)
s = 12
print(s)
return s
print(s)
s = test(s)
print(s)15
15
12
12
This is the recommended Pythonic solution if you want to modify a value from the global scope inside a function.
Define a function add_one which increments and returns the supplied argument (a number) by one. Then evaluate the expression add_one(add_one(add_one(13))).
Define a function mult which multiplies its two input arguments and returns their product. The second parameter should have a default value of 1 so that the function can also be called with only one argument. Test your function with the following three function calls:
mult(3, 7)
mult(12)
mult(2, mult(8, 8))Define a function to_fahrenheit which converts its argument (a temperature in degrees Celsius) to degrees Fahrenheit and returns this result. Furthermore, define another function to_celsius which performs the opposite conversion (from degrees Fahrenheit to degrees Celsius). Verify both functions with the following function calls (feel free to test additional temperature values):
to_fahrenheit(0)
to_celsius(100)
to_celsius(to_fahrenheit(38))Define a function nonsense with three parameters a, b, and c. Arguments b and c should be optional and default to 10 and 13, respectively. The function should return the result of a**2 - b * 2 + c**2. Call the function in the following ways:
Write down each function call, each return value, and the values for each of the three arguments. Make sure that the function calls do not throw errors – this is always possible except for the first case (no arguments)!
This document is licensed under the CC BY-NC-SA 4.0 by Clemens Brunner.