Python Functions and Best Practices

Python functions are small, reusable blocks of code that you can utilize throughout a Python project. The standard Python library provides many useful built-in functions such as print()len()str()int() but you can also define your own functions that can be used in your code. In this article, you'll learn how to write more concise, robust and readable Python code using functions.

When to use functions?

As your Python project starts to get more complex you'll usually find yourself having to write the same code repeatedly. Writing repetitive code is a bad idea because aside from the obvious fact that it’s tedious, it adds unnecessary visual clutter to your code base, it’s prone to human error if it’s more than a few lines, it will need to be changed in multiple places if you need to add new functionality to that process (say better error handling) and finally you have no way of writing unit or integration tests.

So instead of writing repetitive code, you can use Python functions. When is the right time to use functions? Well, it's really up to you but as a rule of thumb if you find yourself writing the same code pattern more than twice then you should probably think about investing some time into writing a function that you can then reuse whenever you need to in your codebase. There’s no shame in going back to change your old code to use a function if you start to see that you’re needing to write the same thing over and over. That’s what refactoring is!

Functions are extremely useful because they allow a piece of code to be written once and then invoked in a single line wherever needed throughout your code. You then only need to make improvements to one place in your code and if the function is well named then whoever reads through your code can easily infer what the function does by looking at its name and arguments instead of reading multiple lines of code.

Defining a Function

A simple example

Here is a simple example of how we can define a function (lines 1 -3) that adds two numbers together. We'll then assign the value that the function returns to a variable called sum (line 5) and print (line 6) the value of that out to the console.

def add(a, b):
    result = a + b
    return result

sum = add(5, 7)
print(sum)
# 12

Okay, let's walk through the definition of our function from lines 1 to 3. Starting a line with def tells the Python interpreter that we are defining a function, we then name the function and specify the parameters, the data that the function takes as input in parenthesis. In this case, we specify parameters as a and b. On the second line, we tell Python that whatever is supplied as a and b should be added together and the result should be assigned to a variable called result. Finally, on line 3, we tell the function to return the result variable.

Note that it is a convention to have two blank lines after you define a function.

It's also a common convention to return a Python expression directly, instead of assigning it to a variable. Here's another way that we can re-write the same function to be more concise:

def add(a, b):
    return a + b

A more complex example

At this point you might be thinking, why is this useful when I could just write a + b in my code? Well, let's take a look at a slightly more complex example where we create a function that returns the highest number in a list of numbers.

def highest_number(numbers):
    highest = 0
    for n in numbers:
        if n > highest:
            highest = number
    return highest

So, in this case, we've specified a parameter called numbers which we then loop through on line 3. Finally, our function should return a value, in this case, the highest number. If you don't specify anything to be returned, your function will return None.

Note that if your function name is more than one word you should use 'snake_case'.

Now that we’ve defined our highest_number() function in our Python code we can call it, pass it an argument (a list of scores in this case) and assign the result it returns to a variable:

scores = [21, 14, 72, 148, 54]
top_score = highest_number(scores)

print(top_score)
# 148

See how clean that is? We now have a function that performs several operations but is concisely expressed in a single line and the result can be assigned to a variable called top_score. We can now use this function wherever we need to in our code and if we make improvements to that function, say to handle a scenario where someone supplies a string as an argument instead of a list, then every place in our code that uses the function will receive that new enhancement automatically.

Additionally, we can now also write unit tests for this function to test that it performs correctly with different scenarios like correct or incorrect input. Writing tests for your code may seem like an unnecessary burden when you're starting out but it becomes increasingly useful when you start building more complex Python programs. For example, if you're building a web application with Django you'll have a bunch of URLs that all point to functions which render various web pages. As your website grows you don't want to manually check that every page is still working when you make a change to your code, it's much faster and more effective to run a suite of tests and get notified if anything is broken.

Now that we're more familiar with how functions work, let's examine the various aspects of a Python function more closely.

Parameters & arguments

Parameters are specified in parenthesis when you're defining a function, arguments are the data that you pass into the function's parameters when you call the function. In the case with the code below, a and b are the _parameters_for the add() function, whereas 5 and 7 are the arguments that are passed into the function when it is called.

