4. Functions#

Hide code cell source

import sys
from pathlib import Path

current = Path.cwd()
for parent in [current, *current.parents]:
    if (parent / '_config.yml').exists():
        project_root = parent  # ← Add project root, not chapters
        break
else:
    project_root = Path.cwd().parent.parent

sys.path.insert(0, str(project_root))

from shared import thinkpython, diagram, jupyturtle

# Register as top-level module so 'import jupyturtle' and
# 'from jupyturtle import ...' work in subsequent cells
sys.modules['jupyturtle'] = jupyturtle

Why custom functions?

The real power of programming comes from creating your own functions. A function is a reusable block of code that performs a specific task. Functions help you organize code, avoid repetition, and make programs easier to understand and maintain. For example, you write a function and (re)use it everywhere in your project, and when you need to modify the function, you only need to modify it in one place.

It may not be clear yet why it is worth the trouble to divide a program into functions. There are several reasons:

  • Creating a new function gives you an opportunity to name a group of statements, which makes your program easier to read and debug.

  • Functions can make a program smaller by eliminating repetitive code.

  • If you make a change to the code, with a function, you only have to make it in one place.

  • Dividing a long program into functions allows you to debug the parts one at a time as a module and then assemble them into a working whole.

  • Well-designed functions are often useful for many programs. Once you write and debug one, you can reuse it.

While there are plenty of Python built-in functions and modules, we still need to learn to design our own custom functions to perform problem-specific tasks, whereas the functions shipped with Python are general-purpose tools. The comparison below of built-ins and custom functions should give you a clear rationale for learning to build custom functions.

Feature

Built-in Functions

Custom Functions

Who defines them

Python

You

Need def

No

Yes

Always available

Yes

No

Reusable

Yes

Yes

Problem-specific

No

Yes

Can call other functions

No

Yes

Can accept defaults

Limited

Yes

Before we officially learn about functions, let us play with the Python turtle module.

4.1. jupyturtle#

Python Turtle graphics is an old module (created in 1967!) that has been used for decades for learning Python. Luciano Ramalho ported it to Jupyter Notebook while reviewing Allen Downey’s Python book . Turtle is fun to use and great for learning Python programming. Here we are using it to learn functions.

To use jupyturtle, you need to install and import the module.

%pip install jupyturtle    ### comment it out after installation
import jupyturtle
# %pip install jupyturtle
import jupyturtle
jupyturtle.make_turtle()
'
<shared.jupyturtle.Turtle at 0x108209160>

make_turtle creates a canvas, which is a space on the screen where we can draw, and a turtle, which is represented by a circular shell and a triangular head. The circle shows the turtle’s location, and the triangle indicates the direction it is facing.

Now we can use the functions defined in the module, like make_turtle and forward.

jupyturtle.make_turtle()       ### create a new canvas
jupyturtle.forward(100)
'

forward moves the turtle a given distance in the direction it’s facing, drawing a line segment along the way. The distance is in arbitrary units – the actual size depends on your computer’s screen.

We will use functions defined in the jupyturtle module many times, so it would be nice if we didn’t have to write the module name every time. That’s possible if we import the module like this.

from jupyturtle import make_turtle, forward

Note:

