Organizing Code with Functions

Local and Global Scope


Learning Objectives

  • You can identify which variables are local to a function and which are global.
  • You can explain what happens when a function tries to access a variable that does not exist locally.
  • You can pass data into a function via parameters and get data back via return values.
  • You can describe why pure functions are easier to read, test, and prompt for.

You have been writing functions for a few chapters now. We’ve briefly noted that variables created inside a function do not seem to exist outside it. This is one of the most important concepts in Python.

The official name for the topic is scope. The practical question it answers is: “which variables can be seen from where, and which cannot?”

The Two Namespaces

When Python runs your program, it keeps track of variables in named collections called namespaces. For now, two namespaces matter:

  • The global namespace holds variables created at the top level of your program — the ones outside any function.
  • A local namespace is created fresh every time a function is called, and it holds variables that exist only during that call. When the call ends, the local namespace is thrown away.

Variables defined in a function live in that function’s local namespace. They are not visible outside it. Variables defined outside any function live in the global namespace.

def double(n):
result = n * 2
return result
print(double(5))
print(result) # NameError: name 'result' is not defined

The first print is fine — it prints 10. The second print crashes. The variable result was created inside double. When double returned, the local namespace of that call was thrown away, and result along with it. By the time the second print runs, there is no such variable anywhere.

Each function gets a clean slate. Names you used inside one function do not pollute the names you use outside it, or in other functions.

Trace local visibility

0 / 5 points

Arrange what happens when this program runs. Some steps are omitted.

def make_total():
total = 12
return total
print(make_total())
print(total)

Parameters Are Local Variables

When you call double(5), the parameter n is also a local variable inside the function call. It is bound to the value 5 for the duration of that call, and it disappears when the call ends.

def double(n):
n = n * 2
return n
x = 5
print(double(x))
print(x)

This program prints 10 and then 5. The function rebinds its local parameter n, but the variable x outside the function is unaffected. The parameter n is a separate local name, not the caller’s variable x.

The useful mental model is that the function gets the value, not the caller’s variable name. Inside the function, the parameter is a new local name; rebinding it has no effect outside the function.

Parameters are local

0 / 5 points

What happens to a parameter variable when a function call ends?

Global Variables and Functions

A function can read a variable from the global namespace, as long as it does not also try to assign to a variable with the same name. This works:

threshold = 60
def is_long_session(minutes):
return minutes > threshold
print(is_long_session(45)) # False
print(is_long_session(90)) # True

The function is_long_session reads threshold from the global namespace because there is no local variable by that name.

This works, but it is rarely a good idea. The function’s behavior now depends on a variable that lives outside it, in a way that is not visible from the line that defines the function. If you change threshold somewhere far away in the program, the function’s behavior silently changes too.

In this course, we stay away from functions that depend on global variables.

A cleaner version makes the dependency explicit by passing it in as a parameter:

def is_long_session(minutes, threshold):
return minutes > threshold
print(is_long_session(45, 60)) # False
print(is_long_session(90, 60)) # True

Now everything the function depends on is in its parameter list. Its behavior is determined entirely by what you pass in.

Passing the threshold

0 / 5 points

Why is is_long_session(minutes, threshold) clearer than a function that reads a global threshold variable?

Scope Helps Avoid Name Collisions

Without scope, every function in your program would have to be careful that the names it uses internally do not collide with names used somewhere else. With many functions and many variables, that becomes impossible.

When a variable is being assigned in a function, Python treats it as a local variable, even if there is a global variable with the same name. The local variable is created when the function starts, and it goes away when the function ends.

For example, the following program prints 100, 5 and then 100 again. The function set_count_to_five creates a local variable count and sets it to 5. The global variable count is unaffected.

def set_count_to_five():
count = 5
return count
count = 100
print(count) # 100
print(set_count_to_five()) # 5
print(count) # 100

Scope gives every function call its own clean slate. The function can use total, count, result, i, or any other short variable name it likes, without worrying about clashing with code elsewhere. Once the call ends, those names go away. The next call gets its own clean slate.

Scope as a feature

0 / 5 points

Why is it useful that each function call has its own local namespace?

The Pure Function Rule of Thumb

A function that takes its inputs entirely through parameters and produces its output entirely through a return value — touching no global state, modifying nothing outside itself — is called a pure function.

Pure functions have three useful properties:

  • They are easy to read. Everything the function depends on is in its parameter list. Everything it produces is in its return value. You do not need to know anything about the rest of the program to understand it.
  • They are easy to test. You give them inputs, you check the outputs, the function is the same on every call. The tests from the previous chapter were all tests of pure functions.
  • They are easy to specify. Pure functions have a clean contract — these inputs go in, this output comes out. That contract is exactly what the chapter Specifying What You Want will teach you to write as a function specification, and what gives an AI assistant the precision it needs to produce reliable code.

Not every function can or should be pure. The study log’s get_session() function uses input(...), which is impure by its nature — the function does not just compute from its arguments; it reads from the user. The print_summary() function prints, which is also impure. These functions exist for good reasons, and you should not pretend otherwise.

But for the computational parts of a program — totalling, classifying, finding the maximum — purity is a reasonable default. If you find yourself reaching for a global variable inside a function, ask whether passing it in as a parameter would be cleaner. Most of the time it would.

Recognizing pure functions

0 / 5 points

Which description best matches a pure function?


A Subtle Case: Mutable Arguments

There is a wrinkle worth knowing about: when you pass a list (or any other mutable value) to a function, the function does not get an independent list to work with. It gets a reference to the same list. If the function modifies the list, the caller sees the modification.

def append_session(sessions, minutes):
sessions.append(minutes)
my_sessions = [45, 30]
append_session(my_sessions, 60)
print(my_sessions)

This prints [45, 30, 60]. The function modified the caller’s list directly. This is different from the number example earlier — there, rebinding the parameter did not affect x. Here, both names point to the same list.

This is occasionally useful and often confusing. As a rule, prefer functions that return new values over functions that mutate their arguments. A function called with_session_added(sessions, minutes) that returns a new list is clearer than append_session(sessions, minutes) that mutates the input — and the caller is then in charge of what to do with the result.

We will return to mutability in more depth later in this course.


Return a new list with a session added

0 / 10 points

Define a function named with_session_added(sessions, minutes).

The function must:

  • take a list sessions and a number minutes,
  • create a new list,
  • add the values from sessions to the new list,
  • append minutes to the new list,
  • return the new list,
  • leave the original sessions list unchanged,
  • and not print inside the function.

No user input is needed. The tests will load your program and call the function directly.

Example calls and return values:

old = [20, 35]
new = with_session_added(old, 45)
new -> [20, 35, 45]
old -> [20, 35]

Check Your Understanding

  1. What is the difference between a local variable and a global variable?
  2. After this program runs, what is the value of count?
count = 5
def update():
count = 100
update()
  1. Why is “everything through parameters and return values” a useful default rule for writing functions?
  2. What is a pure function? Give one example of a function that is pure, and one that cannot be pure.
  3. Suppose you write a function that takes a list and is supposed to leave it unchanged. What is one thing you should watch out for?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0