Lecture 4: Return Values and Scoping¶
Welcome back! Last time we learned how to define and call functions with parameters. Today we're going to learn about two important concepts:
- Return values - how functions can send results back to the caller
- Variable scope - which variables are visible where in your program
By the end of this lecture, you will be able to:
- write functions that return values
- use return values in expressions and assign them to variables
- understand the difference between local and global variables
- understand when variables are created and destroyed
And now you will be ready to tack the rest (Task 3 and 4) of HW1!
Review: Functions That Print vs. Functions That Return¶
So far, the functions we've written mostly use print() to display information. Let's look at an example from last lecture:
def print_stitch_count(num_stitches):
print(f"You need {num_stitches} stitches.")
print_stitch_count(100)
You need 100 stitches.
This function displays a message, but what if we want to use the result in a calculation? Let's try to save the result:
result = print_stitch_count(100)
print(f"The result is: {result}")
You need 100 stitches. The result is: None
Notice that result is None! This is because functions that don't have a return statement automatically return a special value called None.
This is where return values come in.
Side Note: You can think of None as Python's way of saying there is nothing, it's empty, etc. It actually can be assigned to a variable too, stitch_count = None then basically saying I'm just creating a variable named stitch_count without giving it a value, because maybe later I will perform some computation and then assign that result of the computation to it.
Writing Functions with Return Values¶
A return value is the result that a function sends back to the code that called it. We use the return statement to specify what value to send back.
Let's write a function that calculates stitches needed and returns the result instead of printing it:
def calculate_cast_on(head_circumference_inches, stitches_per_inch):
"""
Calculates the number of stitches to cast on for a beanie.
Parameters:
- head_circumference_inches: circumference of the head in inches
- stitches_per_inch: gauge (stitches per inch)
Returns:
- number of stitches to cast on (as an integer)
"""
total_stitches = head_circumference_inches * stitches_per_inch
return int(total_stitches)
Key points about this function:
- The last line
return int(total_stitches)sends the value back - We're returning an integer (since you can't cast on a fraction of a stitch!)
- The docstring says what the function returns
When we call this function in a cell, Jupyter automatically displays the return value:
calculate_cast_on(22, 4.5)
99
beanie_stitches = calculate_cast_on(22, 4.5)
print(f"Cast on {beanie_stitches} stitches for the beanie.")
Cast on 99 stitches for the beanie.
Using in Expressions¶
We can use the return value directly in calculations:
# Calculate total stitches for 2 beanies
total_for_two_beanies = 2 * calculate_cast_on(22, 4.5)
print(f"For two beanies, cast on {total_for_two_beanies} stitches total.")
For two beanies, cast on 198 stitches total.
Passing Return Values to Other Functions¶
We can pass the return value of one function as an argument to another function:
def estimate_yarn_yards(num_stitches):
"""
Estimates yarn needed in yards.
Assumes 10 stitches per yard of yarn.
Returns: yards of yarn needed (as a float)
"""
return num_stitches / 10
# Calculate stitches, then calculate yarn needed
stitches_needed = calculate_cast_on(22, 4.5)
yards_needed = estimate_yarn_yards(stitches_needed)
print(f"You need approximately {yards_needed:.1f} yards of yarn.")
You need approximately 9.9 yards of yarn.
We can even do this in a single line:
yards = estimate_yarn_yards(calculate_cast_on(22, 4.5))
print(f"You need approximately {yards:.1f} yards of yarn.")
You need approximately 9.9 yards of yarn.
Multiple Return Values¶
Python functions can return more than one value at once! When you return multiple values, Python automatically packages them into a tuple (a collection of values).
Here's an example:
def calculate_fabric_dimensions(width_stitches, length_rows, stitches_per_inch, rows_per_inch):
"""
Calculates the width and length of knitted fabric in inches.
Returns:
- width in inches (float)
- length in inches (float)
"""
width_inches = width_stitches / stitches_per_inch
length_inches = length_rows / rows_per_inch
return width_inches, length_inches
When a function returns multiple values, we can unpack them into separate variables:
width, length = calculate_fabric_dimensions(100, 150, 5, 6)
print(f"Your scarf will be {width:.1f} inches wide and {length:.1f} inches long.")
Your scarf will be 20.0 inches wide and 25.0 inches long.
Note: This is called tuple unpacking. The function returns (width_inches, length_inches) as a tuple, and we "unpack" it into two variables: width and length.
You'll use this pattern in HW1 when working with functions that return multiple values!
Return vs. Print: When to Use Each?¶
A common question: should I use return or print()?
Use return when:
- You want to use the result in further calculations
- You're building a reusable function that other code will call
- You want flexibility in how the result is used
Use print() when:
- You want to show information to the user
- You're debugging and want to see intermediate values
- The function's purpose is to display something, not compute it
Often, you'll write functions that return values, and then print those values where needed:
# Good practice: function returns the value, not dictating how it will be used/displayed
def calculate_repeats(total_stitches, stitches_per_repeat):
return total_stitches // stitches_per_repeat
# User decides to print the result when needed
repeats = calculate_repeats(96, 8)
print(f"You can fit {repeats} complete repeats in your pattern.")
You can fit 12 complete repeats in your pattern.
Variable Scope: Where Do Variables Live?¶
The next topic is about scope, the part of your program where a variable "exists" and can be used.
"Existence" of something in Python means that you have to tell Python what a name refers to (whether it's a variable name or a function name), before it can properly interpret it. Otherwise, that name is unrecognizable and therefore does not "exist".
# Issue: using a variable without defining it first
total_yardage = 3 * yard_per_cone
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[13], line 2 1 # Issue: using a variable without defining it first ----> 2 total_yardage = 3 * yard_per_cone NameError: name 'yard_per_cone' is not defined
# Issue: using a function without defining it first
result = nonexistent_function() * 1e-6
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[14], line 2 1 # Issue: using a function without defining it first ----> 2 result = nonexistent_function() * 1e-6 NameError: name 'nonexistent_function' is not defined
Once defined, variables or functions exist within its scope. What's the scope? It depends on where the entity is being defined.
Global Variables¶
Variables created outside of any function are called global variables. They can be accessed from anywhere in your code, including inside functions.
Here's an example:
# This is a global variable
tax_rate = 0.101
def calculate_with_tax(price):
"""
Calculates price including tax.
Uses the global tax_rate variable.
"""
# We can access global variables inside functions
total = price * (1 + tax_rate)
return total
print(f"Price with tax: ${calculate_with_tax(10):.2f}")
Price with tax: $11.01
While we can read global variables inside functions, it's often better practice to pass them as parameters. This makes your functions more flexible and easier to understand.
Compare these two approaches:
# Less flexible - relies on global variable
def calculate_with_tax_v1(price):
return price * (1 + tax_rate)
# More flexible - tax rate is a parameter
def calculate_with_tax_v2(price, tax_rate):
return price * (1 + tax_rate)
# Version 2 can easily handle different tax rates!
print(f"With 10.1% tax: ${calculate_with_tax_v2(10, 0.101):.2f}")
print(f"With 11.1% tax: ${calculate_with_tax_v2(10, 0.111):.2f}")
With 10.1% tax: $11.01 With 11.1% tax: $11.11
The ethos is to treat global variables as invariants, or constants, that you will never need to change. For example, a constant number used in a computation, a file path pointing to a database that all the code is going to use, a url string pointing to a permanent link of something. If that variable is not an invariant to this program, then pass it as parameter.
Local Variables¶
Variables created inside a function are called local variables. They only exist while the function is running, and they can't be accessed from outside the function.
Let's look at an example:
def calculate_total_cost(price_per_ball, num_balls):
"""
Calculates total cost for yarn purchase.
"""
total = price_per_ball * num_balls # 'total' is a local variable
tax = total * 0.101 # 'tax' is also a local variable
final_cost = total + tax # 'final_cost' is also a local variable
return final_cost
cost = calculate_total_cost(4.99, 5)
print(f"Total cost: ${cost:.2f}")
Total cost: $27.47
Now let's try to access total from outside the function:
# This will cause an error!
print(total)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[21], line 2 1 # This will cause an error! ----> 2 print(total) NameError: name 'total' is not defined
You'll get a NameError because total only exists inside the function. When the function finishes running, total, tax, and final_cost all disappear!
Parameters are also local variables! They only exist inside the function.
# This will cause an error
print(num_balls)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[22], line 2 1 # This will cause an error ----> 2 print(num_balls) NameError: name 'num_balls' is not defined
Visualizing Scope with Python Tutor¶
To really understand scope, let's trace through a more complex example step-by-step. Understanding how variables change as code executes is a crucial skill in computer programming.
We'll practice tracing code execution:
- Copy the code into Python Tutor
- Click "Visualize Execution"
- Step through the code using the "Next" button
- Observe how variables appear and disappear
Let's look at a function that calculates the number of rows needed for a two-color striped scarf:
def calculate_stripe_rows(stripe_height, num_stripes):
"""
Takes in the number of rows in a stripe (stripe_height) and the number of stripes
to compute the total number of rows.
"""
return stripe_height * num_stripes
def plan_striped_scarf(total_rows, stripe_height):
"""
Takes in the total number rows in a color-striped scarf and the height for each stripe
and compute the number of rows needed to be knitted for the two colors.
"""
num_stripes = round(total_rows / stripe_height)
color_a_rows = calculate_stripe_rows(stripe_height, num_stripes // 2)
color_b_rows = total_rows - color_a_rows
return color_a_rows, color_b_rows
col_a_needed, col_b_needed = plan_striped_scarf(100, 6)
print(f"We need to knit {col_a_needed} rows of color A and {col_b_needed} rows of color B in total.")
We need to knit 48 rows of color A and 52 rows of color B in total.
You would see something like this image after clicking "Visualize Execution". Use the "Next" button to step through the execution.

Tracing Table Example¶
Here's how we can record what happens at each step. Let's trace through the first 9 steps together:
| Step | Line # | Variable(s) Changed | New Value(s) | Variable(s) Disappeared | Notes |
|---|---|---|---|---|---|
| 1 | / | / | / | / | Step 1 is before execution starts |
| 2 | 1 | / | / | / | Function calculate_stripe_rows() defined |
| 3 | 8 | / | / | / | Function plan_striped_scarf() defined |
| 4 | 18 | total_rows, stripe_height | 100, 6 | / | Parameters receive inputs when function is called |
| 5 | 8 | / | / | / | Beginning execution of plan_striped_scarf() |
| 6 | 13 | num_stripes | 17 | / | Line 13 computes num_stripes |
| 7 | 14 | stripe_height, num_stripes | 6, 8 | / | Calling calculate_stripe_rows() with arguments |
| 8 | 1 | / | / | / | Beginning execution of calculate_stripe_rows() |
| 9 | 6 | Return Value | 48 | / | Function returns 48 |
Poll Time!¶
We just saw that calculate_stripe_rows() returned 48. What happens next at Step 10?
Think a bit for yourself, discuss with your neighbors, and send the answer through PollEverywhere.
Questions to think about:
- What do you know about local variables and function scope?
- What happens when a function finishes executing?
Let's see what actually happens...
| Step | Line # | Variable(s) Changed | New Value(s) | Variable(s) Disappeared | Notes |
|---|---|---|---|---|---|
| 10 | 14 | color_a_rows | 48 | stripe_height, num_stripes, Return Value | Variables from calculate_stripe_rows() disappear |
Key observation: When calculate_stripe_rows() returns, all of its local variables disappear:
- The parameters
stripe_heightandnum_stripesthat belonged tocalculate_stripe_rows()are gone - Only the return value (48) remains, and it gets assigned to
color_a_rows - The
total_rows,stripe_height, andnum_stripesfrom the outer functionplan_striped_scarf()still exist! Even though the variable names are repeated, because they are defined in a different scope, it is legal in Python and they are fully independent of each other.
Now let's finish off tracing the remaining steps:
| Step | Line # | Variable(s) Changed | New Value(s) | Variable(s) Disappeared | Notes |
|---|---|---|---|---|---|
| 11 | 15 | color_b_rows | 52 | / | Line 15 computes color_b_rows |
| 12 | 16 | Return Value | tuple(48, 52) | / | Function prepares to return multiple values |
| 13 | 18 | col_a_needed, col_b_needed | 48, 52 | total_rows, stripe_height, num_stripes, color_a_rows, color_b_rows, Return Value | All variables from plan_striped_scarf() disappear |
| end | 19 | / | / | / | Final output printed to screen |
That's the end of tracing using Python Tutor! In Task 3.2 of HW1, you will practice tracing again with your own code and answer some questions about functions and scopes.
def calculate_area(width, length):
area = width * length
# Oops! Forgot to return area
result = calculate_area(10, 20)
print(f"Area is: {result}") # This will print None!
Area is: None
Mistake 2: Code After Return (Dead Code)¶
Any code after a return statement will never run:
def calculate_gauge(stitches, inches):
gauge = stitches / inches
return gauge
print("This will never print!") # This is dead code
result = calculate_gauge(20, 4)
print(f"Gauge: {result}")
Gauge: 5.0
Mistake 3: Confusing Local and Global Variables¶
Be careful with variable names! If you use the same name inside and outside a function, they're actually different variables because their scopes are different:
stitches = 100 # Global variable
print(f"Global stitches: {stitches}")
def double_stitches(stitches): # Parameter (local variable with same name!)
stitches = stitches * 2 # This creates/modifies the LOCAL stitches
print(f"Local stitches: {stitches}") # The local stitches is modified
return stitches
result = double_stitches(10)
print(f"Result: {result}") # 100
print(f"Global stitches: {stitches}") # Still 100! Not changed by the function
Global stitches: 100 Local stitches: 20 Result: 20 Global stitches: 100
As a quick summary, today we learned about:
Return Values:
- Use
returnto send values back from a function - Return values can be assigned to variables, used in expressions, and passed to other functions
- Functions can return multiple values using tuples
- Functions without
returnstatements returnNone
Variable Scope:
- Local variables exist only inside the function where they're created
- Parameters are local variables
- Local variables disappear when the function ends
- Global variables exist outside functions and can be read from inside functions
- Pass values as parameters rather than relying on global variables
These concepts are crucial for HW1! Next lecture, we'll learn about conditionals and boolean logic, which will let us make our functions even more powerful. See you next week!