To check out the functions available in jupyturtle, use the dot operator (press the Tab keys after t to see.

This version of the import statement imports make_turtle and forward from the jupyturtle module, so we can call them like this.

make_turtle()
forward(100)
'

jupyturtle provides two other functions we’ll use, called left and right. We’ll import them like this.

from jupyturtle import left, right

left causes the turtle to turn left. It takes one argument, which is the angle of the turn in degrees. For example, we can make a 90-degree left turn like this.

make_turtle()
forward(50)
left(90)
forward(50)
'

jupyturtle provides two other functions we’ll use, called left and right. We’ll import them like this.

This program moves the turtle east and then north, leaving two line segments behind. Before you go on, see if you can modify the previous program to make a square.

4.1.1. Making a square#

Here’s one way to make a square.

make_turtle()

forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
'

Because this program repeats the same pair of lines four times, we can do the same thing more concisely with a for loop.

make_turtle()
for i in range(4):
    forward(50)
    left(90)
'

4.1.2. Encapsulation#

Let’s take the square-drawing code from the previous section and put it in a function called square.

def square():
    for i in range(4):
        forward(50)
        left(90)

Now we can call the function like this.

make_turtle()     ### create a canvas object
square()          ### call a custom function
'
### Exercise 1: Polygon
### Try producing the same output as the cell below.
### For now, use forward(50).
### Your code starts here:




### Your code ends here.

Hide code cell source

def polygon(n):
    jupyturtle.make_turtle()
    
    angle = 360/n
    for i in range(n):
        jupyturtle.forward(50)
        jupyturtle.left(angle)

polygon(3)
polygon(4)
polygon(5)
'
'
'

4.2. Defining Functions#

A function definition specifies the name of a new function and the sequence of statements that run when the function is called.

The syntax of a Python function is:

def function_name([parameters]):
    """
    Docstring describing what the function does.
    """
    [function body]            
    [return value]     ### optional

We use the def keyword to define a function. The def line is called a header, and the rest of the lines are the body. The elements here are:

  • Header:

    1. def: Keyword that starts a function definition

    2. Function name: Follows variable naming rules (snake_case)

    3. Parameters: Input values (optional)

    4. Colon

  • Body

    1. Indentation (4 as recommended by PEP8)

    2. Docstring: Documentation string (recommended)

    3. Function body: Indented (instead of {}) code that runs when the function is called

    4. return: Sends a value back to the caller (optional, but almost always have)

def greet(name):
    """
    Print a greeting message to the user with their name.
    
    Parameters:
    name (str): The name of the user to greet.
    
    Returns:
    None
    """
    print(f"Hello, {name}!")

4.2.1. Parameters and Arguments#

With the elements in mind, we can already play with different patterns of functions. Let’s start with parameters. Python functions can have several types of parameters and arguments:

In the function definition:

  1. Positional Parameters: These are matched to arguments based on their position in the function definition.

  2. Default Parameters: These have a default value that’s used if no argument is provided. | Type | Matched By | Order Matters? | Can Use Keyword? | | ————– | ———- | ————– | —————————— | | Positional | Order | Yes | Yes | | Keyword | Name | No (must be after positional) | Yes |

  3. *args (Variable Positional Arguments): Allows a function to accept any number of positional arguments as a tuple. Note that *args itself is a name by convention.

  4. **kwargs (Variable Keyword Arguments): Allows a function to accept any number of keyword arguments as a dictionary. Note that **kwargs itself is a name by convention.

In a function call:

  1. Keyword-Only Parameters: Parameters that can only be passed by name, not by position. They come after *args or a single *.

  2. Keyword Arguments: You can pass arguments by specifying the parameter name, which allows any order.

Note that positional parameter/arguments must come before keyword parameter/arguments. The full precedence order is: positional => *args => keyword-only => **kwargs. Once you start naming arguments (e.g., age=35), everything after that must also be named.

The following example should make the ideas clear:

Now, some simpler patterns and examples:

  • no parameter

  • one parameter

  • multiple parameters

### 1. No Parameter
def greet():
    """Print a greeting message."""
    print("Hello!")

greet()     ### function call output: Hello, World!
Hello!
### 2. Single Parameters**
### Parameters allow functions to work with different inputs:
def greet_person(name):
    """Greet a person by name."""
    print(f"Hello, {name}!")

greet_person("Alice")  # Output: Hello, Alice!
greet_person("Bob")    # Output: Hello, Bob!
Hello, Alice!
Hello, Bob!
### 3. Multiple Parameters**
def add_numbers(a, b):
    """Add two numbers and return the result."""
    result = a + b
    return result

total = add_numbers(5, 3)      # Returns 8
print(total)
8

4.2.2. Default Parameters#

A default parameter has a default value. Parameters can have default values, and they can be overwritten by arguments.

def greet(name, greeting="Hello"):
    """Greet with a customizable greeting."""
    return f"{greeting}, {name}!"

print(greet("Alice"))              # Hello, Alice!   ### default is "Hello"
print(greet("Bob", "Hi"))          # Hi, Bob!        ### now it's Hi
print(greet("Charlie", "Hey"))     # Hey, Charlie!   ### now it's Hey
Hello, Alice!
Hi, Bob!
Hey, Charlie!
### === EXERCISE 2: Functions with Default Parameters ===
### Write a function called greet_with_title() that takes two parameters:
### - name: a person's name
### - title: a title with a default value of "Doctor"
### The function should print: "Hello, Doctor Alice!"
### Call it twice: once with and once without the title parameter.
### Your code starts here:




### Your code stops here.

Hide code cell source

### Solution:
def greet_with_title(name, title="Doctor"):
    print(f"Hello, {title} {name}!")

### Call with and without the title parameter
greet_with_title("Alice")
greet_with_title("Bob", "Professor")
Hello, Doctor Alice!
Hello, Professor Bob!
def power(base, exponent=2):
    """Calculate base raised to exponent (default is 2)."""
    return base ** exponent

print(f"\n5 squared: {power(5)}")        # Uses default exponent=2
print(f"5 cubed: {power(5, 3)}")         # Overrides default
print(f"2 to the 8th: {power(2, 8)}")
5 squared: 25
5 cubed: 125
2 to the 8th: 256
%%expect NameError
### === EXERCISE 3: Functions with Parameters ===
### Write a function called introduce() that takes two parameters: name and age
### The function should print a message like: "Hi, I'm Alice and I am 25 years old"
### Then call introduce() at least twice with different values.
### Your code starts here:



introduce("Alice", 25)
introduce("Bob", 30)
introduce("Charlie", 22)
### Your code stops here.
NameError: name 'introduce' is not defined

Hide code cell source

### Solution:
def introduce(name, age):
    print(f"Hi, I'm {name} and I am {age} years old")

### Test the function with different values
introduce("Alice", 25)
introduce("Bob", 30)
introduce("Charlie", 22)
Hi, I'm Alice and I am 25 years old
Hi, I'm Bob and I am 30 years old
Hi, I'm Charlie and I am 22 years old

4.2.3. *args and **kwargs#

In Python, *args and **kwargs are special syntax used to pass a variable number of arguments to a function. The names args and kwargs are conventional but not mandatory; the single (*) and double (**) asterisks are the key elements that enable this functionality. * means “many values”, while ** means “many key–value pairs”.

The *args: The *args syntax allows a function to accept any number of non-keyword (positional) arguments. Inside the function, these arguments are collected into a tuple.

The **kwargs (Arbitrary Keyword Arguments) syntax allows a function to accept any number of keyword arguments (arguments in the format key=value). Inside the function, these arguments are collected into a dictionary.

Together, *args and **kwargs extend the basic parameter patterns:

  • regular positional parameters (like name)

  • parameters with default values (like greeting="Hello")

  • multiple parameters (like a, b) that can now be followed by *args and **kwargs for extra values.

*args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary. As a comparison:

Feature

*args

**kwargs

Accepts

Positional arguments

Keyword arguments

Stored as

Tuple

Dictionary

Order matters

Yes

No

def describe_order(required, default=0, *args, **kwargs):
    """Mixes normal, default, *args, and **kwargs to show ordering."""
    print(f'required = {required}')
    print(f'default = {default}')
    print(f'args    = {args}')
    print(f'kwargs  = {kwargs}')

describe_order(10, 20, 30, 40, mode='fast', debug=True)
required = 10
default = 20
args    = (30, 40)
kwargs  = {'mode': 'fast', 'debug': True}
def sum_all(*args):
    """Calculates the sum of an arbitrary number of arguments."""
    print(type(args))           ### testing; tuple
    return sum(args)            ### variable name: args

print(sum_all(1, 2, 3))         # 6
<class 'tuple'>
6
def print_details(**kwargs):
    """Prints the key-value pairs of the provided details."""
    for key, value in kwargs.items():
        # print(type(kwargs))    ### checking type
        print(f"{key}: {value}")

print_details(name="John", age=30, city="New York")
name: John
age: 30
city: New York
### === EXERCISE 4: Write the add_numbers() function
### Write a function called add_numbers(*args) that:
### Takes any number of numeric arguments using *args
### Prints how many numbers were passed in
### Returns the sum of all the numbers
### Your code starts here:






### Your code stops here.

Hide code cell source

### Solution:
def add_numbers(*args):
    """Takes any number of arguments and returns their sum"""
    print(f"Number of values passed: {len(args)}")
    total = sum(args)
    return total


# Test it
result = add_numbers(10, 20, 30, 40)
print(f"Result: {result}")
Number of values passed: 4
Result: 100

4.2.4. Return Value#

Functions can return values using the return statement. As you have probably noticed, we usually store return values in a variable for later use. Note that:

  • A function without return returns None

  • You can return multiple values: return x, y, z, but still one thing: a tuple

  • return immediately exits the function

def calculate_area(length, width):
    """Calculate the area of a rectangle."""
    area = length * width
    return area

print(f"Area: {calculate_area(5, 3)}")    ### Area: 15

### or you may do

rect_area = calculate_area(5, 3)
print(f"Area: {rect_area}")               ### Area: 15
Area: 15
Area: 15
### === EXERCISE 5: Functions with Return Values ===
### Write a function called add_tax() that takes two parameters:
### - price: the original price
### - tax_rate: the tax rate as a decimal (e.g., 0.08 for 8%)
### The function should calculate and return the total price including tax
### Then call the function and print results for different prices.

### Your code starts here:




### Your code stops here.

Hide code cell source

### Solution:
def add_tax(price, tax_rate):
    tax_amount = price * tax_rate
    return price + tax_amount

### Test with different prices
total1 = add_tax(100, 0.08)
print(f"Price: $100, Tax Rate: 8%, Total: ${total1}")

total2 = add_tax(50, 0.1)
Price: $100, Tax Rate: 8%, Total: $108.0
def add_numbers(x, y):
    """Add two numbers and return the sum."""
    total = x + y
    return total

result = add_numbers(10, 5)
print(f"\nTotal: {result}")
Total: 15

4.2.5. Function Type Annotations#

A function can include type annotations for its parameters and return value. The parameter annotation goes after the parameter name, and the return annotation goes after -> in the function header.

def function_name(parameter: type) -> return_type:
    ...

These annotations are a lightweight contract. They tell readers and tools what kinds of values the function expects and returns, but Python does not enforce them automatically at runtime.

def greet(name: str) -> str:
    return f"Hello, {name}"

message = greet("Ada")
print(message)
Hello, Ada
def area_rectangle(width: float, height: float) -> float:
    return width * height

print(area_rectangle(3.0, 4.5))
13.5

Use -> None when a function performs an action but does not return a useful value. This is common for functions whose main job is printing, drawing, saving a file, or modifying an object.

def print_receipt(total: float) -> None:
    print(f"Total: ${total:.2f}")

result = print_receipt(12.5)
print(result)
Total: $12.50
None

A function with no return statement still returns None; the annotation simply makes that intention explicit.

4.2.6. Unpacking#

Taking elements out of a collection (tuple, list, dict) and passing them into individual parameters.

Observe the multiple return values of the following example. It has two values enclosed in parentheses, which form a tuple.

def func(a, b):
    return a, b

func(1, 2)
(1, 2)

Here we see that a tuple is returned, and we unpack the tuple into the variables when returned.

def get_stats(numbers):
    """Return min, max, and average of a list."""
    minimum = min(numbers)
    maximum = max(numbers)
    average = sum(numbers) / len(numbers)
    return minimum, maximum, average

data = [10, 20, 30, 40, 50]                     ### arguments
min_val, max_val, avg_val = get_stats(data)     ### returned and "unpacking"

print(f"Return type: {type(get_stats(data))}")
print(f"Stats for {data}:")
print(f"Min: {min_val}, Max: {max_val}, Average: {avg_val}")
Return type: <class 'tuple'>
Stats for [10, 20, 30, 40, 50]:
Min: 10, Max: 50, Average: 30.0
### === EXERCISE 6: Functions Returning Multiple Values ===
### Write a function called rectangle_stats() that takes two parameters:
### - length: the length of a rectangle
### - width: the width of a rectangle
### The function should calculate and return both the area and perimeter
### Then call the function and unpack the results.
### Your code starts here:





### Your code stops here.

Hide code cell source

### Solution:
def rectangle_stats(length, width):
    area = length * width
    perimeter = 2 * (length + width)
    return area, perimeter

### Call and unpack the results
area, perim = rectangle_stats(5, 3)
print(f"Rectangle (5x3): Area = {area}, Perimeter = {perim}")

area2, perim2 = rectangle_stats(10, 4)
Rectangle (5x3): Area = 15, Perimeter = 16

4.2.7. Scopes#

Variables created inside functions are local to that function:

def my_function():
    local_var = 10  # Only exists inside the function
    print(local_var)

my_function()     # Prints: 10
# print(local_var)  # Error: local_var doesn't exist here

Variables outside functions are global:

global_var = 100  # Accessible everywhere

def show_global():
    print(global_var)  # Can read global variable

show_global()  # Prints: 100

4.2.7.1. Variables and parameters are local#

When you create a variable inside a function, it is local, which means it exists only inside the function. For example, the following function takes two arguments, concatenates them, and prints the result twice.

%%expect NameError

def greet():
    message = "Hello"
    print(message)

greet()

print(message)   ### can't do! "message" variable is local to the function definition.
Hello
Hello, Ada
%%expect NameError

def greet():
    global message        ### must declare first
    message = "Hello"
    print(message)

greet()

print(message)   ### can do! "message" variable is now global in the function definition.
Hello
Hello

4.2.8. Function Composition#

Function composition is the process of using the output of one function as the input to another.

def double(x):
    return x * 2

def square(x):
    return x ** 2

result = square(double(3))
print(result)
36
### === EXERCISE 7: Function Composition ===
### Write three functions:
### - calculate_area(length, width): returns the area
### - format_result(value): returns 'Area is X square units'
### Then write report_rectangle() that uses both to calculate and format.
### Your code starts here:






### Your code stops here.

Hide code cell source

### Solution:
def calculate_area(length, width):
    return length * width

def format_result(value):
    return f'Area is {value} square units'

### Function composition: one function calls another
def report_rectangle(length, width):
    area = calculate_area(length, width)
    return format_result(area)

### Test
print(report_rectangle(5, 3))
print(report_rectangle(10, 2))
Area is 15 square units
Area is 20 square units
### === EXERCISE 9: Variable Scopes ===
### Write a function called modify_variables() that:
### - Takes a parameter x
### - Creates a local variable local_var = x * 2
### - Prints both x and local_var inside the function
### Then call it and try to print local_var outside (it should fail).

### Your code starts here:

Hide code cell source

### Solution:
def modify_variables(x):
    local_var = x * 2
    print(f"Inside: x={x}, local_var={local_var}")

### Call the function
modify_variables(5)

### This will cause NameError because local_var doesn't exist outside
Inside: x=5, local_var=10

When we run print_verse, it calls first_two_lines, which calls repeat, which calls print. That’s a lot of functions.

Of course, we could have done the same thing with fewer functions, but the point of this example is to show how functions can work together.

4.2.9. Docstrings#

Docstrings (documentation strings) are used to document functions/methods, classes, and modules. They use triple quotes and should be the first statement after defining a function or class.

Always document your functions with docstrings:

### Function with docstring
def greet(name):
    """
    This function does xxx and yyy.         ### 1. what this function is about
    
    Args:                                   ### 2. input parameters
        name: The person's name (string)    
    
    Returns:                                ### 3. what the function returns
        A greeting message (string)
    """
    return f"Hello, {name}!"

message = greet("Homer")       ### call the function

As an example:

def calculate_bmi(weight, height):
    """
    Calculate Body Mass Index (BMI).
    
    Args:
        weight: Weight in kilograms (float)
        height: Height in meters (float)
    
    Returns:
        BMI value (float)
    
    Example:
        >>> calculate_bmi(70, 1.75)
        22.86
    """
    return weight / (height ** 2)

Good docstrings include:

  1. What the function does

  2. Parameters and their types

  3. What the function returns

  4. Usage examples (optional)

### Practical functions with docstrings

def calculate_discount(price, discount_percent):
    """
    Calculate the final price after applying a discount.
    
    Args:
        price: Original price (float or int)
        discount_percent: Discount percentage (0-100)
    
    Returns:
        Final price after discount (float)
    """
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    return final_price

def is_valid_email(email):
    """
    Check if email has basic valid format.
    
    Args:
        email: Email address to validate (str)
    
    Returns:
        True if email contains @ and ., False otherwise
    """
    return '@' in email and '.' in email

def celsius_to_fahrenheit(celsius):
    """
    Convert Celsius to Fahrenheit.
    
    Args:
        celsius: Temperature in Celsius
    
    Returns:
        Temperature in Fahrenheit
    """
    return (celsius * 9/5) + 32

### Test the functions
print("Testing calculate_discount:")
original = 100
discount = 20
final = calculate_discount(original, discount)
print(f"  ${original} with {discount}% off = ${final}")

print("\nTesting is_valid_email:")
print(f"  'user@example.com' is valid: {is_valid_email('user@example.com')}")
print(f"  'invalid-email' is valid: {is_valid_email('invalid-email')}")

print("\nTesting celsius_to_fahrenheit:")
print(f"  0°C = {celsius_to_fahrenheit(0)}°F")
print(f"  100°C = {celsius_to_fahrenheit(100)}°F")
print(f"  37°C = {celsius_to_fahrenheit(37):.1f}°F")
Testing calculate_discount:
  $100 with 20% off = $80.0

Testing is_valid_email:
  'user@example.com' is valid: True
  'invalid-email' is valid: False

Testing celsius_to_fahrenheit:
  0°C = 32.0°F
  100°C = 212.0°F
  37°C = 98.6°F
### === EXERCISE 10: Writing Good Docstrings ===
### Write a function called validate_password() that:
### - Takes a password string as a parameter
### - Checks if it's at least 8 characters long
### - Returns True if valid, False otherwise
### Include a comprehensive docstring with description, Args, and Returns sections.

### Your code starts here:

Hide code cell source

### Solution:
def validate_password(password):
    """
    Validate if a password meets minimum length requirements.
    
    Args:
        password (str): The password to validate
    
    Returns:
        bool: True if password is at least 8 characters, False otherwise
    """
    return len(password) >= 8

### Test the function
print(validate_password("short"))        ### False
print(validate_password("verylongpassword"))  ### True
False
True

4.3. Lambda Functions#

Syntax: lambda arguments: expression

Lambda functions are small, anonymous functions that can have any number of arguments but can only have one expression. They are useful for short, simple functions that you don’t want to define formally.

4.3.1. Lambda Limitations#

  • Can only contain expressions, not statements

  • Cannot contain assignments, print statements, or other statements

  • Best for simple, one-line functions

  • For complex logic, use regular functions (def)

If you find yourself writing a multi-line lambda, switch to a normal def function instead.

### Basic lambda function
square = lambda x: x ** 2
print(square(5))  # Output: 25

### Lambda with multiple arguments
add = lambda x, y: x + y
print(add(3, 4))  # Output: 7
25
7

4.3.2. Using Lambda with Built-in Functions#

Lambda functions are commonly used with map(), filter(), and sorted().

### Using lambda with map: apply a function to each item
nums = [1, 2, 3, 4]
squares = list(map(lambda x: x ** 2, nums))
print('squares from map:', squares)

### Using lambda with filter: keep only even numbers
evens = list(filter(lambda x: x % 2 == 0, nums))
print('evens from filter:', evens)

### Using lambda with sorted: sort words by length
words = ['spam', 'wonderful', 'eggs', 'SPAM']
by_length = sorted(words, key=lambda w: len(w))
print('sorted by length:', by_length)
squares from map: [1, 4, 9, 16]
evens from filter: [2, 4]
sorted by length: ['spam', 'eggs', 'SPAM', 'wonderful']

4.4. Applications#

4.4.1. Return values and conditionals#

If Python did not provide abs, we could write it like this.

def absolute_value(x):
    if x < 0:
        return -x
    else:
        return x

If x is negative, the first return statement returns -x and the function ends immediately. Otherwise, the second return statement returns x and the function ends. So this function is correct.

However, if you put return statements in a conditional, you must ensure that every possible path through the program hits a return statement (exhaustive). For example, here’s an incorrect version of absolute_value.

def absolute_value_wrong(x):
    if x < 0:
        return -x
    if x > 0:
        return x

Here’s what happens if we call this function with 0 as an argument.

absolute_value_wrong(0)

We get nothing! Here’s the problem: when x is 0, neither condition is true, and the function ends without hitting a return statement, which means that the return value is None, so Jupyter displays nothing.

As another example, here’s a version of absolute_value with an extra return statement at the end.

def absolute_value_extra_return(x):
    if x < 0:
        return -x
    else:
        return x
    
    return 'This is dead code'

If x is negative, the first return statement runs and the function ends. Otherwise the second return statement runs and the function ends. Either way, we never get to the third return statement – so it can never run.

Code that can never run is called dead code. In general, dead code doesn’t do any harm, but it often indicates a misunderstanding, and it might be confusing to someone trying to understand the program.

4.4.2. Boolean functions#

Functions can return the boolean values True and False, which is often convenient for encapsulating a complex test in a function. For example, is_divisible checks whether x is divisible by y with no remainder.

def is_divisible(x, y):
    if x % y == 0:
        return True
    else:
        return False

Here’s how we use it.

is_divisible(6, 4)
False
is_divisible(6, 3)
True

Inside the function, the result of the == operator is a boolean, so we can write the function more concisely by returning it directly.

def is_divisible(x, y):
    return x % y == 0

Boolean functions are often used in conditional statements.

if is_divisible(6, 2):
    print('divisible')
divisible

It might be tempting to write something like this:

if is_divisible(6, 2) == True:
    print('divisible')
divisible

But the comparison is unnecessary.