Chapter 6: Functions and Loops

Functions are the building blocks of almost every Python program. They’re where the real action takes place! You’ve already used several functions such as print, len, and round. These are built‑in functions that come with Python. You can also create your own user‑defined functions to perform specific tasks.

Functions allow code reuse: instead of repeating similar code each time a program needs an operation, you simply call the function. But sometimes you need to repeat some code many times in a row—this is where loops come in.

In this chapter you will learn:

6.1 What Is a Function, Really?

In earlier chapters, you used functions such as print() and len() to display text and determine the length of a string. But what exactly is a function? In this section, we’ll examine len() to understand what a function is and how it executes.

Functions Are Values

One of the most important properties of a function in Python is that functions are values—they can be assigned to variables. Try inspecting the name len in the interactive window:

>>> len
<built‑in function len>

Python tells you that len is a variable whose value is a built‑in function. Just like integers have type int and strings have type str, functions have a type builtin_function_or_method:

>>> type(len)
<class ‘builtin_function_or_method’>

Because functions are values, you can reassign them—though it’s usually a bad idea:

>>> len = "I'm not the len you're looking for."
>>> len
"I'm not the len you're looking for."

Now len holds a string value instead of a function. As you might expect, type(len) now shows:

<class 'str'>

Although you can reassign names like len, doing so will shadow the built‑in function and cause confusion. To restore it, use the del keyword:

>>> del len
>>> len
<built‑in function len>

del removes the name binding, not the underlying object. Since len is built‑in, it reverts to its original definition automatically.

How Python Executes Functions

You can’t execute a function merely by typing its name; you must call it using parentheses.

>>> len
<built‑in function len>

>>> len()
Traceback (most recent call last):
  TypeError: len() takes exactly one argument (0 given)

An argument is the input passed into a function. Some functions take none, others many. len() requires one argument. When a function finishes executing, it returns a value as output. The process can be summarized in three steps:

  1. The function is called, and any arguments are passed as input.
  2. The function executes some action.
  3. The function returns, and the call is replaced by its return value.

For example:

num_letters = len("four")

len("four") computes 4 and replaces the call; after execution, the line effectively becomes num_letters = 4.

Functions Can Have Side Effects

Most functions return a value, but some also produce side effects—changes observable outside the function. You’ve already used one with a side effect: print().

>>> return_value = print("What do I return?")
What do I return?
>>> return_value
None

The text display is a side effect of print(), while its return value is None, an object of type NoneType that signifies “no data.”

>>> type(return_value)
<class 'NoneType'>

When you call print(), the visible text output is not the return value—it is the side effect.

Now that you know that functions are values like numbers and strings, and how they are called and executed, the next section shows how to define your own functions.

6.2 Write Your Own Functions

As your programs grow longer and more complex, you may find that the same sequences of code repeat themselves. While copy‑pasting may seem tempting, it introduces unnecessary maintenance headaches and confusion. The best way to avoid repetition and errors is to extract reusable logic into functions.

The Anatomy of a Function

Every function has two main parts:

  1. The function signature — defines the function’s name and its input parameters.
  2. The function body — contains the code that runs each time the function is called.

Let’s write a simple function that multiplies two numbers. We’ll call it multiply():

def multiply(x, y):
  product = x * y
  return product

Here’s how it works:

The Function Body and Return Statement

Everything indented beneath the signature belongs to the function body. Indentation defines scope in Python. By convention, you indent four spaces:

def multiply(x, y):
  product = x * y
  return product

The first line creates a new variable product that stores the result of the multiplication. The second line uses the return keyword to send that value back to the caller. Once the return statement executes, the function immediately stops running.

You can include as many lines as needed in a function body, as long as they are all properly indented to the same level. After return, any further indented code will never run.

Calling Your Function

Once you’ve defined multiply(), you can call it elsewhere in your code. Simply write the function’s name followed by parentheses containing the arguments:

num = multiply(2, 4)
print(num)

This displays:

8

Functions must be defined before they are called. If you attempt to call a function before Python has read its definition, you’ll encounter a NameError.

Functions With No Return Statement

Every function technically returns a value, even if you omit return. When a function has no return statement, it defaults to returning None.

def greet(name):
  print(f"Hello, {name}!")

result = greet("Alice")
print(result)

Output:

Hello, Alice!
None

You see the message because print() inside the function produces a side effect. But greet()’s return value is None.

Documenting Your Functions

