Functions and Methods

As a programmer, you often need a chunk of code in two or more program locations. No matter how often you want to use that code, you don't want to write that code repeatedly. What can you do?

Sometimes, you may realize that you have some code that would be perfect for use in another program. Can you somehow reuse that code without copying and pasting it?

You may sometimes find yourself with some complicated code that has become hard to read, understand, and maintain. What can you do to improve matters?

Most languages support the concept of procedures, blocks of code that run as separate units. In Python, we call these functions or methods.

Functions help solve all of these problems. They help reduce repetitive code, enable easy code reuse, and improve code readability and maintainability. Methods provide the same benefits as functions, but their behaviors are limited to specific objects.

Calling Functions

Using a function means calling, invoking, executing, or running it. All of those terms mean the same thing. When Python encounters a function call, it transfers program flow to the code that comprises the function and executes that code. When the code finishes its work, the function returns a value to the code that invoked it. The calling code is free to use or ignore the return value as the programmer sees fit. Execution resumes from where the function was called.

To invoke a function, write the function's name followed by a pair of parentheses (). For example, when we call the hello function in the following code, we write hello():

def hello():
    print('Hello')
    return True

hello()         # invoking function; ignore return value
print(hello())  # using return value in a `print` call

The first 3 lines of this program contain the function definition or function declaration. We'll come back to function definitions a little later in this chapter. For now, notice that the function returns the value True when it is done running.

Lines 5 and 6 show two calls to the hello function. In the first invocation, we ignore the return value. In the second, we capture the return value and pass it to the print function for printing.

What does the print function return? That's easy to check:

>>> print(print())
None

We'll see why that is later.

Functions can also take one or more comma-separated arguments between the parentheses. Functions usually use arguments to modify the actions they take. A single argument is passed to the print function on lines 2 and 6 above. In this case, it tells print what to print.

To provide additional arguments, separate them with commas:

print('hello', 'good-bye', 'farewell')

If you have a lot of arguments (or some longish ones), you can spread them over several lines:

print(
    'hello',
    'good-bye',
    'farewell',
    'adios',
    'ciao',
    'auf wiedersehen',
)

Built-in Functions

Built-in functions are functions that are always available in Python. Many of these functions are used in almost every significant Python program. For instance, you should already be comfortable with the print function.

As of Python 3.11.4, there are approximately 70 built-in functions. We've already met a number of these:

  • float, int
  • str, list, tuple
  • set, frozenset
  • input, print
  • type
  • len

You can learn more in the Python documentation for Built-in Functions. Let's look at a few of these functions (we'll meet even more in the Core Curriculum).

min and max

You can use the min and max functions to determine a collection's minimum and maximum values. The collection's objects must have an ordering that recognizes the < and > operators for comparing any pair of those objects.

print(min(-10, 5, 12, 0, -20))      # -20
print(max(-10, 5, 12, 0, -20))      # 12

print(min('over', 'the', 'moon'))   # 'moon'
print(max('over', 'the', 'moon'))   # 'the'

print(max(-10, '5', '12', '0', -20))
# TypeError: '>' not supported between instances
# of 'str' and 'int'

You can also use max and min with a single iterable argument, such as a list, set, or tuple:

my_tuple = ('i', 'tawt', 'i', 'taw', 'a',
            'puddy', 'tat')
print(min(my_tuple)) # 'a'
print(max(my_tuple)) # 'tawt'

We'll discuss iterables in a later chapter. For now, you only need to know that strings, sequences, mappings, and sets are iterable.

ord and chr

Given a single character, ord returns an integer that represents the Unicode code point of that character. For the standard ASCII character sets, the code points refer to the values of the characters in the standard ASCII character set. For example:

print(ord('a'))               # 97
print(ord('A'))               # 65
print(ord('='))               # 61
print(ord('~'))               # 126

chr is the inverse of ord. That is, it converts an integer to the corresponding Unicode character:

print(chr(97))                # a
print(chr(65))                # A
print(chr(61))                # =
print(chr(126))               # ~

Truthy and Falsy

In the remainder of this assignment, we will use the terms truthy and falsy to describe values. These are not the same thing as True and False, but are a little more general. The following values are said to be falsy:

  • False, None
  • all numeric 0 values (integers, floats, complex)
  • empty strings: ''
  • empty collections: [], (), {}, set(), frozenset(), and range(0)
  • Custom data types can also define additional falsy value(s).

All other values are said to be truthy.

We will revisit this concept in more detail in the next chapter. For now, you only need to be aware of the concept. You can think of truthy and falsy as True and False for now.

any and all

Two very useful built-in functions are the any and all functions. They both operate on iterable collections, such as lists, tuples, ranges, dictionaries, and sets.

The any function returns True if any element in a collection is truthy, False if all of the elements are falsy. On the other hand, all returns True if all of the elements are truthy, False otherwise.

collection1 = [False, False, False]
collection2 = (False, True, False)
collection3 = {True, True, True}

