Basics of Functions

Note:

There are reading-comprehension exercises included throughout the text. These are meant to help you put your reading to practice. Solutions for the exercises are included at the bottom of this page.

Defining a function allows you to encapsulate a segment of code, specifying the information that enters and leaves the code. You can make use of this “code-capsule” repeatedly and in many different contexts. For example, suppose you want to count how many vowels are in a string. The following defines a function that accomplishes this:

def count_vowels(in_string):
    """ Returns the number of vowels contained in `in_string`"""
    num_vowels = 0
    vowels = "aeiouAEIOU"

    for char in in_string:
        if char in vowels:
            num_vowels += 1  # equivalent to num_vowels = num_vowels + 1
    return num_vowels

Executing this code will define the function count_vowels. This function expects to be passed one object, represented by in_string, as an input argument, and it will return the number of vowels stored in that object. Invoking count_vowels, passing it an input object, is referred to as calling the function:

>>> count_vowels("Hi my name is Ryan")
5

The great thing about this is that it can be used over and over!

>>> count_vowels("Apple")
2

>>> count_vowels("envelope")
4

In this section, we will learn about the syntax for defining and calling functions in Python

Definition:

A Python function is an object that encapsulates code. Calling the function will execute the encapsulated code and return an object. A function can be defined so that it accepts arguments, which are objects that are to be passed to the encapsulated code.

The def Statement

Similar to if, else, and for, the def statement is reserved by the Python language to signify the definition of functions (and a few other things that we’ll cover later). The following is the general syntax for defining a Python function:

def <function name>(<function signature>):
    """ documentation string """
    <encapsulated code>
    return <object>
  • <function name> can be any valid variable name, and must be followed by parentheses and then a colon.
  • <function signature> specifies the input arguments to the function, and may be left blank if the function does not accept any arguments (the parentheses must still be included, but will not encapsulate anything).
  • The documentation string (commonly referred to as a “docstring”) may span multiple lines, and should indicate what the function’s purpose is. It is optional.
  • <encapsulated code> can consist of general Python code, and is demarcated by being indented relative to the def statement.
  • return if reached by the encapsulated code, triggers the function to return the specified object and end its own execution immediately.

The return statement is also reserved by Python. It denotes the end of a function; if reached, a return statement immediately concludes the execution of the function and returns the specified object.

Note that, like an if-statement and a for-loop, the def statment must end in a colon and the body of the function is delimited by whitespace:

# wrong indentation
def bad_func1():
x = 1
    return x + 2

# wrong indentation
def bad_func2():
    x = 1
return x + 2

# missing colon
def bad_func3()
    x = 1
    return x + 2

# missing parenthesis
def bad_func4:
    x = 1
    return x + 2

# this is ok
def ok_func():
    x = 1
    return x + 2

Reading Comprehension: Writing a Basic Function

Write a function named count_even. It should accept one input argument, named numbers, which will be an iterable containing integers. Have the function return the number of even-valued integers contained in the list. Include a reasonable docstring.

The return Statement

In general, any Python object can follow a function’s return statement. Furthermore, an empty return statement can be specified, or the return statement of a function can be omitted altogether. In both of these cases, the function will return the ``None`` object.

# this function returns `None`
# an "empty" return statement
def f():
    x = 1
    return
# this function returns `None`
# return statement is omitted
def f():
    x = 1

All Python functions return something. Even the built-in print function returns None after it prints to standard-output!

# the `print` function returns `None`
>>> x = print("hi")
hi

>>> x is None
True

Warning!

Take care to not mistakenly omit a return statement or leave it blank. You will still be able to call your function, but it will return None no matter what!

A function also need not have any additional code beyond its return statement. For example, we can make use of sum and a generator comprehension (see the previous section of this module) to shorten our count_vowels function:

# the returned object of a function can be specified straight-away
def count_vowels(in_string):
    """ Returns the number of vowels contained in `in_string`"""
    return sum(1 for char in in_string if char in "aeiouAEIOU")

Multiple return Statements

You can specify more than one return statement within a function. This can be useful for handling edge-cases or optimizations in your code. Suppose you want your function to compute \(e^{x}\), using a Taylor series approximation. The function should immediately return 1.0 in the case that \(x = 0\):

def compute_exp(x):
    """ Use a Taylor Series to compute e^x """
    if x == 0:
        return 1.0

    from math import factorial
    return sum(x**n / factorial(n) for n in range(100))

