12  Functions

A function is a block of code that performs a specific task and may return a value. Functions are the building blocks of Python code. Functions are the building blocks of Python code.

User-defined functions are custom functions created by the user to perform specific calculations or tasks that are not available in Python’s built-in functions.

Modular programming is a software design technique that emphasizes breaking down programs into smaller, self-contained modules or procedures. Each module performs a specific task and can be reused in different parts of the program. This approach makes code more manageable, easier to understand, and less error-prone.

12.1 Anatomy of a Function

In Python, a function consists of the following parts:

  • Function Name: The name of the function.
  • Parameters: The values received by the function when it’s called. Some functions may not require parameters or have default values.
  • Function Signature: The function name and parameters (if type hints are not used).
  • Function Body: The code that performs the function’s task. It is indented (i.e., the code block is aligned to the right, typically using four spaces) to indicate that it is part of the function.
  • Return Statement (optional): The return statement is used to return a value from the function. If omitted, the function returns None.
  • End of Function: Functions end when the indented code block ends.

12.2 Defining and Using Functions

In Python, you can define a function using the def keyword followed by the function name and parameters. The function body is indented to indicate that it is part of the function. For example, in Listing 12.1, the function add_one_to adds one to a number and returns the result. This function:

  • Receives a number (parameter num)
  • Adds one to the number received and assigns the result to variable result
  • Returns the variable result using the return statement
Listing 12.1: Function to add one to a number. The code after the return statement will not be executed. The block of code inside the function is indented. The function is called with the argument 1, and the return value is assigned to the variable value. Then, the function is called two more times with the previous result as the argument.
def add_one_to(num):

    # Assign result of calculation to variable "result"
    result = num + 1

    # Return the result
    return result

    # Everything you put here will NOT be executed!

# This is not part of the function
value = add_one_to(1)  # Output: 2
value = add_one_to(value)  # Output: 3
value = add_one_to(value)  # Output: 4
Return Value

In Python, the return statement is used to return a value from a function. If the return statement is omitted, the function will return None by default.

To use a function, you need to:

  • Pass the required arguments: The number and order of arguments must match the function’s parameters, unless you are using default or keyword arguments.
  • Call/Invoke the Function: Use the function name followed by parentheses and arguments (if any). The function can be called from other functions or code.
  • Capture the return value if needed: The return value can be assigned to a variable or used directly.

In Listing 12.1, the function add_one_to is called with the argument 1, and the return value is assigned to the variable value.

Arguments vs. Parameters

The difference between arguments and parameters is that arguments are the actual values passed to a function, while parameters are the variables that receive the values. These terms are often used interchangeably, but it’s important to understand the distinction. The parameters are placeholders for the arguments that will be passed to the function.

12.3 Returning a Value (return Statement)

In Python, you can exit a function at any point using the return statement. This statement is used to immediately exit the function and optionally return a value.

For example:

Listing 12.2: Function to add two numbers and exit. The code after the first return statement will not be executed.
def func_and_exit(n1, n2):
    return n1 + n2
    # The code below will not be executed
    return 0

Notice that since the return statement returns a value, you can use the function to assign the result to a variable. In Listing 12.3, the function power_two calculates the square of a number. The result of the function is then used as an argument for the next call to the function, creating a chain of calculations:

Listing 12.3: Example of chaining function calls. The function power_two calculates the square of a number. The result of the function is then used as an argument for the next call to the function. The result of the last call is assigned to the variable result.
def power_two(n):
    return n ** 2

result = power_two(power_two(power_two(power_two(2))))

12.4 Returning None Value

If a function does not contain a return statement, it will return None by default. In Listing 12.4, the function no_return does not contain a return statement. When called, the function will return None. If the return keyword is used without a value, it will also return None.

Listing 12.4: Function that does not return a value. The function no_return does not contain a return statement. When called, the function will return None. The variable x is assigned the return value of the print function, which is None. Also, the function return_keyword contains a return statement without a value, which will return None.
def no_return():
    print("This function does not return anything.")

def return_keyword():
    print("This function does not return anything.")
    return

result = no_return()
print(result)  # Output: None

x = print("Hello, World!")
print(x)  # Output: None

result = return_keyword()
print(result)  # Output: None

The None value is a special constant in Python of type NoneType. It is often used to indicate the absence of a value or as a placeholder when a value is not needed. None is useful when you want to explicitly return nothing from a function or assign a variable to an empty value.

Returning None

If a function does not contain a return statement, it will return None by default. Be careful when assigning the return value of such functions to variables, as they may not behave as expected.

12.5 Empty Functions (pass Statement)

Sometimes you may need to define a function without any code inside it. This can be useful when you are planning to implement the function later or when you want to create a placeholder for future functionality.

In Python, you can define an empty function using the pass statement. The pass statement is a null operation that does nothing when executed.

For example, in Listing 12.5, the function empty_function does not contain any code. It is defined using the pass statement. If you call this function, it will not perform any action and will return None.

Listing 12.5: Empty function defined using the pass statement. The function does not contain any code and will not perform any action when called, returning None.
def empty_function():
    pass

12.6 Exiting a Function