print(any(collection1))       # False
print(any(collection2))       # True
print(any(collection3))       # True
print(any([]))                # False

print(all(collection1))       # False
print(all(collection2))       # False
print(all(collection3))       # True
print(all([]))                # True

Notice that any returns False for an empty collection since none of the elements are truthy. However, all returns True for the same collection since none of the elements are falsy (all of the elements are thus truthy).

This example may not seem very useful. However, if you have a collection of something other than Booleans, you can use a comprehension with any or all to test whether any or all of the elements meet a certain condition. (We'll meet comprehensions a little later.)

For instance, assume you want to determine whether a list of numbers has any even values in it. You can use a list comprehension to determine which values are even or odd:

numbers = [2, 5, 8, 10, 13]
print([number % 2 == 0 for number in numbers])
# [True False True True False]

You can take that one step further to determine whether any or all of the numbers are even:

numbers = [2, 5, 8, 10, 13]
print(any([number % 2 == 0 for number in numbers])) # True
print(all([number % 2 == 0 for number in numbers])) # False

numbers = [5, 13]
print(any([number % 2 == 0 for number in numbers])) # False
print(all([number % 2 == 0 for number in numbers])) # False

numbers = [2, 8, 10]
print(any([number % 2 == 0 for number in numbers])) # True
print(all([number % 2 == 0 for number in numbers])) # True

Functions for the REPL

Some functions are intended for use in a REPL. These functions provide quick access to information about your program or Python itself.

The id Function

The id function returns an integer that serves as an object's identity. Every object has a unique identity that does not change during the object's lifetime. (The identity may be reused later in the program if the original object is discarded.)

In most cases, two instances of an object with the same value will always have two distinct identities. This is not always true, though. For instance, in a process called interning, every unique integer object from -5 through 256 has the same identity. Interning also applies to some strings:

# Paste this code into the Python REPL
# Don't run it as a program

s = 'Hello, world!'
t = 'Hello, world!'
print(id(s) == id(t))         # False

s = 'supercalifragilisticexpialidocious'
t = 'supercalifragilisticexpialidocious'
print(id(s) == id(t))         # True

x = 5
y = 5
print(id(x) == id(y))         # True

x = 5
y = 6
print(id(x) == id(y))         # False

Interning yields memory space savings and performance improvements. We discuss it mainly because new Python programmers sometimes discover this behavior and think they've found a bug. In practice, it's not an important concept, but it's worth being aware of.

By the way: the reason we asked you to paste that code into the Python REPL is that Python interns different things when you run a program file. In the above code, the first print will output True if you run it as a program.

Later on, we'll see why id is useful.

Interning is an implementation detail that varies based on the flavor and version of Python you are using. While most flavors and versions intern the small integers shown above, some intern other values such as certain strings.

Python "flavors" are different implementations of Python. The flavor you are most likely using is CPython, but other flavors include Jython, PyPy, AnacondaPython, and more.

The dir Function

When used without arguments, the dir function returns a list of all identifiers in the current local scope. When used with an argument, dir() returns a list of the object's attributes (typically, the object's methods and instance variables).

>>> dir()
['__builtins__', '__name__', 'struct']

>>> dir(5)
['__abs__', '__add__', '__and__', '__bool__',
    ... a bunch of stuff omitted ...,
 '__xor__', 'as_integer_ratio', 'bit_count',
 'bit_length', 'conjugate', 'denominator',
 'from_bytes', 'imag', 'numerator', 'real',
 'to_bytes']

Many of the names shown by dir begin and end with two underscores. These are names for the so-called dunder (double-underscore) or magic methods and magic variables. We'll encounter some of these in the Object Oriented Programming book.

Helpful Hint: Use the sorted function with the output of dir:

>>> sorted(dir(range(1)))
['__bool__', '__class__', '__contains__',
 '__delattr__', '__dir__', '__doc__', '__eq__',
    ... a bunch of stuff omitted ...,
 'count', 'index', 'start', 'step', 'stop']

Another Helpful Hint: Use pretty print to print the output in an easier to read format:

>>> from pprint import pp
>>> names = sorted(dir(range(1)))
>>> pp(names)
['__bool__',
 '__class__',
 '__contains__',
 '__delattr__',
 ... a bunch of stuff omitted ...,
 'count',
 'index',
 'start',
 'step',
 'stop']

Yet Another Helpful Hint: Use a comprehension to limit the output to just the names that don't contain __.

>>> names = sorted(dir(range(1)))
>>> names = [name for name in names
...          if '__' not in name]
>>> print(names)
['count', 'index', 'start', 'step', 'stop']

The help Function

When used without arguments, the help function prints some basic help on how to use help, then leaves you in a special help mode that uses a help> prompt.

>>> help()

Welcome to Python 3.11's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the internet at https://docs.python.org/3.11/tutorial/.

Enter the name ...
   <omitted text>

help>

From the help> prompt, you can request help on module names, Python keywords, built-in functions, class names, etc. Type the appropriate words at the help> prompt and press {Return} or {Enter}.