If x==0 is True, then the first return statement is reached. 1.0 will be returned and the function will be “exited” immediately, without ever reaching the code following it.

As stated above, a return statement will trigger a function to end its execution immediately when reached, even when subsequent code follows it. It is impossible for multiple ``return`` statements to be visited within a single function call. Thus if you want to return multiple items, then your function must return a single container of those items, like a list or a tuple.

# Returning multiple items from a function
def bad_f(x):
    """ return x**2 and x**3"""
    return x**2
    # this code can never be reached!
    return x**3

def good_f(x):
    """ return x**2 and x**3"""
    return (x**2, x**3)
>>> bad_f(2)
4

>>> good_f(2)
(4, 8)

Inline Functions

Functions can be defined in-line, as a single return statement:

def add_2(x):
    return x + 2

can be rewritten as:

def add_2(x): return x + 2

This should be used sparingly, for exceedingly simple functions that can be easily understood without docstrings.

Arguments

A sequence of comma-separated variable names can specified in the function signature to indicated positional arguments for the function. For example, the following specifies x, lower, and upper as input arguments to a function, is_bounded:

def is_bounded(x, lower, upper):
    return lower <= x <= upper

This function can then be passed its arguments in several way:

Specifying Arguments by Position

The objects passed to is_bounded will be assigned to its input variables based on their positions. That is, is_bounded(3, 2, 4) will assign x=3, lower=2, and upper=4, in accordance with the positional ordering of the function’s input arguments:

# evaluate: 2 <= 3 <= 4
# specifying inputs based on position
>>> is_bounded(3, 2, 4)
True

Feeding a function too few or too many arguments will raise a TypeError

# too few inputs: raises error
is_bounded(3)

# too many inputs: raises error
is_bounded(1, 2, 3, 4)

Specifying Arguments by Name

You can provide explicit names when specifying the inputs to a function, in which case ordering does not matter. This is very nice for writing clear and flexible code:

# evaluate: 2 <= 3 <= 4
# specify inputs using explicit input names
>>> is_bounded(lower=2, x=3, upper=4)
True

You can mix-and-match positional and named input by using position-based inputs first:

# evaluate: 2 <= 3 <= 4
# `x` is specified based on position
# `lower` and `upper` are specified by name
>>> is_bounded(3, upper=4, lower=2)
True

Note that if you provide a named input, all the inputs following it must also be named:

# positional arguments cannot follow named arguments
>>> is_bounded(3, lower=2, 4)
SyntaxError: positional argument follows keyword argument

Default-Valued Arguments

You can specify default values for input arguments to a function. Their default values are utilized if a user does not specify these inputs when calling the function. Recall our count_vowels function. Suppose we want the ability to include “y” as a vowel. We know, however, that people will typically want to exclude “y” from their vowels, so we can exclude “y” by default:

def count_vowels(in_string, include_y=False):
    """ Returns the number of vowels contained in `in_string`"""
    vowels = "aeiouAEIOU"
    if include_y:
        vowels += "yY"  # add "y" to vowels
    return sum(1 for char in in_string if char in vowels)

Now, if only in_string is specified when calling count_vowels, include_y will be passed the value False by default:

# using the default value: exclude y from vowels
>>> count_vowels("Happy")
1

This default value can be overridden:

# overriding the default value: include y as a vowel
>>> count_vowels("Happy", True)
2

# you can still specify inputs by name
>>> count_vowels(include_y=True, in_string="Happy")
2

Default-valued input arguments must come after all positional input arguments in the function signature:

# this is ok
def f(x, y, z, count=1, upper=2):
    return None
# this will raise a syntax error
def f(x, y, count=1, upper=2, z):
    return None

Reading Comprehension: Functions and Arguments

Write a function, max_or_min, which accepts two positional arguments, x and y (which will hold numerical values), and a mode variable that has the default value "max".

The function should return min(x, y) or max(x, y) according to the mode. Have the function return None if mode is neither "max" nor "min".

Include a descriptive doc-string.

Accommodating an Arbitrary Number of Positional Arguments

Python provides us with a syntax for defining a function, which can be called with an arbitrary number of positional arguments. This is signaled by the syntax def f(*<var_name>).

# The * symbol indicates that an arbitrary number of
# arguments can be passed to `args`, when calling `f`.
def f(*args):
    #  All arguments passed to `f` will be "packed" into a
    #  tuple that is assigned to the variable `args`.
    # `f()`  will assign `args = tuple()`
    # `f(x, y, ...)` will assign `args = (x, y, ...)`
    return args

