Writing functions

Author

Marie-Hélène Burle

Python comes with a number of built-in functions. Packages can provide additional ones. In many cases however, you will want to create your own functions to perform exactly the computations that you need.

In this section, we will see how to define new functions.

Syntax

The function definition syntax follows:

def <name>(<arguments>):
    <body>

Once defined, new functions can be used as any other function.

Let’s give this a try by creating some greeting functions.

Function without argument

Let’s start with the simple case in which our function does not accept any argument:

def hello():
    print('Hello!')

Then we call it:

hello()
Hello!

This was great, but …

hello('Marie')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[88], line 1
----> 1 hello('Marie')

TypeError: hello() takes 0 positional arguments but 1 was given

… it does not accept arguments.

Function with one argument

Let’s step this up with a function which can accept an argument:

def greetings(name):
    print('Hello ' + name + '!')

This time, this works:

greetings('Marie')
Hello Marie!

However, this does not work anymore:

greetings()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[91], line 1
----> 1 greetings()

TypeError: greetings() missing 1 required positional argument: 'name'

🙁

F-strings

To be more fancy, you can use a formatted string literal or f-string instead of a simple string. F-strings allow to include the expressions that are replaced by arguments to be included inside the string and to format them.

To use them, you use f or F just before the string expression (without space) as in f'This is a formatted string literal'. Then you include the expressions that will be replaced by arguments inside the string, but in curly braces as in f'This is a formatted string literal with an {expression}'.

Example:

def greetings(name):
    print(f'Hello {name}!')

greetings('Marie')
Hello Marie!

Note the difference in syntax. Here, we aren’t using + anymore as we aren’t concatenating a series of strings. Instead, we create a single string which includes the expression name that will be replaced by the argument.

With f-strings, you can now add formatting to the output.

Example:

# Add quotes around the expression
def greetings(name):
    print(f'Hello {name!r}!')

greetings('Marie')
Hello 'Marie'!

You can explore more tricks that can be done with f-strings in the official Python tutorials.

Function with a facultative argument

Let’s make this even more fancy: a function with a facultative argument. That is, a function which accepts an argument, but also has a default value for when we do not provide any argument:

def howdy(name='everyone'):
    print(f'Hello {name}!')

We can call it without argument (making use of the default value):

howdy()
Hello everyone!

And we can call it with an argument:

howdy('Marie')
Hello Marie!

This was better, but …

howdy('Marie', 'Alex')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[97], line 1
----> 1 howdy('Marie', 'Alex')

TypeError: howdy() takes from 0 to 1 positional arguments but 2 were given

… this does not work.

Function with two arguments

We could create a function which takes two arguments:

def hey(name1, name2):
    print(f'Hello {name1} and {name2}!')

Which solves our problem:

hey('Marie', 'Alex')
Hello Marie and Alex!

But it is terribly limiting:

# This doesn't work
hey()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[100], line 2
      1 # This doesn't work
----> 2 hey()

TypeError: hey() missing 2 required positional arguments: 'name1' and 'name2'
# And neither does this
hey('Marie')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[101], line 2
      1 # And neither does this
----> 2 hey('Marie')

TypeError: hey() missing 1 required positional argument: 'name2'
# Nor to mention this...
hey('Marie', 'Alex', 'Luc')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[102], line 2
      1 # Nor to mention this...
----> 2 hey('Marie', 'Alex', 'Luc')

TypeError: hey() takes 2 positional arguments but 3 were given

Function with any number of args

Let’s create a function which handles all cases.

We will have to break it down into the various scenarios, but we already saw how to do this in the previous lesson with if statements.

The scenarios are:

  • no name given (we need to set some default somehow),
  • one name given (no grammar syntax needs adding),
  • two names given (we need to add “and”),
  • more than two names (we need to add commas after all but the last name and we need to add “and” before the last name).

We saw above how to create a default value. Here, we will use a different approach that will make our life easier. We will use the argument as the list of names. That allows us to get its length (to see which scenario we are in) and to index it (to add the grammar syntax at the right place).

Finally, we need a way to make the function work with any number of arguments. To do this, we use an arbitrary argument list with * followed by a name. When the function already accepts some arguments, by convention, people use *args to signify that any number of additional arguments can be passed to the function. But you can use any name preceded by the asterisk.

Here, because we will only use the starred argument, let’s call it *names.

This and that Stack Overflow questions attracted a lot of very useful answers to explain the concepts of * and **.

Here is our function:

def hi(*names):
    # Case 1: No names were provided.
    if not names:
        print("Hello everyone!")
        return

    # Case 2: Only one name was provided.
    if len(names) == 1:
        # names is a tuple, so we access the first element with names[0]
        print(f"Hello {names[0]}!")
        return

    # Case 3: Two names were provided.
    if len(names) == 2:
        print(f"Hello {names[0]} and {names[1]}!")
        return

    # Case 4: Three or more names were provided (the general case).
    # We take all names except the last one for the main list.
    all_but_last = names[:-1]
    last_person = names[-1]

    # We join the main list with commas.
    greeting_list = ", ".join(all_but_last)

    # Then we construct the final sentence.
    print(f"Hello {greeting_list}, and {last_person}!")