Quitting the help system may involve two separate steps. If you are currently reading a long help item (such as the help for str), you may need to press q to terminate the output. After terminating the output, you may need to type another q and then press {Return} or {Enter} to exit from the help> prompt.

As of Python 3.13.0, you no longer need to use write help() to access the help system. You can, instead, just write help. When supplying an argument to help, you still need to use the parentheses.

You can also request help more directly by placing an appropriate identifier between the parentheses:

>>> help(ord)
Help on built-in function ord in module builtins:

ord(c, /)
    Return the Unicode code point for a one-character string.

>>> help(bool)
Help on class bool in module builtins:

class bool(int)
 |  bool(x) -> bool
 |
 |  Returns True when the argument x is true, False otherwise.
 |  The builtins True and False are the only two instances of the class bool.
 |  The class bool is a subclass of the class int, and cannot be subclassed.
 |
 |  Method resolution order:
 |      bool
 |      int
 |      object

   <omitted text>

>>> help('topics')

Here is a list of available topics. Enter any topic name to get more help.

ASSERTION           DELETION            LOOPING             SHIFTING
ASSIGNMENT          DICTIONARIES        MAPPINGMETHODS      SLICINGS
ATTRIBUTEMETHODS    DICTIONARYLITERALS  MAPPINGS            SPECIALATTRIBUTES
ATTRIBUTES          DYNAMICFEATURES     METHODS             SPECIALIDENTIFIERS
AUGMENTEDASSIGNMENT ELLIPSIS            MODULES             SPECIALMETHODS
   <omitted text>

>>> help('BOOLEAN')
Boolean operations
******************

   or_test  ::= and_test | or_test "or" and_test
   and_test ::= not_test | and_test "and" not_test
   not_test ::= comparison | "not" not_test

In the context of Boolean operations, and also when expressions are
used by control flow statements...

   <omitted text>

Creating Functions

Unless a function is built-in or provided by an imported module, you must create it. A typical function definition (a.k.a., function declaration) looks like this:

def func_name():
    func_body

Here, we're assigning the function to func_name, and func_body is some code that performs the function's required action(s).

Here's a function named say that prints Hi!:

def say():
    print('Hi!')

Why do we want a function named say? To say something, of course! Let's try it. First, we'll create a file named say.py, then place the following code inside the file.

def say():
    print('Output from say')

print('First')
say()
print('Last')

Save the file and run it from the terminal:

$ python say.py
First
Output from say
Last

On line 5 of say.py, the code say() is a function call to the say function. When Python runs this program, it creates a function named say whose body causes Python to print the text Output from say when the function executes. However, that doesn't happen immediately.

On line 4, we use print to display First on the terminal. On line 5, we call the function say by appending a pair of parentheses -- () -- to the function's name. This causes Python to temporarily jump into the function's body, which prints the text Output from say. Finally, Python returns to the code that immediately follows the call to say and prints Last.

Note that the parentheses on line 5 -- () -- make this code a function call. Without the parentheses, say doesn't do anything useful. It's the function's name, not an invocation. Forgetting the parentheses is usually a bug that can be tough to find since the code may run long after the line with the error. It just won't work. We'll learn about function objects later in the Core Curriculum.

Python programmers often add a triple-quoted string at the beginning of a function's block. This string is a documentation comment -- a docstring -- that Python can access with its help() function and the __doc__ property. It has no effect on your code unless your program is somehow interested in the comments (which can happen):

def say():
    """
    The say function prints "Hi!"
    """
    print('Hi!')

print('-' * 60)
print(say.__doc__)
print('-' * 60)
help(say)
------------------------------------------------------------

    The say function prints "Hi!"

------------------------------------------------------------
Help on function say in module \_\_main\_\_:

say()
    The say function prints "Hi!"

We won't follow this convention at Launch School, but you should learn to recognize it. You will likely encounter it elsewhere.

Scope

The scope of an identifier determines where you can use it. Python determines scope by looking at where you initialize the identifier. In Python, identifiers have function scope. That means that anything initialized inside a function is only available within the body of that function and any nested functions. No code outside of the function body can access that identifier.

Consider this code:

def well_howdy(who):
    greeting = 'Howdy'
    print(f'{greeting}, {who}')

well_howdy('Angie')
print(greeting)

In this code, we have a greeting variable in the body of well_howdy. When we call the function, Howdy is assigned to greeting, and a more complete greeting message is printed.

When the function is done running, we try to print the value of the greeting variable. However, instead of seeing Howdy, we get an error message instead:

NameError: name 'greeting' is not defined

The error occurs since the greeting variable is only available inside the well_howdy function. The surrounding code has no way to access the variable.

What happens if we define our own greeting variable in the outer scope?

greeting = 'Salutations'

def well_howdy(who):
    greeting = 'Howdy'
    print(f'{greeting}, {who}')

well_howdy('Angie')
print(greeting)