In Python, you can exit a function at any point using the return statement. For example, in Listing 12.6, the function add_and_exit adds two numbers and exits the function immediately after printing the result. The code after the return statement will not be executed.

Listing 12.6: Function to add two numbers and exit. The code after the return statement will not be executed.
def add_and_exit(n1, n2):
    print(n1 + n2)
    return

    # The code below will not be executed
    print(0)
    return n1 + n2

12.7 Default Arguments

In Python, you can define default values for function parameters. If a default value is provided for a parameter, it becomes an optional argument, and the function can be called without providing a value for that parameter.

For example, in #lst-default-arguments, the function salute_recipient has a default value for the parameter salutation. If no value is provided for salutation, the default value "Hello" will be used.

Listing 12.7: Function to salute a recipient with a default salutation. If no salutation is provided, the default value “Hello” is used.
def salute_recipient(name, salutation="Hello"):
    return f"{salutation} {name},"

print(salute_recipient("Alice"))
# Output: "Hello Alice,"

print(salute_recipient("Bob", "Dear"))
# Output: "Dear Bob,"

12.8 Order of Arguments

In Python, the order of arguments passed to a function must match the order of parameters defined in the function signature. If the order is incorrect, the function may not work as expected.

For example, in #lst-order-of-arguments, the function calculate_speed expects the parameters distance and time in that order. If the order is reversed when calling the function, the result will be incorrect.

Listing 12.8: Function to calculate speed in kilometers per hour. The order of arguments must match the order of parameters. The functions expect to receive first the distance and then the time.
def calculate_speed_km_h(distance_km, time_h):
    return distance_km / time_h

# Correct order of arguments
distance_km = 100
time_h = 10
calculate_speed(distance_km, time_h)  # Output: 10

# Incorrect order of arguments
calculate_speed(time_h, distance_km)  # Output: 0.1

12.9 Keyword Arguments

In Python, you can use keyword arguments to specify the values of parameters by name when calling a function. This allows you to pass arguments in any order, as long as you specify the parameter names.

For example, in #lst-keyword-arguments, we adjust the calls to the function calculate_speed (see Listing 12.8) using keyword arguments. This way, we can pass the arguments in any order.

Listing 12.9: Using keyword arguments to specify the order of arguments when calling a function calculate_speed (see Listing 12.8). The function expects to receive first the distance and then the time. The order of the arguments is not important when using keyword arguments.
# Correct order of arguments using keyword arguments
calculate_speed(time_h=10, distance_km=100)  # Output: 10

12.10 Returning Multiple Values

In Python, a function can return multiple values by separating them with commas. When multiple values are returned, they are packed into a tuple. You can then unpack the tuple into separate variables when calling the function.

For example, in #lst-return-multiple-values, the function get_name_and_surname returns two values: the name and the surname. When calling the function, we unpack the tuple into two variables: name and surname.

Listing 12.10: Function to split a full name into name and surname. split is a built-in method that splits a string into a list of substrings at a specified separator (the space character by default). The function returns two values, which are unpacked into separate variables.
def get_name_and_surname(full_name):
    name, surname = full_name.split()
    return name, surname

full_name = "Alice Smith"
name, surname = get_name_and_surname(full_name)
What Is a Tuple?

A tuple is an ordered collection of elements enclosed in parentheses ( ). Tuples are similar to lists, but they are immutable, meaning their elements cannot be changed after creation. Tuples are often used to store related data that should not be modified.

12.11 Functions Without Return Values

In Python, functions that perform actions without returning a meaningful value are still defined using the def keyword. If a function does not have a return statement, it returns None by default.

For example in #lst-display-message, the function display_message prints a message without returning a value.

Listing 12.11: Function that prints a message without returning a value. The first call to the function will display the message “Hello, World!” The second call will display the message again, but the variable a will be assigned None.
def display_message():
    print("Hello, World!")

display_message()
a = display_message()

12.12 Pre-Defined Functions

Pre-defined functions are built-in functions that perform common tasks. They are used to simplify coding and avoid redundancy. Most programming languages have a set of built-in functions; therefore, before “reinventing the wheel,” check if there is a pre-defined function that can be used.