Functions can have multiple parameters separated by a comma, like this:

def add(a, b):
    return a + b

result = add(5, 7)

When you call a function you need to supply arguments for all of the parameters that the function expects, otherwise, the Python interpreter will raise a TypeError and your code will stop running if this error is not handled. You'll also need to supply your arguments in the same order that the function parameters are specified so that Python assigns the arguments to the correct parameters.

As a rule of thumb, you should try to keep the number of parameters for a function as low as possible so that it's easier for you and other people to remember what arguments your function takes and in what order. Having fewer parameters also reduces the temptation to try and do too much work inside your function. Functions should have a single responsibility, which we'll discuss as part of best practices further on.

Keyword arguments

Keyword arguments are liked named parameters, this is how you can define and use them:

def greet(greeting, name=None):
    if name:
        print(greeting + " " + name)
    else:
        print(greeting + " there")

# Call function with keyword argument
greet('Hello', name='Alex')
# Hello Alex

# Call function without keyword argument
greet('Hello')
# Hello there

They can make your code easier to read and also allow you to specify a default argument if no argument has been provided when the function is called. In the above example, the greet() function will print the persons name if it is provided as the name keyword agument. If the name keyword is not provided then the function will execute the else code b While normal arguments need to be in the correct order and passed into a function before keyword arguments, keyword arguments don't need to be called in any specific order as they are named and so the Python interpreter knows where to assign them. Just remember that keyword arguments must always be put after normal parameters when defining and calling your function.

return

The return statement is used to tell your function to return a value. Typically you will assign the result returned from the function to a variable. In the code below, we'll define a function and then call it and have the result assigned to the result variable.

def is_even(num):
    if num % 2 == 0:
        return True
    return False

result = is_even(6)
print(result)
# True

Notice that we can have more than one return statement for different results. Once the Python interpreter reaches a return statement, it will return the expression that follows it and no other code will be run. The % (modulus) operator used above returns only the remainder of a division calculation. So you write 5 % 2 Python will divide 5 by 2 which equals 2.5 and only return the number to the right of the decimal point, in this case, 0.5. Using the modulus operator to divide by two is a common technique used to check if a number is even.

*args

At some point, you may come across a function definition that has a strange looking parameter with an * prefix, like this:

def throw_party(host, *args):
    print(host, 'is throwing a party!')
    for guest in guests:
        print(guest, 'has arrived.')

Basically, the * operator is used to handle an unknown number of arguments. You'd use this when you may have a situation where you could have a variable number of arguments that you'd want to handle. For example, now we can use this function with any number of guests:

throw_party('Anika', 'Jethrow', 'Roxanne', 'Sadiyah', 'Andrei')

which will produce the following result:

Anika is throwing a party!
Jethrow has arrived.
Roxanne has arrived.
Sadiyah has arrived.
Andrei has arrived.

You don't have to name this parameter as *args, it's just a typical convention that is used. You could just as well use *guests and your code would continue to work just the same.

**kwargs

Similarly, you might see a function that makes use of a **kwargs parameter like this:

def scoreboard(game, **kwargs):
    print(game, "scores:")
    for key, value in kwargs.items():
        print(key, value)

**kwargs is similar to *args except it is used to tell a function to accept any number of keyword arguments, which can be called like this:

scoreboard("Space Invaders", player_a=98, player_b=250, player_c=176)

this will then output the following results:

Space Invaders scores:
player_a 98
player_b 250
player_c 176

Now that we've looked at the different ways that you can define functions in Python let's look at some best practices.

Best Practices

Single responsibility principle

You may have heard of the single responsibility principle. This means that your functions should only focus on handling a single aspect of your program.

For example, say that you want to write some code that does three things: fetch a web page, extract data from the page and print the data out to the terminal. You wouldn't write a function that extracts the data from the web page AND prints the results out to the terminal, because perhaps one day you might need to change your code to save those results to a file instead or push them to an API.