This time, the code prints Salutations on line 8, thus showing that greeting is in scope on line 8. However, the outer greeting variable plays no part in the function's body. The assignment on line 4 hides the greeting variable from line 1. We call this variable shadowing.

Let's remove the assignment on line 4 and see what happens:

greeting = 'Salutations'

def well_howdy(who):
    print(f'{greeting}, {who}')

well_howdy('Angie')
print(greeting)

This time, we get:

Salutations, Angie
Salutations

The key difference here is that we are no longer assigning the greeting variable in this function. Instead, we're just accessing its current value. It's the assignment in the first example in this section that causes Python to create a new local variable named greeting. Without any assignments in the function body, Python looks for greeting in the lexical scope, which means it searches the outer scopes for the nearest definition of greeting. In this example, the only outer scope of concern is the topmost scope, i.e., the global scope.

If you're familiar with some other programming languages, the following example may be a little surprising:

def scope_test():
    if True:
        foo = 'Hello'
    else:
        bar = 'Goodbye'

    print(foo)
    print(bar)

scope_test()
Hello
UnboundLocalError: cannot access local variable
'bar' where it is not associated with a value

In some languages, the corresponding print code may treat both foo and bar as out of scope. Both names may be in scope in other languages, but one will have a default value such as nil or undefined. In still other languages, only one name is in scope. Python comes closest to this last approach, but both names are technically in scope. However, since the else block never runs, bar is left unassigned. Thus, we get the UnboundLocalError message when we try to print it.

As you might expect, constants have the same scoping behavior as variables. In fact, so do function, parameter, class, and module names. Method names act a little differently, but that's a topic for another time.

Namespaces

Closely related to the concept of scope is the concept of a namespace. In Python, a namespace is defined as a mapping of identifiers to objects. In essence, namespaces define the scopes in which Python will look for identifiers. When you refer to an identifier, Python first looks for the identifier in the local namespace. That is, it looks in the local scope. If the identifier is not found in the local namespace, Python next looks in any enclosing namespaces, i.e., the outer scopes (but not including the global scope). Next, it searches the global namespace, which is equivalent to the global scope, and, finally, the built-in namespace.

We will not often refer to namespaces at Launch School, but you may encounter the term when looking at other material.

Arguments & Parameters

Create a new file named say2.py with the following code:

print('hello')
print('hi')
print('how do you do')
print('Quite all right')

Go ahead and run it if you want. Notice how we've duplicated the print call several times. Instead of calling it every time we want to output something, we want to have code we can call when we need to print something.

Now, let's update say2.py as follows:

def say(text):
    print(text)

say('hello')
say('hi')
say('how do you do')
say('Quite all right')

At first glance, this program seems silly. It doesn't reduce the amount of code. In fact, it adds more. Also, the say function doesn't provide any functionality that print doesn't already offer. However, there is a benefit here. We've extracted the logic for displaying text in a way that makes our program more flexible. If we need to change the output style, we can change it in one place: the say function. We don't have to change any other code to get the updated behavior. We'll see such an update in a few minutes.

As we saw earlier, we invoke functions by typing their name and a pair of parentheses. say2.py illustrates how we define and call a function that takes a value known as an argument. Arguments let you pass data from outside the function's scope into the function so it can access that data. You don't need arguments when the function doesn't need access to outside data.

In say2.py, the function definition includes (text) after the function name. This syntax tells us that we should supply (pass) a single argument to the function when we call it. We include an argument between the invocation parentheses to pass it to say. Thus, say('hello') passes the argument 'hello' to the function. We assign the argument's value to the text parameter inside say.

The names between parentheses in the function definition are called parameters. You can think of parameters as placeholders for potential arguments, while arguments are the values assigned to those placeholders.

Function names and parameters are both considered variable names in Python. Parameters, in particular, are local variables. They are defined locally within the function's body. A function's name is global or local, depending on whether it is at the program's top level or nested inside a class, module, or another function. We'll see several exercises about these issues below; they are essential even if there isn't much to say about them.

Since parameters are merely placeholders, we typically talk of them as declarations: they are being used to introduce those names as local variables into a function, but don't obviously provide an immediate value until the function is called. That is, they simply declare variable names.

Arguments are objects passed to a function during invocation. Parameters are placeholders for the objects that will be passed to the function when it is invoked.

Many developers conflate the terms "parameter" and "argument" and use them interchangeably. The difference is typically not crucial to understanding, but using correct terminology is a good idea. We expect you to do so on Launch School assessments. Parameters are not arguments. Arguments are not parameters.

You can name functions and parameters with any valid variable name. However, you should use meaningful, explicit names that follow the same naming conventions discussed in the Variables chapter. In the above example, we name the parameter text since the say function expects the caller to pass in some text. Other suitable names might be sentence or message.

When we call the say function, we should provide a value -- the argument -- to assign to the text parameter. For instance, on line 4 of say2.py, we pass the value 'hello' to the function, which it uses to initialize text. The function can use the value in any way it requires by referencing the parameter name. It can even ignore the argument entirely.