Because Python cannot foresee how many arguments will be passed to f, all of the objects that are passed to it will be packed into a tuple, which is then assigned to the variable args:

# pass zero arguments to `f`
>>> f()
()

# pass one argument to `f`
>>> f(1)
(1,)

# pass three arguments to `f`
>>> f((0, 1), True, "cow")
((0, 1), True, "cow")

This syntax can be combined with positional arguments and default arguments. Any variables specified after a packed variable must be called by name:

def f(x, *seq, y):
    print("x is: ", x)
    print("seq is: ", seq)
    print("y is: ", y)
    return None
>>> f(1, 2, 3, 4, y=5)  # `y` must be specified by name
x   is:  1
seq is:  (2, 3, 4)
y   is:  5
>>> f("cat", y="dog")  # no additional positional arguments are passed
x   is:  "cat"
seq is:  ()
y   is:  "dog"

Reading Comprehension: Arbitrary Arguments

Write a function named mean, which accepts and arbitrary number of numerical arguments, and computes the mean of all of the values passed to the function. Thus mean(1, 2, 3) should return \(\frac{1 + 2 + 3}{3} = 2.0\)

This function should return 0. if no arguments are passed to it. Be sure to test your function, and include a docstring.

We see that * indicates the packing of an arbitrary number of arguments into a tuple, when used in the signature of a function definition. Simultaneously, * signals the unpacking of an iterable to pass each of its members as a positional argument to a function, when used in the context of calling a function:

# Using `*` when calling a function, to unpack an
# iterable. Passing its members as distinct arguments
# to the function

def f(x, y, z):
    return x + y + z

>>> f(1, 2, 3)
6

# `*` means: unpack the contents of [1, 2, 3]
# passing each item as x, y, and z,
# respectively
>>> f(*[1, 2, 3])  # equivalent to: f(1, 2, 3)
6

In the following example, we use * to:

  1. Define a function to accept an arbitrary number of arguments, which get packed into a tuple.
  2. Call the function, passing it an arbitrary number of arguments, by unpacking an iterable.
def number_of_args(*args):
    return len(args)
>>> number_of_args(None, None, None, None)
4

>>> some_list = [1, 2, 3, 4, 5]

# passing the list itself as the sole argument
>>> number_of_args(some_list)
1

# unpacking the 5 members of the list,
# passing each one as an argument to the function
>>> number_of_args(*some_list)
5

Accommodating an Arbitrary Number of Keyword Arguments

We can also define a function that is able to accept an arbitrary number of keyword arguments, using the syntax: def f(**<var_name>)

Note that a single asterisk, *, was used to denote an arbitrary number of positional arguments, whereas ** signals the acceptance of an arbitrary number of keyword arguments.

# The ** symbol indicates that an arbitrary number of
# keyword arguments can be passed to `args`, when calling `f`.
def f(**args):
    #  All keyword arguments passed to `f` will be "packed" into a
    #  dictionary that is assigned to the variable `args`.
    # `f()`  will assign `args = {}` (an empty dictionary)
    # `f(x=1, y=2, ...)` will assign `args = {"x":1, "y":2, ...}`
    return args

Because Python cannot foresee how many arguments will be passed to f, all of the keyword arguments that are passed to it will be packed into a dictionary, where a given keyword is set as a key (cast as a string) that maps to the corresponding value. This dictionary is then assigned to the variable args. Dictionaries will be discussed in detail in a later section.

>>> f()            # pass zero arguments to `f`
{}

>>> f(x=1)           # pass one argument to `f`
{'x': 1}

>>> f(x=(0, 1), val=True, moo="cow")  # pass three arguments to `f`
{'moo': 'cow', 'val': True, 'x': (0, 1)}

This syntax can be combined with positional arguments and default arguments. No additional arguments may come after a ** entry in a function-definition signature:

def f(x, y=2, **kwargs):
    print("x is: ", x)
    print("y is: ", y)
    print("kwargs is: ", kwargs)
    return None
# passing arbitrary keyword arguments to `f`
>>> f(1, y=9, z=3, k="hi")
x is:  1
y is:  9
kwargs is:  {'z': 3, 'k': 'hi'}
# no additional keyword arguments are passed
>>> f("cat", y="dog")
x is:  cat
y is:  dog
kwargs is:  {}