Python provides the built‑in help() tool and docstrings to keep function documentation in code. A docstring is a triple‑quoted string immediately following the function signature. Here’s an improved version of multiply() with a docstring:

def multiply(x, y):
  """Return the product of two numbers x and y."""
  product = x * y
  return product

In the interactive window, you can display this documentation by typing:

>>> help(multiply)
Help on function multiply in module __main__:
multiply(x, y)
 Return the product of two numbers x and y.

Docstrings explain the function’s purpose and expected parameters. Most Python developers follow the conventions defined in PEP 257 for writing clear, concise docstrings.

Putting It All Together

Here’s a complete example saved as multiply.py:

def multiply(x, y):
  """Return the product of two numbers x and y."""
  product = x * y
  return product

num = multiply(3, 7)
print(num)

Output:

21

Review Exercises

  1. Create a function called cube() that takes one numeric parameter and returns that number cubed. Test your function with several inputs.
  2. Create a function called greet() that accepts a string parameter name and prints Hello name! using an f‑string.

Once you’ve mastered basic function definitions, you’re ready for the next section — a fun coding challenge on converting temperatures!

6.3 Challenge — Convert Temperatures

Write a script called temperature.py that defines two functions:

  1. convert_cel_to_far() — takes one float parameter representing degrees Celsius and returns a float representing the same temperature in degrees Fahrenheit using the formula:
F = C × (9 / 5) + 32
  1. convert_far_to_cel() — takes one float parameter representing degrees Fahrenheit and returns a float representing the same temperature in degrees Celsius using the formula:
C = (F − 32) × (5 / 9)

Your script should:

Example Run

Enter a temperature in degrees F: 72
72 degrees F = 22.22 degrees C

Enter a temperature in degrees C: 37
37 degrees C = 98.60 degrees F

This practice reinforces multiple concepts — function definition, user input, type conversion, and formatting numeric output with round().

6.4 Run in Circles (Loops)

One of the great things about computers is that they never complain about doing repetitive tasks. A loop is a block of code that runs repeatedly — either a fixed number of times or until a condition is met. Python provides two primary loop structures: while and for.

The While Loop

A while loop executes its body as long as its condition remains true. When the condition becomes false, the loop stops.

count = 0
while count < 5:
  print("count is", count)
  count += 1

Output:

count is 0
count is 1
count is 2
count is 3
count is 4

On each iteration, Python evaluates the expression count < 5. If the expression is true, the body executes. Once count reaches 5, the loop exits.

If the condition of a while loop never becomes false, you’ve created an infinite loop! Use a control variable like count carefully to ensure the loop can terminate.

The For Loop

A for loop is used to iterate over a sequence (such as a list, tuple, string, or range). Unlike while, it automatically updates the loop variable each time.

Example — Iterating Over a List

colors = ["red", "green", "blue"]
for color in colors:
  print(color)

This loop runs three times — once for each element in the list. Inside the loop, the variable color refers to each item in turn.

Using Range()

You can loop over a sequence of numbers generated by range(). For example:

for i in range(3):
  print(i)

Output:

0
1
2

Remember that range(3) generates 0, 1, and 2 — up to but not including 3.

Breaking and Continuing

Sometimes you need to alter the natural flow of a loop — either to exit early or skip a single iteration. Python provides two statements for this: break and continue.

for num in range(1, 6):
  if num == 4:
    break
  print(num)

Output:

1
2
3

The loop terminates immediately once num equals 4.

The continue statement jumps to the next iteration without running the rest of the body:

for num in range(1, 6):
  if num == 3:
    continue
  print(num)

Output:

1
2
4
5

Use break for stopping a loop entirely and continue for skipping just one round of iteration.

Combining Loops and Functions

You can combine functions with loops to perform repeated operations efficiently. For example, compute squares of numbers with a function and a for loop:

def square(x):
  return x ** 2

for n in range(1, 6):
  print(square(n))

Output:

1
4
9
16
25

6.5 Challenge — Track Your Investments

In this challenge, you will write a short program named invest.py that models how an investment grows over time. An initial deposit (principal amount) is made, and each year the total increases by a fixed percentage called the annual rate of return.

For example, an initial deposit of 100 with an annual rate of return of 5 increases to 105 the first year and then 110.25 the second year. Your task is to print the balance at the end of each year.

Specification

Define a function named invest() that accepts three parameters:

  1. amount — the initial principal
  2. rate — the annual interest rate as a decimal (e.g., 0.05 for 5%)
  3. years — the number of years to invest