Remember: a parameter's scope is the function where the parameter is used. Thus, the scope of the text parameter in say2.py is the say function. You can't reference the parameter outside of the function's body.

You can define functions that take any number of parameters and accept any number of arguments. For instance, the add function defined below takes two parameters, and we invoke it with 2 arguments:

def add(left, right):
    sum = left + right
    return sum

sum = add(3, 6)

In the above code, left and right are parameters on line 1. On line 2, however, they are local variables that contain the argument values passed to add on line 5.

Python will raise an error if you pass too many or too few arguments to a function. However, it is possible to bypass this restriction, as we'll see later.

Let's return to say2.py. When we called say('hello'), we passed the string 'hello' as the argument. Thus, 'hello' was assigned to the text parameter.

As mentioned earlier, one benefit that functions give us is the ability to make changes in one location. Suppose we want to add a ==> to the beginning of every line that say outputs. All we have to do is change one line of code -- we don't have to change the function invocations:

def say(text):
    # highlight
    print('==> ' + text)
    # endhighlight

say('hello')        # ==> hello
say('hi')           # ==> hi
say('how are you')  # ==> how are you
say("I'm fine")     # ==> I'm fine

Run this code to see the result. We've now added a ==> to the beginning of each line. We only had to change a single line of code. Now, you're starting to see the power of functions.

Return Values

As we've seen in some earlier examples, functions can perform an operation and return a result to the caller for later use. We do that with return values and the return statement.

All Python function calls evaluate to a value, provided the function doesn't raise an exception (an error). By default, that value is None; this is the implicit return value of most Python functions. However, when you use a return statement, you can return a specific value from a function. This is an explicit return value. There is no distinction between implicit and explicit return values outside the function. Still, it's important to remember that all functions return something unless they raise an exception, even if they don't explicitly execute a return statement.

Let's create an add function that returns the sum of two numbers:

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

add(2, 3)           # returns 5

When you run this program, it doesn't print anything since add doesn't call print or any other output functions. However, the function does return a value: 5. When Python encounters the return statement, it evaluates the expression, terminates the function, then returns the expression's value to the location where we called add.

Let's capture the return value in a variable and print it to verify that:

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

# highlight
two_and_three = add(2, 3)
print(two_and_three)  # 5
# endhighlight

Python uses the return statement to return a value to the code that invoked the function. If you don't specify a value, it returns None. Either way, the return statement terminates the function and returns control to the calling function.

Functions that always return a Boolean value, i.e., True or False, are called predicates. For example, the following function is a predicate:

def is_digit(char):
    if char >= '0' and char <= '9':
        return True

    return False

You will almost certainly encounter this term in future readings and videos, so commit it to memory.

Default Parameters

It's sometimes helpful to invoke a function without some of its arguments. You can do that by providing default values for the function's parameters. Let's update say to use a default value when the caller omits the argument.

def say(text='hello'):
    print(text + '!')

say('Howdy') # Howdy!
say()        # hello!

Notice that invoking say without arguments, as on line 5, prints 'hello!'. We can invoke say without arguments since we've provided a default value for text. Nice!

You can provide defaults for any or all of a function's parameters. However, once you specify a default value for a parameter, all subsequent positional parameters must also have default values:

def say(msg1, msg2, msg3='dummy message', msg4):
    pass
# SyntaxError: non-default argument follows default argument

It's also worth noting that you can't accept the default value for a parameter and provide an explicit value for a subsequent parameter:

def say(msg1, msg2, msg3='dummy message',
                    msg4='omitted message'):
    print(msg1)
    print(msg2)
    print(msg3)
    print(msg4)

say('a', 'b', 'c', 'd')
# a
# b
# c
# d

say('a', 'b', 'c')
# a
# b
# c
# omitted message

say('a', 'b')
# a
# b
# dummy message
# omitted message

say('a', 'b', , 'd')
# a
# b
# d               # oops - expecting 'dummy message'
# omitted message # oops again - expected 'd'

Python has a variety of ways to specify parameters. The easiest is with positional parameters. With positional parameters, the parameter values are taken from the corresponding argument position. Thus, if you have a function that takes 3 parameters, the first parameter is set to the first argument, the second parameter to the second argument, and the third parameter to the third argument. For instance, in the say function above, the 4 parameters are assigned based on the order of the arguments. Of course, default parameters mean you can omit some arguments.

We'll see some of Python's other parameter types later in the Core Curriculum.

Functions vs. Methods

Thus far, all our function calls have used the function_name(obj, ...) syntax. We call a function by writing parentheses after its name. Any arguments the function needs are provided inside the parentheses.

However, suppose you want to convert a string to all uppercase letters. You might expect to use a function invocation like upper(my_str). However, that won't work. You must use a different syntax called method invocation.