If you try to handle all three of these output cases in your function, you'd probably end up having to add another parameter to specify which output to use, and then add that argument to every place in your code that calls the function. Then what about specifying a filename for when saving the results to a file? Or the URL and access credentials for pushing the results to an API? You can see how breaking your code down into smaller reusable functions prevents it from becoming exponentially more complicated and keeps it flexible for reusing around your codebase.

Naming your functions

A well-named function should almost read like a sentence when used. When someone is reading the code it should be immediately obvious what is happening in the code. A good rule of thumb is to design and name your function so that it can be used to simplify a more complex set of operations into a single expression. For example, we can reuse the function we wrote earlier in this article to perform all of those operations in an expression on line 3 below:

def increment(num, by):
    return num + by

result = increment(5, 2)
print(result)
# 7

Docstrings

When you write your Python functions you can write docstrings to add inline documentation to your functions. They can be one-liners or multi-line docstrings. Here is an example of how you'd write a one-liner docstring for a function:

def double(*numbers):
    """Take numbers as args, return a list of the numbers doubled."""
    results = []
    for n in numbers:
        results.append(n*2)
    return results

There are a few things to note about docstrings:

  • they should be enclosed in three double quotes with no space at the beginning or end
  • they should always occur on the first line of a function, with no blank line above or below it
  • they should be written as a concise sentence in the tone of a command (take this and return that) instead of a description (takes argument and returns a value) and end with a period.
  • try to indicate what data type or data structure the function will return (e.g. number, string, list, dictionary)

By doing this, when people start using your functions their IDE will usually display the docstring to them to assist when using your function. This is a useful way to provide more information about the parameters your function accepts and what it returns.

Multi-line docstrings should have one summary line followed by a blank line and then a description.

Organizing your functions

Once you start writing multiple functions for your Python project you should think about how you want to organise them. If it's a simple project with only a few functions you may want to define them at the beginning of your Python module. If your project size is a bit bigger you may want to put them into their own module so that they can be imported into your code and used wherever. You may also want to create methods for classes. These topics are beyond the scope of this article for now but are worth you doing some research on to learn more.

That's it for your introduction to Python's functions! Once you've had some practice it's recommended that you start learning about Python Classes. Classes are the foundation of object-oriented programming in any language and are an essential part of professional programming.


Samuel Morhaim picture

What should a function return to be consistent?

highestnumber(['some text']) # An exception? None? -1? False highestnumber([120, 120]) # An array? Again none, -1, False? both numbers?

In my situation, I have a route that calls a service function (todos). The function could return an array of todos? An empty array? A False, "Error message", or a dict {data: [todos], status: True } or {data; None, status: False, message: "Some error"}

What is the right way?

Rhett Trickett picture

Hi Samuel,

If I recall correctly functions from the Python standard library will raise a ValueError exception if they are passed an argument type that is not supported. So that seems like the reasonable thing to do.

If you pass an array containing two identical numbers to the function, in your second case, it should just return a single number which is the highest in the sequence, so 120.

Finally, it seems like you may building a web API for your todos. If you don't want to return any meta data about the response then you can just return an array of todo objects. However, sometimes you will also want to include additional info in your response. For example, you may have a large number of todos and only want to return 20 at a time, so it would be useful to include the total number in there as well so that someone can determine how many 'pages' they have to iterate through. For example:

{
    total: 99999999
    todos: [
        {...},
        {...},
        {...},
    ]
}

It's up to you how you want to define your response, just make sure it's documented so other people know how to use it :)

Another thing would be to make use of an HTTP status code (200:Success, 403:Forbidden, 404:Not Found etc) in your HTTP response. This is the standard way to help users or your API handle various situations.

If you are using a web framework to build your API they typically come with helpful utilities that let you pass your dictionary in and set a status code, which generates the appropriate HTTP response text.

For example, Django has a JsonResponse class. This allows you to just pass in your dictionary data and a status code and it will generate an appropriate HTTP response containing your dictionary data as JSON with the appropriate status code.

from django.http.response import JsonResponse

def todos(request)
    # route function code goes here
    return JsonResponse({"error": "not found"}, status=404)

If your APIs typically return JSON it would be a good idea to serve any error messages in the same format.