The function should print the balance for each year, rounded to two decimal places.

def invest(amount, rate, years):
  for year in range(1, years + 1):
    amount = amount * (1 + rate)
    print(f"year {year}: {amount:.2f}")

Example Run

>>> invest(100, 0.05, 4)
year 1: 105.00
year 2: 110.25
year 3: 115.76
year 4: 121.55

Complete Program

def invest(amount, rate, years):
  for year in range(1, years + 1):
    amount = amount * (1 + rate)
    print(f"year {year}: {amount:.2f}")

principal = float(input("Enter the initial amount: "))
rate = float(input("Enter the annual rate (e.g. 0.05 for 5%): "))
years = int(input("Enter number of years: "))

invest(principal, rate, years)

6.6 Understand Scope in Python

Whenever you create variables, Python keeps track of where those names exist. That region of code where a name is recognized is called its scope. Understanding scope is crucial because it determines which variables are visible to different parts of your program.

Local and Global Scope

Variables defined inside a function exist only within that function and are said to have local scope. Variables created outside any function exist globally and can be accessed by any part of the program.

x = "Hello, World"
def func():
  x = 2
  print(f"Inside func, x = {x}")

func()
print(f"Outside func, x = {x}")

Output:

Inside func, x = 2
Outside func, x = Hello, World

The two variables named x do not interfere with each other because they exist in different scopes.

The LEGB Rule

Python resolves names using the **LEGB rule**: Local → Enclosing → Global → Built‑in.

x = 5

def outer_func():
  y = 3
  def inner_func():
    z = x + y
    print("Result:", z)
  inner_func()

outer_func()

The variable x is in the global scope, y is in the enclosing scope of outer_func, and z is local to inner_func. Python searches for names from local up to built‑in as needed.

Using Global Variables Carefully

While you can read a global variable inside a function, you cannot modify it unless you declare it using the global keyword. For example:

total = 0

def add_to_total(n):
  global total
  total = total + n

add_to_total(5)
print(total)

Output:

5

The global keyword allows a function to modify a variable outside its own scope, but this practice is considered poor style and often leads to hard‑to‑trace bugs. Prefer returning values instead of relying on global mutations.

6.7 Debugging Functions and Loops

As your programs grow more complex, debugging becomes an essential skill. Functions and loops introduce their own types of potential errors that you need to learn to identify and fix.

Common Function Debugging Issues

Missing or faulty return statements: Functions that should return a value but return None instead.

def add(a, b): result = a + b # Forgot return! # Should be: return result

Wrong parameter handling: Functions called with wrong arguments or types.

def divide(a, b): return a / b # Error: divide(1, 0) - ZeroDivisionError # Error: divide("a", 2) - TypeError

Unreachable code: Code after return that never executes.

def check_number(n): if n > 0: return "positive" if n < 0: return "negative" return "zero" print("This never prints") # Unreachable

Debugging Loops

Infinite loops: The most common and dangerous loop bug. Always ensure the loop condition can become false.

count = 0 while count < 5: # Infinite if count doesn't increase print(count)

Off-by-one errors: Loops that run one iteration too many or too few.

for i in range(1, 6): # Runs 5 times: 1, 2, 3, 4, 5 print(i) # If we wanted 1 to 5 inclusive, this is correct # But if we wanted 0 to 4, we'd use range(5)

Using IDLE for Debugging

Print tracing: Add print statements to see what your code is doing.

def fibonacci(n): if n <= 1: return n # Debug prints a = fibonacci(n-1) b = fibonacci(n-2) result = a + b print(f"fib({n}) = fib({n-1}) + fib({n-2}) = {a} + {b} = {result}") return result

Interactive testing: Test function components in the interactive window.

>>> range(5) range(0, 5) >>> list(range(5)) [0, 1, 2, 3, 4] >>> len("hello") 5

Debugging Best Practices

6.8 Summary and Additional Resources

In this chapter, you learned how to create functions to organize and reuse code, how to control repetition with while and for loops, and how Python determines variable visibility with the LEGB scope resolution rule.

Review Exercises

  1. Modify your temperature converter so it loops until the user chooses to quit.
  2. Write a new function that calls invest() and returns the final amount instead of printing each year's balance.
  3. Experiment with creating variables in different scopes and trace how Python resolves them using print().

End of Chapter 6 — Functions and Loops

100%