Method invocations occur when you prepend an object followed by a period (.) to a function invocation, e.g., 'xyzzy'.upper(). We call such function invocations method calls. We also speak of the function as a method. We cover this topic in more detail in the Core Curriculum when we get to Object-Oriented Programming. You can think of the previous code as the function upper returning a modified version of the string 'xyzzy'.

Methods provide the same benefits as functions. However, methods work with specific objects. All methods are functions but not vice versa. Every method belongs to a class and requires an object of that class to call it. For instance, in the following example, we're using the string object represented by my_str to call the upper method from the str class:

my_str = 'abc-123-def'
print(my_str.upper())         # ABC-123-DEF

Writing your own methods requires classes, which is how you create custom data types in Python. We're not ready to write our own classes and methods at this point; you'll learn how to do that in our Object Oriented Programming in Python book.

In Python, the distinction between functions and methods may sometimes seem fuzzy. Some function invocations look like method invocations. For instance, consider the math module from Python's standard libraries. The module provides some mathematical functions that any program can use. Once you import the module, you just call the functions you need:

import math

print(math.sqrt(5))           # 2.23606797749979

Wait. Is that a method call? It sure looks like one. It quacks like one. Since math references an object, math.sqrt() must be a method call. However, it is not. While math is technically a reference to a module object, we're not using it to perform operations on that object. The sole purpose of the math object here is to tell Python where to look for those functions.

For brevity, we often speak of functions when discussing functions and methods together. However, you should say methods when discussing functions explicitly designed to require calling objects.

Mutating the Caller

Sometimes, a method mutates the object used to invoke the method: it mutates the caller. In the following example, the pop method mutates the caller (the list referenced by odd_numbers):

odd_numbers = [1, 3, 5, 7, 9]
odd_numbers.pop()
print(odd_numbers)  # [1, 3, 5, 7]

We can also talk about whether functions mutate their arguments. The following function illustrates this concept:

def add_new_number(my_list):
    my_list.append(9)

numbers = [1, 2, 3, 4, 5]
add_new_number(numbers)
print(numbers) # [1, 2, 3, 4, 5, 9]

This code uses the list.append method to add a new number to the list my_list.Thus, list.append mutates its caller. However, the add_new_number function doesn't mutate a caller (there is no caller). Instead, it mutates its argument.

In general, mutating the caller is acceptable practice; many built-in functions and methods do just that. However, you should avoid mutating arguments since such functions can be tough to debug and is considered poor practice. Almost no built-in functions mutate their arguments.

# Returning a new object
def add_new_number(my_list):
    return my_list + [9]

numbers = [1, 2, 3, 4, 5]
new_numbers = add_new_number(numbers)
print(new_numbers) # [1, 2, 3, 4, 5, 9]
print(numbers)     # [1, 2, 3, 4, 5]

How do you know which methods mutate the caller and which don't? That's easy when the caller is immutable: the answer is always, "This method does not mutate the caller." However, when the caller is mutable, there's no way to say without inspecting the method's code or checking the documentation.

We discuss object mutation in much more detail in our courses at Launch School.

Summary

Functions and methods are fundamental concepts in Python programming. Knowing what a function does is crucial to your development as a Python programmer. You'll use them all the time in programs both big and small.