Let’s test it:

hi()
hi('Marie')
hi('Marie', 'Alex')
hi('Marie', 'Alex', 'Luc')
hi('Marie', 'Alex', 'Luc', 'Grace')
Hello everyone!
Hello Marie!
Hello Marie and Alex!
Hello Marie, Alex, and Luc!
Hello Marie, Alex, Luc, and Grace!

Everything works! 🙂

Note the presence of the keyword return in this function. When the return statement is encountered during a function execution, the function terminates immediately and any code after that statement is not executed. This is why we could write this function with a series of if statements.

Instead, we could have written our function using if elif else statements as we saw in the previous lesson.

Your turn:

Write a version of this function that does not use return to exit the function, but uses if elif else statements instead.

Documenting functions

It is a good habit to document what your functions do. As with comments, those “documentation strings” or “docstrings” will help future you or other users of your code.

PEP 257—docstring conventions—suggests to use single-line docstrings surrounded by triple quotes.

Remember the function definition syntax we saw at the start of this chapter? To be more exhaustive, we should have written it this way:

def <name>(<arguments>):
    """<docstrings>"""
    <body>

Example:

def hi(*names):
    """Greets a variable number of people with proper grammar."""
    # Case 1: No names were provided.
    if not names:
        print("Hello everyone!")
        return

    # Case 2: Only one name was provided.
    if len(names) == 1:
        # names is a tuple, so we access the first element with names[0]
        print(f"Hello {names[0]}!")
        return

    # Case 3: Two names were provided.
    if len(names) == 2:
        print(f"Hello {names[0]} and {names[1]}!")
        return

    # Case 4: Three or more names were provided (the general case).
    # We take all names except the last one for the main list.
    all_but_last = names[:-1]
    last_person = names[-1]

    # We join the main list with commas.
    greeting_list = ", ".join(all_but_last)

    # Then we construct the final sentence.
    print(f"Hello {greeting_list}, and {last_person}!")

PEP 8—the style guide for Python code—suggests a maximum of 72 characters per line for docstrings.

If your docstring is longer, you should create a multi-line one. In that case, PEP 257 suggests to have a summary line at the top (right after the opening set of triple quotes), then leave a blank line, then have your long docstrings (which can occupy multiple lines), and finally have the closing set of triple quotes on a line of its own:

def <name>(<arguments>):
    """<summary docstrings line>"""

    <more detailed description>
    """
    <body>

Example:

def hi(*names):
    """
    Greets a variable number of people with proper grammar.

    This function uses *args to accept any number of string arguments.
    """
    # Case 1: No names were provided.
    if not names:
        print("Hello everyone!")
        return

    # Case 2: Only one name was provided.
    if len(names) == 1:
        # names is a tuple, so we access the first element with names[0]
        print(f"Hello {names[0]}!")
        return

    # Case 3: Two names were provided.
    if len(names) == 2:
        print(f"Hello {names[0]} and {names[1]}!")
        return

    # Case 4: Three or more names were provided (the general case).
    # We take all names except the last one for the main list.
    all_but_last = names[:-1]
    last_person = names[-1]

    # We join the main list with commas.
    greeting_list = ", ".join(all_but_last)

    # Then we construct the final sentence.
    print(f"Hello {greeting_list}, and {last_person}!")

You can now access the documentation of your function as you would any Python function:

help(hi)
Help on function hi in module __main__:

hi(*names)
    Greets a variable number of people with proper grammar.

    This function uses *args to accept any number of string arguments.

Or:

print(hi.__doc__)

Greets a variable number of people with proper grammar.

This function uses *args to accept any number of string arguments.

Returning values

So far, all the functions we looked at printed something. Often, you will want your functions to calculate some result. This result needs to be “returned”. This is also done with the keyword return that we saw above, this time followed by the value(s) to be returned.

Let’s create a dummy function:

def add_one(value):
    value + 1

and test it:

add_one(4)

We don’t get any result. 🤔

That’s because our function is not returning anything. To fix it, we need to return the result:

def add_one(value):
    return value + 1

Now it works:

add_one(4)
5

Printing vs returning

So what’s the difference between printing and returning?

Printing is called a side-effect: it modifies the state of the terminal by displaying some text on it, but it doesn’t return any value to the program (in fact it returns None):

def test_print():
    print('Printing function')

a = test_print()
Printing function
type(a)
NoneType
print(a)
None

On the contrary, returning a value makes it available to the program:

def test_return():
    return 3

a = test_return()
type(a)
int
print(a)
3

Your turn:

Write a function that calculates an area. It should:

  • be documented,
  • accept 2 arguments: length and width,
  • print an error message if length and/or width is negative.