In the following, we will discuss some common pre-defined functions in Python:

  • print(objs, end="\n", sep=" "): Prints objects to the console. The end parameter specifies the string that is printed at the end (default is a newline), and the sep parameter specifies the separator between the objects (default is a space). For example:

    print("Hello", "World! ", end="", sep="-") 
    print("Welcome to Python!")

    Will output:

    Hello-World! Welcome to Python!
  • input(prompt=""): Reads a line of input from the console and returns it as a string. The optional prompt parameter is the message displayed to the user before input. For example:

    name = input("Enter your name: ")
    print("Hello,", name)

    Will prompt the user to enter their name and then display a greeting.

  • type(obj): Returns the type of an object. For example:

    x = 5
    print(type(x))

    Will output:

    <class 'int'>
  • len(obj): Returns the length of an object (e.g., a string, list, tuple, dictionary). For example:

    text = "Hello, World!"
    print(len(text))

    Will output:

    13
  • int(obj): Converts an object to an integer. For example:

    x = "10"
    y = 3.14
    print(int(x) + int(y))

    Will output:

    13
  • float(obj): Converts an object to a floating-point number. For example:

    x = "3.14"
    y = 10
    print(2 * float(x))
    print(float(y))

    Will output:

    6.28
    10.0
  • str(obj): Converts an object to a string. For example:

    x = 42
    y = 3.14
    print("The answer is: " + str(x))
    print("The value of pi is: " + str(y))

    Will output:

    The answer is: 42
    The value of pi is: 3.14
  • abs(num): Returns the absolute value of a number. For example:

    x = -10
    print(abs(x))

    Will output:

    10
  • round(num, ndigits=None): Rounds a number to a specified number of decimal places. If ndigits is not provided, the number is rounded to the nearest integer. For example:

    x = 3.14159
    print(round(x, 2))

    Will output:

    3.14
  • max(numbers), min(numbers)`: Returns the maximum or minimum value from a sequence of numbers. For example:

    print(max(10, 20, 30, 40, 50))
    print(min(10, 20, 30, 40, 50))

    Will output:

    50
    10

12.13 Naming Conventions

When naming functions in Python, it is important to follow naming conventions to make the code more readable and maintainable. The rules for naming functions are similar to the rules for naming variables (see Variable Naming Conventions). Here are some aditional guidelines for naming functions:

  • Use snake_case for function names: Function names should be in lowercase and words should be separated by underscores. For example, calculate_gpa, display_results, validate_input.
  • Use descriptive names: Choose function names that clearly describe what the function does. This makes the code easier to understand and maintain.
  • Use verbs for action functions: Function names should typically start with a verb or verb phrase to indicate the action performed by the function. For example:
    • calculate_gpa
    • display_student_info
    • get_total_score
    • is_input_valid
    • is_capacity_reached
    • has_permission
    • validate_input

12.14 Type Hints in Python

Starting from Python 3.5, we can use type hints (also known as type annotations) to specify the expected data types of variables, function parameters, and return values. This can help in code readability and IDEs can use this information for type checking.

12.14.1 Static vs. Dynamic Typing

Python is a dynamically typed language, which means that the data type of a variable is determined at runtime based on the value assigned to it. For example, in Listing 12.12, the variable x is assigned an integer value, and later it is assigned a string value. The data type of the variable x changes based on the assigned value.

Listing 12.12: Example of dynamic typing in Python. The variable x is assigned an integer value and later a string value. The data type of the variable changes based on the assigned value.
x = 10 * 10
print(type(x))  # Output: <class 'int'>
x = 'X' * 10
print(type(x))  # Output: <class 'str'>
<class 'int'>
<class 'str'>

Dynamic typing allows for flexibility but can lead to errors if the wrong type of value is assigned to a variable. For example, in Listing 12.13, the variable x is assigned a string value, and the variable y is assigned an integer value. When the code tries to add x and y, a TypeError occurs because Python does not allow adding a string and an integer.

Listing 12.13: Example of dynamic typing error in Python. The variable x is assigned a string value, and the variable y is assigned an integer value. When the code tries to add x and y, the error: TypeError: can only concatenate str (not "int") to str occurs. Python will not prevent you from assigning different types of values to variables.
def sum_numbers(x, y):
    return x + y

x = "Hello, "
y = 42
print(sum_numbers(x, y))

In contrast, statically typed languages, such as C++ and Java, require the data type of a variable to be declared explicitly. The data type of a variable is determined at compile time, and the compiler checks for type errors before the code is executed. Therefore, in statically typed languages, the code will not compile if the wrong type of value is assigned to a variable; the example in Listing 12.13 would not compile in a statically typed language.

Therefore, although dynamic typing provides flexibility, it can lead to errors that are not caught until runtime.

Runtime vs. Compile time
  • Compile time: Refers to the time when the code is compiled (translated into machine code).
  • Runtime: Refers to the time when the code is executed (after being compiled).

12.14.2 Using Type Hints

To add type hints to a function, you can specify the data types of the parameters using a colon (:) after the parameter name, followed by the data type. You can also specify the return type of the function using the -> arrow followed by the return type.

For example, in Listing 12.14, the function add_one_to_with_type_hint adds one to a number and specifies the data types of the parameter num and the return value result.

Listing 12.14: Function to add one to a number with type hints. The parameter num is expected to be an integer, and the return value is an integer.
def add_one_to_with_type_hint(num: int) -> int:
    result: int = num + 1
    return result

In Listing 12.15, we have three versions of a function to sum numbers. The first function func1 does not explicitly declare the types of variables. The second function func2 uses type hints to specify that the parameters and return value are integers. The third function func3 uses type hints to specify that the parameters and return value are floating-point numbers.

Listing 12.15: Three versions of a function to sum numbers.
(a) Function to sum numbers without explicitly declaring the types of variables (no type hints).
def func1(
        p1,
        p2,
        p3
    ):
    return (
        p1
        + p2
        + p3
    )
(b) Function to sum integers with type hints. This function also sums floating-point numbers.
def func2(
        p1: int,
        p2: int,
        p3: int
    ) -> int:
    return (
        p1
        + p2
        + p3
    )
(c) Function to sum floating-point numbers with type hints. This function also sums integers.
def func3(
        p1: float,
        p2: float,
        p3: float
    ) -> float:
    return (
        p1
        + p2
        + p3
    )

In Figure 12.1, you can see type hints in action in Visual Studio Code. The editor shows the expected types of the parameters and return values. Using the hints, a programmer can understand the expected types and write code accordingly.

Figure 12.1: Type hints in action in VS Code. The editor shows the expected types of the parameters and return values. The autocomplete feature suggests the expected types, which can help in writing correct code.

Since the hints are not enforced by the Python interpreter (i.e., they are not used for type checking), the code will run even if the types do not match the hints. In Listing 12.16, the function func2 expects integer parameters and returns an integer, but it can receive even str or float values.

Listing 12.16: Testing the function func2 with different types of arguments. The function expects integer parameters and returns an integer, but it can receive any type of value due to Python’s dynamic typing. (Note: due to precision issues, the output of the first call may not be exactly 7.7. In computers, floating-point numbers are stored in binary format, which can lead to small rounding errors.)
print(func2(1.1, 2.2, 4.4))  # Output: 7.7
print(func2("This ", "is ", "Sparta"))
7.700000000000001
This is Sparta
A Hint Only

Type hints are not enforced by the Python interpreter, meaning that the code will run even if the types do not match the hints. Type hints are used for documentation and can be used by external tools for type checking.

12.15 Call Stack

The call stack is a data structure that stores information about the active subroutines (functions) of a program. When a function is called, a new frame is added to the top of the call stack to store information about the function call, such as the function’s parameters and local variables.

When a function returns, its frame is removed from the call stack, and the program continues execution from the point where the function was called. The call stack ensures that functions are executed in the correct order and that local variables are stored separately for each function call.

In Listing 12.17, we have a simple example of a call stack in Python. The function main calls func1, which in turn calls func2. Inside each function, we print the value of a local variable x. Every time a function is called, a new frame is added to the call stack, and when the function returns, its frame is removed from the call stack.

Listing 12.17: Example of a call stack in Python. The function main calls func1, which in turn calls func2. When a function is called, a new frame is added to the call stack. This frame has its own scope, that is, its own set of local variables. When a function returns, its frame is removed from the call stack.
def func2():
    x = 10
    print("Inside func2", x)

def func1():
    x = 20
    print("Inside func1", x)
    func2()

def main():
    x = 30
    print("Inside main", x)
    func1()

main()

When you run the code in Listing 12.17, you will see the following output:

Inside main 30
Inside func1 20
Inside func2 10

The output shows the order of function calls and the values of the local variables x in each function. The call stack ensures that functions are executed in the correct order and that local variables are stored separately for each function call.

In the example in Listing 12.17, the variable x is defined in each function with the same name. However, each function has its own scope, and the variable x is local to that scope. Therefore, the variable x in func2 does not overwrite the variable x in func1 or main. An analogy is that each function is like a separate room in a house, and the variables are like items stored in each room. You can have items with the same name in different rooms without them interfering with each other because each room has its own storage space.

But what is at the bottom of the call stack, below main? The bottom of the call stack is the initial frame that is created when the program starts. This frame contains information about the program itself and is the starting point for the call stack. Below, you can see a visualization of the call stack for the example in Figure 12.2.

main()

Stack (1)

func1()

main()

Stack (2)

func2()

func1()

main()

Stack (3)

func1()

main()

Stack (4)

main()

Stack (5)

Empty

Stack (6)

Figure 12.2: Visualization of the call stack for the example in Listing 12.17. The function being executed is at the top of the stack, and the initial frame is at the bottom. The call stack starts with the initial frame for the program (main). When a function is called, a new frame is added to the call stack. When a function returns, its frame is removed from the call stack. When the program finishes, the call stack is empty.

12.16 Variable Scope: Local vs. Global

The lifetime of a variable refers to the period during which the variable exists in memory. In Python, the lifetime of a variable is determined by its scope, which is the region of the code where the variable is accessible.

  • Local Variables: Variables defined inside a function have local scope and are created when the function is called. They exist as long as the function is executing and are destroyed when the function returns.
  • Global Variables: Variables defined outside any function have global scope and are accessible throughout the program. They are created when the program starts and are destroyed when the program ends.

In Listing 12.18, we have an example of local and global variables in Python. The function func defines a local variable x, and the program defines a global variable y. When the function is called, the local variable x is created, and when the function returns, x is destroyed. The global variable y exists throughout the program’s execution.

Listing 12.18: Example of local and global variables in Python. The function func defines a local variable x, and the program defines a global variable y. The local variable x is created when the function is called and is destroyed when the function returns. The global variable y exists throughout the program’s execution.
y = 10

def func():
    x = 20
    print("Inside func:", x, y)

func()
print("Outside func:", y)

When you run the code in Listing 12.18, you will see the following output:

Inside func: 20 10
Outside func: 10

12.16.1 Variable Shadowing

When a local variable has the same name as a global variable, the local variable shadows the global variable. This means that the local variable takes precedence over the global variable within its scope. For example, in Listing 12.19, the function func defines a local variable y with the same name as the global variable y. When the function is called, the local variable y shadows the global variable y.

Listing 12.19: Example of variable shadowing in Python. The function func defines a local variable y with the same name as the global variable y. The local variable y shadows the global variable y within the function’s scope.
y = 10

def func():
    x = 20
    y = 20
    print("Inside func:", x, y)

func()
print("Outside func:", y)

When you run the code in Listing 12.19, you will see the following output:

Inside func: 20 20
Outside func: 10

The output shows that the local variable y inside the function func shadows the global variable y. Inside the function, y refers to the local variable, and outside the function, y refers to the global variable.

12.16.2 The global Keyword

In Python, you can modify a global variable inside a function using the global keyword. This allows you to change the value of a global variable from within a function.

In Listing 12.20, the function modify_global uses the global keyword to modify the global variable y. The function increments the value of y by 10.

Listing 12.20: Example of modifying a global variable inside a function using the global keyword. The function modify_global increments the value of the global variable y by 10.
y = 10

def modify_global():
    global y
    y += 10

print(y)
modify_global()
print(y)

When you run the code in Listing 12.20, you will see the following output:

10
20

The output shows that the value of the global variable y is incremented by 10 inside the function modify_global.

12.16.3 Avoiding Global Variables

While global variables can be useful, they can also lead to code that is difficult to understand and maintain. But why are global variables evil? Since global variables can be accessed and modified from anywhere in the program, they can introduce hidden dependencies and side effects that make the code more complex, error-prone, and difficult to debug. If a function relies on global variables, you cannot easily determine the function’s behavior by looking at its code alone. You need to know the state of the global variables at the time the function is called, which can be challenging in a large codebase.

Therefore, using global variables is generally discouraged as it can quickly lead to Spaghetti code (code that is difficult to understand and maintain due to its complex and tangled structure).

12.16.4 Black Box Principle

To avoid global variables, you can use function parameters to pass values between functions or return values from functions. This makes the code more modular, easier to test, and less error-prone. A function can be considered a black box that takes input (parameters) and produces output (return value) without relying on external state. When a function is self-contained and does not depend on global variables, it is easier to reason about its behavior and test it in isolation. When you use built-in functions like print, len, or abs, you are using functions that do not rely on global variables. They take input (arguments) and produce output without modifying external state. As users, we do not need to know how these functions work internally; we only need to know how to use them.

12.16.5 When to Use Global Variables

While it is generally recommended to avoid global variables, there are situations where they can be useful:

  • Constants: Global variables can be used to store constants that are used throughout the program.
  • Configuration Settings: Global variables can be used to store configuration settings that are accessed by multiple functions. For example, database connection settings, logging levels, etc.
  • Caching: Global variables can be used to store cached data that needs to be accessed by multiple functions. For example, the result of an expensive computation that is reused multiple times.

When using global variables, it is important to document their purpose and usage to make the code easier to understand and maintain.

What Is a Cache?

A cache is a temporary storage area that stores frequently accessed or recently used data to speed up access to that data. Caching is used to reduce the time it takes to access data by storing a copy of the data in a faster storage location (e.g., memory) so that it can be retrieved more quickly.

12.17 By-Reference vs. By-Value

In Python, function arguments can be passed by reference or by value, depending on the type of the argument. Understanding the difference between passing by reference and passing by value is important for writing correct and efficient code.

  • Pass by Value: When a function is called with a value as an argument, a copy of the value is passed to the function. Changes made to the parameter inside the function do not affect the original value.
  • Pass by Reference: When a function is called with a reference (memory address) as an argument, changes made to the parameter inside the function affect the original value.

In Python, immutable objects (e.g., numbers, strings, tuples) are passed by value, while mutable objects (e.g., lists, dictionaries, objects) are passed by reference.

In Listing 12.21, the function modify_value takes an integer as an argument and increments the value by 10. When the function is called, a copy of the integer x is passed to the function, and the original value of x is not modified.

Listing 12.21: Example of passing a value by value in Python. The function modify_value takes an integer as an argument and increments the value by 10. The original value of x is not modified.
def modify_value(x):
    x += 10
    print("Inside function:", x)

x = 10
modify_value(x)
print("Outside function:", x)
Inside function: 20
Outside function: 10

In Listing 12.22, the function modify_list takes a list as an argument and appends a new element to the list. When the function is called, a reference to the list lst is passed to the function, and changes made to the list inside the function affect the original list.

Listing 12.22: Example of passing a reference by reference in Python. The function modify_list takes a list as an argument and appends a new element to the list. Changes made to the list inside the function affect the original list.
def modify_list(lst):
    lst.append(4)
    print("Inside function:", lst)

my_list = [1, 2, 3]
modify_list(my_list)
print("Outside function:", my_list)
Inside function: [1, 2, 3, 4]
Outside function: [1, 2, 3, 4]

12.17.1 Function Documentation

When writing functions, it is important to provide documentation that describes what the function does, its parameters, and its return value. This documentation helps other programmers understand how to use the function and what to expect from it.

In Python, you can add documentation to a function using docstrings. A docstring is a string literal that appears as the first statement in a function and provides information about the function. Docstrings are enclosed in triple quotes (""") and can span multiple lines.

In Listing 12.23, we provide two versions of the function calculate_area using different styles of docstrings (NumPy and Google). The function calculates the area of a circle given its width and height and an optional parameter precision to specify the number of decimal places in the result.

Listing 12.23: Two versions of the function calculate_area with different styles of docstrings (NumPy and Google). The function calculates the area of a rectangle given its width and height.
(a) Numpy-style docstring.
def calculate_area_circle(radius, precision=2):
    """
    Calculate the area of a circle.

    Parameters
    ----------
    radius : float
        The radius of the circle.
    precision : int, optional
        The number of decimal places in the result (default is 2).

    Returns
    -------
    float
        The area of the circle.

    Examples
    --------
    >>> calculate_area_circle(5)
    78.54
    >>> calculate_area_circle(5, 3)
    78.540
    """
    return np.pi * radius ** 2
(b) Google-style docstring.
def calculate_area_circle(radius, precision=2):
    """
    Calculate the area of a circle.

    Args:
        radius (float): The radius of the circle.
        precision (int, optional): The number of decimal 
            places in the result (default is 2).

    Returns:
        float: The area of the circle.

    Examples:
        >>> calculate_area_circle(5)
        78.54
        >>> calculate_area_circle(5, 3)
        78.540
    """
    return np.pi * radius ** 2
Quick Documentation in VS Code

If you are using an IDE like Visual Studio Code, you can hover over a function to see its docstring. This can help you understand what the function does and how to use it without looking at the function’s implementation. Also, by using extensions like AutoDocstring in VS Code, you can generate docstrings automatically by typing """ and pressing Enter above a function definition. This can save you time and ensure that your functions are well-documented.

For a more comprehensive guide on writing docstrings, you can refer to the NumPy Docstring Guide (see example) and the Google Python Style Guide.

12.18 Exercises

12.18.1 Testing Pre-Defined String Functions

Create functions to test the following built-in string functions applied to a str string:

  • str.strip(): Removes leading and trailing spaces from a string.
  • str.upper(): Converts a string to uppercase.
  • str.lower(): Converts a string to lowercase.
  • str.replace(old, new): Replaces occurrences of the substring old with the substring new in a string.
  • str.split(sep): Splits a string into a list of substrings based on a separator sep.
  • str.find(sub): Returns the index of the first occurrence of the substring sub in a string.
  • str.count(sub): Returns the number of occurrences of the substring sub in a string.
  • str.isalpha(): Returns True if all characters in a string are alphabetic.
  • str.isdigit(): Returns True if all characters in a string are digits.

Create a function for each of the above functions and test them with different input strings. Below, you can see an example of testing the len() and count() functions:

def test_len():
    assert len("Abracadabra") == 11

def test_count():
    assert "Abracadabra".count("a") == 4
    assert "01010101".count("01") == 4

12.18.2 Testing Pre-Defined Math Functions

Create functions to test each one of the following built-in or math module functions:

  • abs(): Returns the absolute value of a number.
  • math.sqrt(): Returns the square root of a number.
  • round(): Rounds a number to a specified number of decimal places.
  • int(): Converts a number to an integer (truncates the decimal).
  • math.pi: Returns the mathematical constant Pi (approximately 3.14159).
  • random.random(): Generates a (pseudo) random number between 0 and 1.
  • math.exp(): Returns the exponential value of a number.

12.18.3 Mayer Multiple Calculation

The Mayer Multiple is a ratio used to evaluate the price of Bitcoin in relation to its historical performance. It is calculated by dividing the current price of Bitcoin by the 200-day moving average (200 DMA) of its price:

\[ \text{Mayer Multiple} = \frac{\text{Current Price}}{\text{200 DMA}} \]

The logic for using the Mayer Multiple in trading decisions is simple:

  • If the Mayer Multiple is greater than 2.4, it signals that Bitcoin is overbought and a potential sell signal.
  • If the Mayer Multiple is less than 1.0, it indicates that Bitcoin is underbought and might be a good opportunity to buy.
  • Otherwise, it suggests holding the position.

Create a function calculate_mayer_multiple(bitcoin_price, dma_200) to calculate the Mayer Multiple given the current price of Bitcoin and the 200-day moving average. Your function should:

  • Receive the current price of Bitcoin and the 200-day moving average as arguments.
  • Calculate the Mayer Multiple using the formula provided.
  • Return the Mayer Multiple.

Then, create three functions to determine the trading decision based on the Mayer Multiple:

  • sell_signal(mayer_multiple): Returns True if the Mayer Multiple is greater than 2.4.
  • buy_signal(mayer_multiple): Returns True if the Mayer Multiple is less than 1.0.
  • hold_signal(mayer_multiple): Returns True if the Mayer Multiple is between 1.0 and 2.4.

Test your functions with different values to ensure they are working correctly.

12.19 Multiple-Choice Questions

12.19.1 Function Syntax

Which of the following functions will return the square of a number passed as an argument?

Choose the correct option:

Listing 12.24: Square number function.
(a)
def sqr(num):
    result = num ** 2
(b)
def sqr(num):
    return
    result = num ** 2
(c)
def sqr(num):
    return num ** 2
(d)
def sqr(num):
    result = num ** 2
    return

Answer: Listing 12.24 (c)

The function must return the square of the number passed as an argument. The correct syntax is to assign the result to the function name (SquareNumber = num ^ 2).

12.19.2 Calling a Function

Which of the following will display the message "Hello, World!" in the console? Assume that the function main is the entry point for the program.

Choose the correct option:

Listing 12.25: Calling a function from a function main().
(a)
def hello_world():
    print("Hello, World!")

def main():
    hello_world()
(b)
def hello_world():
    return "Hello, World!"

def main():
    hello_world()
(c)
def hello_world():
    print("Hello, World!")

def main():
    print(hello_world())
(d)
def hello_world():
    return
    print("Hello, World!")

def main():
    hello_world()

Answer: Listing 12.25 (a)

  • Listing 12.25 (a): The function hello_world prints the message, and the main function calls it.
  • Listing 12.25 (c): The function hello_world prints the message, but the main function tries to print the return value of hello_world, which is None.
  • Listing 12.25 (b): The function hello_world returns the message, but the main function does not print it.
  • Listing 12.25 (d): The function hello_world returns None before printing the message, so the message is not displayed.

12.19.3 Eye Array Drawing Algorithm

Which of the following flowcharts correctly represents the algorithm of a function called draw_eye_array that generates a symmetrical array (eye pattern) in a 2D list?

The algorithm should create a square array of size N x N and set the value of the diagonal cells to 1 and the rest to 0.

An array is a data structure that stores a collection of elements, typically of the same type. A 2D array is an array of arrays, where each element is itself an array. In this case, the 2D array represents a matrix or grid of values.

For example, for a 5x5 array, the output should be:

\[ \begin{array}{ccccc} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \\ \end{array} \]

Figure 12.3: Eye Pattern Array (5x5). The diagonal cells are set to 1, and the rest are set to 0. The positions are indexed starting from 0, for example, the first value is in position (0,0), the second in (1,1), and so on.

The function draw_eye_array should read the dimension N and generate the array.

Setting List Values

Consider that the command array[row][col] = 1 sets the value of the cell in the row and col position to 1. For example, array[0][0] = 1 sets the value of the first cell to 1.

Select the correct flowchart from the following options:

  1. Flochart in Figure 12.4 (a)
  2. Flochart in Figure 12.4 (b)
  3. Flochart in Figure 12.4 (c)
  4. Flochart in Figure 12.4 (d)
graph LR
    A((Start)) --> B[/"N"/]
    B --> R["Set\nrow = 0"]
    R --> C{"Is\nrow > N?"}
    E -- No --> J["Increment\nrow by 1"]
    C -- No --> E{"Is\ncol > N?"}
    E -- Yes --> EYE{"Is\nrow\nequal to\ncol?"}
    EYE -- Yes --> F[/"array[row][col] = 1"/]
    EYE -- No --> G[/"array[row][col] = 0"/]
    F --> I["Increment\ncol by 1"]
    J --> C
    G --> I
    I --> E
    C -- Yes --> L((End))
(a)
graph LR
    A((Start)) --> B[/"N"/]
    B --> R["Set\nrow = 0"]
    R --> C{"Is\nrow >= N?"}
    E -- No --> J["Increment\nrow by 1"]
    C -- No --> D["Set\ncol = 0"]
    D --> E{"Is\ncol >= N?"}
    E -- Yes --> EYE{"Is\nrow\nequal to\ncol?"}
    EYE -- Yes --> F[/"array[row][col] = 1"/]
    EYE -- No --> G[/"array[row][col] = 0"/]
    F --> I["Increment\ncol by 1"]
    J --> C
    G --> I
    I --> E
    C -- Yes --> L((End))
(b)
graph LR
    A((Start)) --> B[/"N"/]
    B --> R["Set\nrow = 0"]
    R --> C{"Is\nrow > N?"}
    E -- No --> J["Increment\nrow by 1"]
    C -- No --> D["Set\ncol = 0"]
    D --> E{"Is\ncol > N?"}
    E -- Yes --> EYE{"Is\nrow\nequal to\ncol?"}
    EYE -- Yes --> F[/"print(1)"/]
    EYE -- No --> G[/"print(0)"/]
    F --> I["Increment\ncol by 1"]
    J --> C
    G --> I
    I --> E
    C -- Yes --> L((End))
(c)
graph LR
    A((Start)) --> B[/"N"/]
    B --> R["Set\nrow = 0"]
    R --> C{"Is\nrow >= N?"}
    C -- No --> D["Set\ncol = 0"]
    D --> E{"Is\ncol >= N?"}
    E -- Yes --> J["Increment\nrow by 1"]
    E -- No --> EYE{"Is\nrow\nequal to\ncol?"}
    EYE -- Yes --> F[/"array[row][col] = 1"/]
    EYE -- No --> G[/"array[row][col] = 0"/]
    F --> I["Increment\ncol by 1"]
    J --> C
    G --> I
    I --> E
    C -- Yes --> L((End))
(d)
Figure 12.4: Alternative flowcharts for the algorithm to generate an eye pattern array in a 2D list.

Answer: Flowchart in Figure 12.4 (d)

Explanation:

  • Figure 12.4 (d): The algorithm reads the dimension N, sets the initial row and column values, and then iterates over the rows and columns to set the cell values based on the condition “Is row equal to col”.
  • Figure 12.4 (a): The algorithm does not set the initial column value before checking if the column is greater than N. Also, row > N should be row >= N, since the last row is N-1.
  • Figure 12.4 (b): When the column is greater than or equal to N (i.e., is col >= N? evaluates to Yes), the algorithm should Increment row by 1 because all columns in the row have been set. However, the flowchart inverted the logic and increments the column instead.
  • Figure 12.4 (c): The algorithm prints 1 and 0 instead of setting the values in the array. The correct action is to set the values in the array based on the condition “Is row equal to col”.

In the correct version, the algorithm reads the dimension N, sets the initial row and column values, and then iterates over the rows and columns to set the cell values based on the condition “Is row equal to col”.

The Python code is as follows:

Listing 12.26: Python code for the function draw_eye_array to generate an eye pattern array in a 2D list. The code is here for reference only. You do not need to know the Python syntax to answer the question.
def draw_eye_array(N):
    array = [[0 for _ in range(N)] for _ in range(N)]
    
    for row in range(N):
        for col in range(N):
            if row == col:
                array[row][col] = 1
            else:
                array[row][col] = 0
    
    return array

# Example usage
n = 5
eye_array = draw_eye_array(n)
for row in eye_array:
    print(row)
[1, 0, 0, 0, 0]
[0, 1, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 1, 0]
[0, 0, 0, 0, 1]

12.19.4 Global vs. Local Variables 1

What is the output of the code in Listing 12.27?

Listing 12.27: A code snippet that defines a global variable x and a function func.
x = 10

def func():
    x += 5
    print(x)

func()
print(x)

Choose the correct option:

  • The code will print 15 and 10.
  • The code will print 10 and 15.
  • The code will print 15 and 15.
  • The code will raise a UnboundLocalError.

Answer: The code will raise a UnboundLocalError.

More specifically, the error is UnboundLocalError: local variable 'x' referenced before assignment. The variable x is defined outside the function, so it is considered a global variable. To modify a global variable inside a function, you need to use the global keyword.

12.19.5 Global vs. Local Variables 2

What is the output of the code in Listing 12.28?

Listing 12.28: A code snippet that defines a global variable x and a function func.
x = 10

def func():
    global x
    x += 5
    print(x)

func()
print(x)

Choose the correct option:

  • The code will print 15 and 10.
  • The code will print 10 and 15.
  • The code will print 15 and 15.
  • The code will raise a UnboundLocalError.

Answer: The code will print 15 and 15.

The global keyword is used to modify the global variable x inside the function func. The value of x is incremented by 5 inside the function and outside the function.

12.19.6 Global vs. Local Variables 3

What is the output of the code in Listing 12.29?

Listing 12.29: A code snippet that defines a global variable x and a function func.
x = 10

def func():
    x = 15
    print(x)

func()
print(x)

Choose the correct option:

  • The code will print 15 and 10.
  • The code will print 10 and 15.
  • The code will print 15 and 15.
  • The code will raise a UnboundLocalError.

Answer: The code will print 15 and 10.

The variable x inside the function func is a local variable that shadows the global variable x. The local variable x is assigned the value 15, and the global variable x remains unchanged.

12.19.7 Global vs. Local Variables 4

What is the output of the code in Listing 12.30?

Listing 12.30: A code snippet that defines a global variable x and a function func.
x = 10

def func():
    x = 15
    print(x)
    global x
    print(x)

func()
print(x)

Choose the correct option:

  • The code will print 15, 10, and 15.
  • The code will print 10, 15, and 15.
  • The code will raise a SyntaxError.
  • The code will raise a UnboundLocalError.

Answer: The code will raise a SyntaxError.

More specifically, the error is SyntaxError: name 'x' is used prior to global declaration. The global keyword must be used before the variable is referenced in the function. Placing the global keyword after the variable assignment will raise a SyntaxError.

12.19.8 Global vs. Local Variables 5

What is the output of the code in Listing 12.31?

Listing 12.31: A code snippet that defines global variables x and y and a function func.
x = 10
y = 15

def func():
    global x
    x = 'A'
    global y
    y = 'B'
    print(x)
    print(y)

func()
print(x)
print(y)

Choose the correct option:

  • The code will print A, B, A, and B.
  • The code will print A, B, 10, and 15.
  • The code will print 10, 15, A, and B.
  • The code will raise a SyntaxError.

Answer: The code will print A, B, A, and B.

The global keyword is used to modify the global variables x and y inside the function func. The values of x and y are changed inside the function and outside the function.

12.19.9 Type Hints

What is the output of the code in Listing 12.32?

Listing 12.32: A code snippet that defines a function add_one_to with type hints.
def add_one_to(num: int) -> int:
    result: int = num + 1
    return result

print(add_one_to(5))
print(add_one_to(3.14))

Choose the correct option:

  • The code will print 6 and raise a TypeError.
  • The code will print 6 and 4.
  • The code will raise a TypeError and print 4.
  • The code will print 6 and 4.14.

Answer: The code will print 6 and 4.14.

Since type hints are not enforced by the Python interpreter, the code will run even if the types do not match the hints. Therefore, the function add_one_to can accept both integer and floating-point numbers as input.