Exercises

  1. What happens when you run the following program? Why do we get that result?

    def set_foo():
        foo = 'bar'
    
    set_foo()
    print(foo)
    

    Solution

    The program outputs an error: NameError: name 'foo' is not defined

    Since foo is created inside a function, it isn't in scope when executing code that isn't part of that function.

    Video Walkthrough

    Please register to play this video

  2. Take a look at this code snippet:

    foo = 'bar'
    
    def set_foo():
        foo = 'qux'
    
    set_foo()
    print(foo)
    

    What does this program print? Why?

    Solution

    The program prints bar since the assignment on line 4 creates a new variable that is local to the function. That is, the foo variable on line 4 shadows the foo variable on line 1, so line 4 doesn't change the value of foo from line 1.

    Video Walkthrough

    Please register to play this video

  3. Write a program that uses a multiply function to multiply two numbers and returns the result. Ask the user to enter the two numbers, then output the numbers and result as a simple equation.

    $ python multiply.py
    Enter the first number: 3.1416
    Enter the second number: 2.7183
    3.1416 * 2.7183 = 8.53981128
    

    Solution

    def multiply(left, right):
        return left * right
    
    def get_number(prompt):
        entry = input(prompt)
        return float(entry)
    
    first_number = get_number('Enter the first number: ')
    second_number = get_number('Enter the second number: ')
    product = multiply(first_number, second_number)
    print(f'{first_number} * {second_number} = {product}')
    

    Video Walkthrough

    Please register to play this video

  4. Consider this code:

    def multiply_numbers(num1, num2, num3):
        result = num1 * num2 * num3
        return result
    
    product = multiply_numbers(2, 3, 4)
    

    Identify the following items in that code:

    Item
    function name
    function arguments
    function definition
    function body
    function parameters
    function invocation
    function return value
    all identifiers

    Solution

    Item Answer
    function name multiply_numbers on lines 1 and 5
    function arguments 2, 3, and 4 between the parentheses on line 5
    function definition everything on lines 1-3
    function body everything on lines 2 and 3
    function parameters num1, num2, and num3 on line 1
    function invocation multiply_numbers(2, 3, 4) on line 5
    function return value 24
    all identifiers multiply_numbers, num1, num2, num3, result, and product

    Video Walkthrough

    Please register to play this video

    Errata: At about 1:56 the instructor says that the function name is on lines 3 and 5. In fact, the correct line numbers in the video are lines 3 and 7.

  5. What does the following code print?

    def scream(words):
        return words + '!!!!'
    
    scream('Yipeee')
    

    Solution

    This program doesn't output anything. The function returns a value, Yipeee!!!!, but it doesn't do anything with it. In particular, it doesn't print it.

    Video Walkthrough

    Please register to play this video

  6. What does the following code print?

    def scream(words):
        words = words + '!!!!'
        return
        print(words)
    
    scream('Yipeee')
    

    Solution

    This code also doesn't print anything. The return on line 3 terminates the function before it can print anything.

    Video Walkthrough

    Please register to play this video

  7. Without running the following code, what do you think it will do?

    def foo(bar, qux):
        print(bar)
        print(qux)
    
    foo('Hello')
    

    Solution

    The code will raise an error:

    TypeError: foo() missing 1 required positional
    argument: 'qux'
    

    We've defined foo with two parameters. However, we've only passed it one argument. This is an error.

    Video Walkthrough

    Please register to play this video

  8. Without running the following code, what do you think it will do?

    def foo(bar, qux):
        print(bar)
        print(qux)
    
    foo(42, 3.141592, 2.718)
    

    Solution

    The code will raise an error:

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

    We've defined foo with two parameters. However, we've passed the function three arguments. This is an error.

    Video Walkthrough

    Please register to play this video

  9. Without running the following code, what do you think it will do?

    def foo(first, second=3, third=2):
        print(first)
        print(second)
        print(third)
    
    foo(42, 3.141592, 2.718)
    

    Solution

    The code will print the following:

    42
    3.141592
    2.718
    

    Here, we've defined foo with three parameters, with the second and third parameters having default values. However, we've passed all three arguments to the function, so Python ignores the default values.

    Video Walkthrough

    Please register to play this video

  10. Without running the following code, what do you think it will do?

    def foo(first, second=3, third=2):
        print(first)
        print(second)
        print(third)
    
    foo(42, 3.141592)
    

    Solution

    The code will print the following:

    42
    3.141592
    2
    

    Here, we've defined foo with three parameters, with the second and third parameters having default values. We've passed the function two arguments, so Python uses the default value for the last argument.

    Video Walkthrough

    Please register to play this video

  11. Without running the following code, what do you think it will do?

    def foo(first, second=3, third=2):
        print(first)
        print(second)
        print(third)
    
    foo(42)
    

    Solution

    The code will print the following:

    42
    3
    2
    

    Here, we've defined foo with three parameters, with the second and third parameters having a default value. We've passed the function one argument, so Python uses the default value for the last two parameters.

    Video Walkthrough

    Please register to play this video

  12. Without running the following code, what do you think it will do?

    def foo(first, second=3, third=2):
        print(first)
        print(second)
        print(third)
    
    foo()
    

    Solution

    The code will raise an error:

    TypeError: foo() missing 1 required positional
    argument: 'first'
    

    Here, we've defined foo with three parameters, with the second and third parameters having default values. We haven't passed the function any arguments. That's an error, though - the first parameter has no default value.

    Video Walkthrough

    Please register to play this video

  13. Without running the following code, what do you think it will do?

    def foo(first, second=3, third):
        print(first)
        print(second)
        print(third)
    
    foo(42)
    

    Solution

    The code will raise an error:

    SyntaxError: non-default argument follows
    default argument
    

    Here, we've defined foo with three parameters, with the second parameter having a default value. This is an error, however. Once Python sees a positional parameter with a default value, all subsequent parameters must have default values.

    Video Walkthrough

    Please register to play this video

  14. Identify all of the identifiers on each line of the following code.

    def multiply(left, right):
        return left * right
    
    def get_num(prompt):
        return float(input(prompt))
    
    first_number = get_num('Enter the first number: ')
    second_number = get_num('Enter the second number: ')
    product = multiply(first_number, second_number)
    print(f'{first_number} * {second_number} = {product}')
    

    Solution

    The identifiers in this code are multiply, left, right, first_number, second_number, get_num, prompt, float, input, product, and print. The following table shows where each identifier appears in the code.

    Identifier Appears on these lines
    multiply 1, 9
    left 1, 2
    right 1, 2
    first_number 7, 9, 10
    second_number 8, 9, 10
    get_num 4, 7, 8
    prompt 4, 5
    float 5
    input 5
    product 9, 10
    print 10

    Video Walkthrough

    Please register to play this video

  15. Using the code below, classify the identifiers as global, local, or built-in. For our purposes, you may assume this code is the entire program.

    def multiply(left, right):
        return left * right
    
    def get_num(prompt):
        return float(input(prompt))
    
    first_number = get_num('Enter the first number: ')
    second_number = get_num('Enter the second number: ')
    product = multiply(first_number, second_number)
    print(f'{first_number} * {second_number} = {product}')
    

    Solution

    Category Identifiers
    Global multiply, get_num, first_number, second_number, product
    Local left, right, prompt
    Built-in float, input, print

    Functions defined in a program file are global identifiers unless those functions are defined as an object property or nested inside another function. Thus, multiply, get_num, and product are globals in this program. Function parameters and variables initialized inside a function are always local identifiers. Thus, left, right, and prompt are all local variables. first_number and second_number are initialized as global variables on lines 7 and 8 respectively, then used on lines 9 and 10.

    Video Walkthrough

    Please register to play this video

  16. In the code shown below, identify all of the function names and parameters present in the code. Include the line numbers for each item identified.

    def multiply(left, right):
        return left * right
    
    def get_num(prompt):
        return float(input(prompt))
    
    first_number = get_num('Enter the first number: ')
    second_number = get_num('Enter the second number: ')
    product = multiply(first_number, second_number)
    print(f'{first_number} * {second_number} = {product}')
    

    Solution

    • Function names:
      • multiply: defined on line 1, used on line 9.
      • get_num: defined on line 4, used on lines 7 and 8.
      • float: built-in function used on line 5.
      • input: built-in function used on line 5.
      • print: built-in function used on line 10.
    • Parameters:
      • left and right are defined on line 1 and then used on line 2.
      • prompt is defined on line 4 and then used on line 5.

    Note that left and right are defined as parameters for the multiply function on line 1. We reference those parameters on line 2, but usually speak of them as local variables instead of parameters. For this exercise, it's okay if you said that left and right are present on line 2. It's also okay if you omitted it.

    Likewise, prompt is defined as a parameter for the get_num function on line 4, but used as a local variable on line 5.

    Video Walkthrough

    Please register to play this video

  17. Which of the identifiers in the following program are function names? Which are method names? Which are built-in functions?

    def say(message):
        print(f'==> {message}')
    
    string1 = input()
    string2 = input()
    
    say(max(string1.upper(), string2.lower()))
    

    Solution

    Function Names Method Names Built-in Function Names
    say
    print
    input
    max
    upper
    lower

    If you identified all the method and built-in function names as function names, that's an acceptable answer as well: all methods are functions, and built-in functions are just functions that are, well, built-in.

    Video Walkthrough

    Please register to play this video

  18. The following function returns a list of the remainders of dividing the numbers in numbers by 3:

    def remainders_3(numbers):
        return [number % 3 for number in numbers]
    

    Use this function to determine which of the following lists contains at least one number that is not evenly divisible by 3 (that is, the remainder is not 0):

    numbers_1 = [0, 1, 2, 3, 4, 5, 6]
    numbers_2 = [1, 2, 4, 5]
    numbers_3 = [0, 3, 6]
    numbers_4 = []
    

    Note: when working with integers, a value of 0 is "falsy"; all other integers are "truthy".

    Solution

    print(any(remainders_3(numbers_1)))     # True
    print(any(remainders_3(numbers_2)))     # True
    print(any(remainders_3(numbers_3)))     # False
    print(any(remainders_3(numbers_4)))     # False
    

    remainders_3 returns a list of integers between 0 and 2, inclusive. A value of 0 means the corresponding number is divisible by 3, while a value of 1 or 2 means the number is not divisible by 3. Since 0 is falsy and 1 and 2 are truthy, we can use any to determine whether any of the numbers are non-zero.

    Video Walkthrough

    Please register to play this video

  19. The following function returns a list of the remainders of dividing the numbers in numbers by 5:

    def remainders_5(numbers):
        return [number % 5 for number in numbers]
    

    Use this function to determine which of the following lists do not contain any numbers that are divisible by 5:

    numbers_1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    numbers_2 = [1, 2, 3, 4, 6, 7, 8, 9]
    numbers_3 = [0, 5, 10]
    numbers_4 = []
    

    Note: when working with integers, a value of 0 is "falsy"; all other integers are "truthy".

    Solution

    print(all(remainders_5(numbers_1)))     # False
    print(all(remainders_5(numbers_2)))     # True
    print(all(remainders_5(numbers_3)))     # False
    print(all(remainders_5(numbers_4)))     # True
    

    remainders_5 is similar to remainders_3 in the previous exercise in that it returns a list of integers. In this list, a value of 0 means the corresponding number is divisible by 5, while a non-zero value means the number is not divisible by 5. Since 0 is falsy and the other integers are truthy, we can use all to determine whether all of the numbers are not divisible by 5.

    Video Walkthrough

    Please register to play this video