Function fundamentals in Python

Think of functions as shortcuts for executing blocks of code.

Summary

If we think of variables as a sort-of label for data values, then think of functions as another kind of label, but for code that is meant to be called at a later time. Just as it is convenient to give a human-readable name – a.k.a. assigning a variable – to a complicated data object for later reference, it’s convenient ttindo give a label/nickname to a series of expressions that we intend to execute again.

Table of contents

How to use a function?

We've been using functions from the very start:

print("hello world")

The token print is not a special Python keyword. It's a name assigned to a block of code that handles the work of taking an argument – "hello world" – and printing it to screen. The actual code that does that work is much more complex than you probably thought:

Imagine typing all of those instructions just to print something to screen. But we never have to actually do that. By having all the Python instructions needed to print to screen wrapped up in the label, print, we just have to remember that label (i.e. print) and call it with the appropriate arguments:

print("hello world", "it's", "me")

Defining our own Python functions

Why do we want to define our own functions? Because we're lazy, and we're tired of having to copy and paste the same code, over and over, when we need to do something repeatedly. Defining our own functions let's us, at a bare minimum, clean up our scripts and make things much more readable.

Defining a Python function is almost as easy as using them, but more complicated than assigning a variable.

Basically, we use the def keyword, then pick a name, and then write the code we want to save for later execution as an indented block.

The rest of this guide focuses on the bare syntax for defining functions. Check out Al Sweigart's Chapter 3: Functions, via Automate the Boring Stuff for a good walkthrough that covers functions and some of their related topics.

Syntax for the basic Python function

Here's a very basic function:

def hello_world():
    print("Hello world!")

Jump into the interactive Python shell and type out the code above. Nothing should happen because nothing is being executed when we're simply defining the function.

The function's code only executes when we call the function by name:

>>> hello_world()
Hello world!

Note that the parentheses have to be included to indicate that we want to execute the function. Otherwise, the function acts just as any another Python object:

>>> hello_world
<function __main__.hello_world>
>>> type(hello_world)
function

Syntax components of a basic Python function

From the Python documentation:

The keyword def introduces a function definition. It must be followed by the function name and the parenthesized list of formal parameters. The statements that form the body of the function start at the next line, and must be indented.

The function example from above does not have a parenthesized list of formal parameters. We'll deal with that example later.

Essentially, a function can be broken down to this bare form:

def the_function_name():
    what_the_function_does   

the def keyword

This is just one of those Python key words you memorize. It is short for, ``

The function name

This can basically have the same conventions as a variable name. In general, stick to lowercase letters and underscore characters.

As for what kind of code can go inside the indented code block; anything that is valid Python code. Just like indented blocks for conditional branches and for-loops.

A function that returns something

One subtle aspect of the print function is that it does not return a value. What does that mean? It's easier to demonstrate a function that does return a value: len

>>> x = len("hello")

The variable x contains the value 5, because the len function is designed to return an integer that represents the length of whatever value was passed to it.

Now try it with print():

>>> pval = print("hello")
hello

The function obviously executed, because hello is printed to screen. But what is assigned to the variable pval? Nothing

>>> type(pval)
NoneType

The return keyword

The simple hello_world function didn't return anything because it simply called the print function. To have our function actually return a value, we need to use the return keyword – yet another of one of Python's few keywords:

def hello_world_again():
    return "Hello world!"

Try this out in your interactive console:

>>> txt = hello_world_again()
>>> type(txt)
str

Note that nothing is printed to screen. That's because hello_world_again doesn't call the print function itself. If we want to print the result of hello_world_again, we have to call print ourselves:

>>> print(hello_world_again())
Hello world!

Again, note that the closed parentheses have to follow the function name in order for it to be executed. That is, you probably don't want to print the function itself, like this:

>>> print(hello_world_again)
<function hello_world_again at 0x10c289bf8>

A return statement effectively ends a function; that is, when the Python interpreter executes a function's instructions and reaches the return, it will exit the function at that point.

The function below will always just return "hello":

def foo_a():
    return "hello"
    return "world"

Functions with arguments

The function examples so far have been very dull. The use of arguments allows us to call a function for different situations, making functions vastly more useful.

Here's a simple example:

def hello_there(someone):
    print("Hello, %s!" % str(someone).upper())
>>> hello_there('dan')
Hello, DAN!
>>> hello_there(42 * 42)
Hello, 1764!

In the function definition, the arguments are in the parentheses that follow the function name. Think of them as variable names to refer to in the function body. At the time that we define the function, we don't know exactly what values the user will pass to it.

Functions can have more than one argument; use a comma to separate each argument:

def sup(first_name, last_name):
    print("Sup, %s %s?" % (last_name, first_name))
>>> sup('dan', 'nguyen')
Sup, nguyen dan?

Arguments can be defined as optional by assigning a default value to them in the argument list:

def sup_2(first_name, last_name='Doe'):
    print("Sup, %s %s?" % (last_name, first_name))
>>> sup_2('John', "Johnson")
Sup, John Johnson?
>>> sup_2('John')
Sup, John Doe?

Wrapping requests and JSON

When should you define a function? Whenever you want. But typically, we define functions for things that we do, over and over.

For example, we have repeatedly been fetching files from the web and, in the case of JSON files, deserializing them into Python objects:

import requests
import json
resp = requests.get("http://stash.compciv.org/congress-twitter/2016-01/sentedcruz.json")
mydata = json.loads(resp.text)

If we had to do that for many JSON files (that require downloading from the web), we'd have to copy-paste that snippet of code, over and over. It's repetitive. And it's prone to errors.

So let's make it into a function. The main strategy in designing a function is knowing:

What is it I need to do?

Well, given a URL for a JSON file, we need to download it and turn it into a data object. We just did that above, so let's just copy-paste that code and throw it into a function body named fetch_json:

def fetch_json():
    import requests
    import json
    resp = requests.get("http://stash.compciv.org/congress-twitter/2016-01/sentedcruz.json")
    mydata = json.loads(resp.text)

What should the function return?

If you copy-paste that code above and then call the function:

>>> fetch_json()

It will work. But it doesn't actually return anything, so it will, for all practical purposes, not be useful.

So let's just have it return the deserialized object:

def fetch_json():
    import requests
    import json
    resp = requests.get("http://stash.compciv.org/congress-twitter/2016-01/sentedcruz.json")
    mydata = json.loads(resp.text)
    return mydata

Note that mydata, and all the other variable names, do not have meaning outside the scope of the function definition. In other words, you don't have to worry about resp and mydata clashing with other places that you've used those same variable names.

>>> thing = fetch_json()
>>> type(thing)
dict
>>> thing['name']
'Senator Ted Cruz'

What can I abstract?

This is where we think about arguments. The fetch_json function works…but only if we want to keep fetching the stashed version of Ted Cruz's Twitter profile.

So, what is the part of the function that is repeatable. And what is the part of the function that changes?

In this case, the url is the thing that we'd like to vary each time. So let's make that an argument:

def fetch_json(url):
    import requests
    import json
    resp = requests.get(url)
    mydata = json.loads(resp.text)
    return mydata

Now, we can point it to any URL we want that purportedly contains JSON formatted text. Try calling fetch_json on these URLs out for yourself:

More reading

Check out Al Sweigart's Chapter 3: Functions, via Automate the Boring Stuff for a good walkthrough that covers functions and some of their related topics.

References and Related Readings

Chapter 3: Functions
Defining functions