The following function accepts an arbitrary number of positional arguments and an arbitrary number of keyword arguments:

# accepting arbitrary positional and keyword arguments
def f(*x, **y):
    # all positional arguments get packed into the tuple `x`
    # all keyword arguments get packed into the dictionary `y`
    print(x)
    print(y)
    return None

>>> f(1, 2, 3, hi=-1, bye=-2, sigh=-3)
(1, 2, 3)
{'hi': -1, 'bye': -2, 'sigh': -3}

We see that ** indicates the packing of an arbitrary number of keyword arguments into a dictionary, when used in the signature of a function definition. Simultaneously, ** signals the unpacking of a dictionary to pass each of its key-value pairs as a keyword argument to a function, when used in the context of calling a function:

# Using `**` when calling a function, to unpack a
# dictionary, passing its members as keyword arguments
# to the function
def f(x, y, z):
    return 0*x + 1*y + 2*z

>>> f(z=10, x=9, y=1)
21

>>> args = {"x": 9, "y": 1, "z": 10}
>>> f(**args)  # equivalent to: f(x=9, y=1, z=10)
21

In the following example, we use ** to:

  1. Define a function to accept an arbitrary number of keyword arguments, which get packed into a dictionary.
  2. Call the function, passing it an arbitrary number of keyword arguments, by unpacking a dictionary.
def print_kwargs(**args):
    print(args)
>>> print_kwargs(a=1, b=2, c=3, d=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

>>> some_dict = {"hi":1, "bye":2}

# unpacking the key-value pairs of the dictionary
# as keyword arguments and values, to the function
>>> print_kwargs(a=2, umbrella=True, **some_dict)
{'a': 2, 'umbrella': True, 'hi': 1, 'bye': 2}

Functions are Objects

Once defined, a function behaves like any other Python object, like a list or string or integer. You can assign a variable to a function-object:

>>> var = count_vowels  # `var` now references the function `count_vowels`
>>> var("Hello")        # you can now "call" `var`
2

You can store functions in a list:

my_list = [count_vowels, print]

for func in my_list:
    func("hello")

# iteration 0: calls `count_vowels("hello")`
# iteration 1: calls `print("hello")`

You can also call functions anywhere, and their return-value will be returned in-place:

if count_vowels("pillow") > 1:
    print("that's a lot of vowels!")

And, of course, this works within comprehension expressions as well:

>>> sum(count_vowels(word, include_y=True) for word in ["hi", "bye", "guy", "sigh"])
6

“Printing” a function isn’t very revealing. It simply tells you the memory address where the function-object is stored:

>>> print(count_vowels)
<function count_vowels at 0x000002A32898C6A8>

Reading Comprehension Exercise Solutions:

Writing a Basic Function: Solution

def count_even(numbers):
    """ Counts the number of even integers in an iterable"""
    total_even = 0
    for num in numbers:
        if num % 2 == 0:
            total += 1
    return total

or, using a generator comprehension:

def count_even(numbers):
    """ Counts the number of even integers in an iterable"""
    return sum(1 for num in numbers if num % 2 == 0)

Functions and Arguments: Solution

def max_or_min(x, y, mode="max"):
    """ Return either `max(x,y)` or `min(x,y)`,
        according to the `mode` argument.

        Parameters
        ----------
        x : Number

        y : Number

        mode : str
           Either 'max' or 'min'

        Returns
        -------
        The max or min of the two values. `None` is
        returned if an invalid mode was specified."""
    if mode == "max":
        return max(x, y)
    elif mode == "min":
        return min(x, y)
    else:
        return None

Note that you can actually have your function raise an “exception” (an error) in the case that mode wasn’t passed a proper value. In fact, that is likely the more appropriate behavior for this function.

Such a solution would look like:

def max_or_min(x, y, mode="max"):
    if mode == "max":
        return max(x, y)
    elif mode == "min":
        return min(x, y)
    else:
        raise Exception("`mode` was passed an invalid value: {}".format(mode))

Arbitrary Arguments: Solution

def mean(*seq):
    """ Returns the mean of the function's arguments """
    if len(seq) == 0:
        return 0

    total = 0
    for num in seq:
        total += num
    return total / len(seq)

or, being a bit more fancy :

  • using the fact that bool(seq) is False if seq is empty
  • using the inline if-else syntax
def mean(*seq):
    """ Returns the mean of the function's arguments """
    return sum(seq) / len(seq) if seq else 0