Where variables live, how long they last, and why functions that communicate through parameters and return values are easier to trust than functions that reach into hidden shared state
When you write a function, the variables inside it do not automatically connect to variables outside it. Python keeps them separate on purpose. This separation is called scope, and understanding it will save you hours of confusion and debugging time.
This chapter explains how scope works, why it exists, what happens to variables when a function finishes running, what global variables are and why you should use them carefully, and how to design functions that communicate clearly through parameters and return values rather than through hidden shared variables.
Two different functions in the same program both use a variable named total. Does changing total inside one function change the total inside the other? Does it change a total that exists in the main program? The answer depends entirely on scope — and getting the answer wrong is one of the most common sources of confusing bugs for new programmers. By the end of this chapter, you will be able to answer this question correctly every time, and you will know how to design functions that avoid the confusion in the first place.
By the end of this chapter, you should be able to:
NameError and UnboundLocalError.global keyword; risks of overusing itImagine you are working at a large company. Every department has its own filing cabinet. A folder labeled "report" in the accounting department is a completely different folder from one labeled "report" in the marketing department. Neither department accidentally reads or changes the other's files. The same name in two places refers to two different things.
Python uses this same idea for variables inside functions. When you write a variable inside a function, Python treats it as belonging to that function. A variable named total inside one function is not the same variable as total inside a different function, and it is not the same as a total that might exist in the main body of your program.
This system is called scope. Scope answers the question: which part of the program can see and use this variable?
New programmers sometimes treat scope as a strange rule to memorize. But scope exists for a very practical reason: it prevents functions from accidentally interfering with each other.
Think about what would happen without scope. If every variable in every function were shared, then calling one function might accidentally overwrite a variable that another function was relying on. Tracking down that kind of bug — where two completely unrelated functions share a name by accident — would be a nightmare. Programs would become fragile and unpredictable.
Scope gives every function a private working space. You can use any variable name you want inside your function without worrying about whether you are accidentally changing something in another function or in the main program.
Read this program carefully:
When you run this program, Python prints the greeting. But then the second print(message) causes an error:
The variable message was created inside greet_user. It belongs to that function. Once the function finishes, message is gone. The main program cannot see it at all.
This is scope working exactly as intended.
An error like this is not a sign that something is broken. It is Python correctly enforcing a boundary. Learning to read this kind of error and recognize it instantly as a scope issue — rather than a mystery — is one of the most useful skills you will build in this chapter.
A local variable is a variable that is created inside a function. It belongs to that function and only exists while that function is running.
Here is how to recognize a local variable: if you see an assignment statement inside a function definition — that is, a line with = that creates a new name — that variable is local to that function.
In this function, discount_amount and final_price are both local variables. They are created when calculate_discount is called and they cease to exist when the function returns.
The parameters price and rate are also local. Parameters behave exactly like local variables — they are created when the function call happens and they disappear when the function ends. You will read more about this in Section 13.4.
You cannot access a local variable from outside the function that owns it. Attempting to do so raises a NameError.
The variable greeting is local to make_greeting. After the function returns, it does not exist anywhere you can reach.
Because local variables belong to their own functions, two different functions can use the same variable name without any conflict. They are completely separate variables.
Both functions use a variable named result. These are two completely different variables. Neither function knows or cares that the other one also uses the name result. Each result lives in its own private space.
This is one of the great advantages of functions and scope: you can name your variables clearly and naturally without constantly worrying about name collisions.
A variable's lifetime is the period during which it exists in memory. For local variables, the lifetime is short and well-defined:
Each time you call a function, its local variables are created fresh. They do not carry values from a previous call.
Every time count_up is called, a fresh counter variable is created, set to 0, incremented to 1, and printed. The counter does not accumulate across calls because the variable is destroyed when the function returns.
If you want a value to persist between function calls, you need a different approach — such as passing values through parameters and return values, or (carefully) using a global variable. You will learn about global variables in Section 13.5.
It helps to trace lifetime explicitly. Consider this program:
Here is what happens step by step:
| Step | Event | Variables That Exist |
|---|---|---|
| 1 | Program starts | (none yet) |
| 2 | square(4) is called | n = 4 created |
| 3 | result = 4 * 4 executes | n = 4, result = 16 |
| 4 | return result executes; function ends | n and result are destroyed |
| 5 | x = 16 in the main program | x = 16 |
| 6 | square(7) is called | n = 7 created (new copy) |
| 7 | result = 7 * 7 executes | n = 7, result = 49 |
| 8 | return result executes; function ends | n and result are destroyed |
| 9 | y = 49 in the main program | x = 16, y = 49 |
| 10 | print(x, y) executes | prints 16 49 |
Notice that n and result are created twice — once for each call — and destroyed twice. They never accumulate. The main program's variables x and y persist because they live outside any function.
This section makes a point that is easy to overlook: parameters behave exactly like local variables.
When you call a function, Python creates local variables for each parameter and assigns them the values you passed as arguments. From that moment on, the parameter is just a local variable with an initial value.
When double(x) is called:
number and sets it to 5 (the value of x).number = number * 2 changes the local number to 10.10.number is destroyed.x in the main program was never touched. It is still 5.Assigning a new value to a parameter inside a function does not change the original variable that was passed in. The parameter is a local copy.
Read this program and predict the output before continuing.
The function prints 90 because it added 10 to its local copy of value. But score in the main program never changed — it is still 80. The assignment inside the function affected only the local variable value, not the variable score that was passed in.
A global variable is a variable that is created in the main body of a program — outside of any function. It exists for the entire lifetime of the program.
Here program_name is created outside any function, so it is global. The function show_title can read it.
By default, a function can read a global variable. Reading means looking up its value without changing it.
The function calculate_total reads tax_rate from the global space. This works without any special instruction.
If a function tries to assign a new value to a global variable without special permission, Python creates a new local variable with that name instead. This is a very common source of confusion.
Running this produces:
Python saw the assignment count = count + 1 inside the function and decided count must be a local variable. But then it tried to read count on the right side of = before any local count had been given a value. That is a contradiction, and Python raises an error.
To allow a function to modify a global variable, you use the global keyword:
The line global count tells Python: "When I use the name count in this function, I mean the global variable count, not a new local one."
The global keyword works, but it should be used with caution. Here is why.
When a function modifies a global variable, it creates a hidden connection between the function and the rest of the program. This hidden connection is called a side effect — you will read about side effects in Section 13.7.
Hidden connections make programs harder to understand and debug. If something goes wrong with count, you have to check every function that uses global count to find the problem. As programs grow, this becomes very difficult.
The better design is almost always to pass values in through parameters and receive values back through return values. Use global variables for configuration values or constants that never change — not for values that functions modify.
This version is clearer. The function does not reach into global space. You can read count = increment(count) and immediately understand that count is being updated.
Writing global count inside every function as a habit, just to avoid an error. The presence of many global declarations scattered across a program is usually a sign that the program's design should be reconsidered — not a problem to be patched with more global statements.
Program state is the collection of all variable values at a specific moment during execution. Every variable that exists, everywhere in the program, contributes to the program's state.
Understanding state is important because bugs are almost always a matter of state. When your program produces the wrong output, it is because some variable somewhere has the wrong value. Debugging means finding which variable has the wrong value and figuring out how it got there.
State is not fixed. It changes every time an assignment statement executes.
The state of balance changes three times: once when it is created, once after deposit, and once after withdraw. At any moment during execution, the state of the program includes the current value of balance plus anything else that exists.
Local variables contribute to the local state of the currently running function. Once the function returns, that local state disappears. Global variables contribute to the global state, which persists across all function calls.
When you have a bug, it helps to ask: "Which piece of state is wrong? Is it a local variable inside a function? Is it a global variable? Is it a value that was passed through a parameter?"
When tracking down a bug, sketch the program's state at each step on paper — a quick table of variable names and values is often faster than re-reading the code from the top. This habit, introduced fully in Chapter 10, becomes even more valuable once functions and scope are involved.
A side effect is any change that a function makes to something outside its own local scope. If a function changes a global variable, prints output, writes to a file, or modifies a list that was passed in, those are side effects.
Side effects are not always bad. Printing output is a side effect, and printing output is often exactly what you want. The important thing is to know when your functions have side effects and to design them thoughtfully.
The function record has a side effect: it modifies the global list log. This is a reasonable design for a logging system, but it is worth knowing that the function is doing more than just computing a value — it is changing shared state.
The danger of side effects is that they create invisible connections between different parts of your program. A function that only takes parameters and returns values is easy to test: call it with specific inputs and check the output. A function that also modifies global state is harder to test because you also need to check what it did to the global state.
Compare these two versions of the same idea:
Both produce the same result. But Version B is easier to understand, test, and reuse. The function add_score in Version B has no side effects — you give it two values and it gives you one value back. You can test it independently, use it in different programs, and reason about it without thinking about global state.
Printing is a side effect, and it is common and intentional. Functions like greet_user() or display_menu() exist specifically to produce output. That is their job, and producing output is a perfectly reasonable side effect.
Side effects on global data structures like lists and dictionaries can also be reasonable in carefully controlled situations. You will learn more about this when you study lists and objects in later chapters.
If your function is computing a value, prefer returning that value over storing it in a global variable. Save global state for things that genuinely need to persist across many function calls and cannot easily be passed through parameters.
The clearest functions are those that follow a simple pattern:
A function like this is sometimes called a pure function — though you do not need to memorize that term right now. The key idea is that the function's behavior is predictable and testable.
Suppose you want to compute the letter grade for a numeric score.
This works, but get_grade depends on a global variable. If you want to test it with a different score, you have to change the global variable first. If another part of your program also uses a global named score, you might have a conflict.
Design B is better. The function clearly states what it needs (a score), clearly states what it produces (a grade letter), and does not reach into global space. You can call it with any score at any time without worrying about global state.
One acceptable use of global variables is for constants: values that are set once at the top of a program and never changed. Constants make sense as globals because they are not being modified — they are just shared reference values.
Here MAX_SCORE, PASSING_SCORE, and PROGRAM_NAME are global constants. The functions read them but do not modify them. This is a clean and readable pattern. By convention, Python programmers write constants in ALL_CAPS to signal that they should not be changed.
Receive information through parameters. Compute a result. Return that result. Reserve global variables for constants or for program-wide state that genuinely cannot be passed through parameters.
The best way to build a solid understanding of scope is to trace programs carefully, paying attention to which variables exist where and when.
When tracing a program that involves functions, maintain separate columns for the main program scope and for each function call scope. Create a function column when the function is called, and mark it as finished when the function returns.
Trace this program:
Main program scope:
| Variable | Value at Each Step |
|---|---|
a | 5 (from line a = 5) |
b | 11 (after compute(a) returns) |
c | 21 (after compute(10) returns) |
First call to compute(a) — i.e., compute(5):
| Variable | Created | Value | Destroyed |
|---|---|---|---|
x | when called | 5 | when function returns |
y | after y = x * 2 | 10 | when function returns |
z | after z = y + 1 | 11 | when function returns |
Returns: 11
Second call to compute(10):
| Variable | Created | Value | Destroyed |
|---|---|---|---|
x | when called | 10 | when function returns |
y | after y = x * 2 | 20 | when function returns |
z | after z = y + 1 | 21 | when function returns |
Returns: 21
Final output: 5 11 21
Notice that a stays 5 throughout. The function call compute(a) passes the value 5 — it does not give the function access to the variable a itself.
Trace this program:
Scope trace:
| Step | Event | total (global) | result1 | result2 | result3 |
|---|---|---|---|---|---|
| Start | total = 0 | 0 | — | — | — |
Call add(10) | amount = 10, total = 0 + 10 = 10 | 10 | — | — | — |
Return 10 | result1 = 10 | 10 | 10 | — | — |
Call add(25) | amount = 25, total = 10 + 25 = 35 | 35 | 10 | — | — |
Return 35 | result2 = 35 | 35 | 10 | 35 | — |
Call add(5) | amount = 5, total = 35 + 5 = 40 | 40 | 10 | 35 | — |
Return 40 | result3 = 40 | 40 | 10 | 35 | 40 |
Output: 40 10 35 40
Scope errors are some of the most confusing errors for new programmers because the error message often points to a line that looks correct. Understanding what Python is actually telling you will help you fix these bugs quickly.
What happened: username was created inside setup and belongs to that function. Once setup returns, username no longer exists.
Fix: Return the value and capture it in the main program.
What happened: Python saw the assignment points = ... inside earn_points and decided points must be a local variable. But then it tried to read points on the right side before any local points had been assigned a value.
Fix option A: Use the global keyword.
Fix option B (usually better): Pass the value through parameters and return values.
What happened: value is a local variable (a copy of score). Assigning 0 to value changes the local copy. The variable score in the caller is untouched.
Fix: Return the new value and assign it in the caller.
What happened: scale reads multiplier from global scope. When multiplier changed, scale started producing different results — even though the function itself was never modified. This makes the function unpredictable.
Fix: Pass the multiplier as a parameter.
Now the function's behavior depends only on its inputs, not on a hidden global.
What happened: There are two completely separate variables both named result. The one inside process is local and exists only during the function call. The one in the main program is in the global scope and is unaffected by the function. This is not a bug — it is scope working correctly — but it can be confusing if you are not expecting it.
When possible, avoid using the same variable name in the main program and inside functions for different things. Clear naming reduces confusion even when scope technically keeps them separate.
global keyword — and overusing global creates hidden, hard-to-debug connections.global, used inside a function to declare that a name refers to a global variable rather than a new local variable.For each program, explain in your own words what each line does and identify the scope of every variable before predicting the output.
Answer these questions before beginning the full review questions. They check basic understanding, not deep analysis.
global keyword do, and why must it be used carefully?= changes only that local copy. The caller's original variable is completely unaffected. (Note: this is different from modifying a mutable object like a list through its methods, which you will explore in later chapters — but simple reassignment, as covered here, never affects the caller.)global declaration is local to the function it appears in. If you want a second function to also modify the same global variable, that function needs its own global statement. Each function's scope rules are independent.MAX_SCORE or PROGRAM_NAME. The caution in this chapter is specifically about global variables that functions modify, because that creates hidden dependencies that are hard to track. Reading a global constant is not a problem at all.NameError happens when a name does not exist anywhere in the current scope — for example, trying to use a local variable from a function that already returned. An UnboundLocalError happens specifically when Python has already decided a name is local (because it is assigned somewhere in the function) but the code tries to read that name before any local assignment has actually happened. The fixes are related but the causes are different, and recognizing which one you are looking at speeds up debugging.For each program, trace the program's execution. Create a table showing variable values in each scope — main program and each function call. State the final output.
balance at each step.
Answer these questions based on careful reading. Do not run the programs — reason through them first, then verify.
count in the main program does not change.
get_discount does not use a global variable. Use a parameter instead.
temp. Will they interfere with each other? Explain why or why not.
For each function, identify whether it has side effects. If it does, describe what the side effect is. Then suggest an improved version without the side effect, if improvement is appropriate.
For each function above, answer: Does it have a side effect? If yes, what is it? Is the side effect intentional and reasonable, or should the function be redesigned?
Each program below contains a scope-related bug. Identify the error, explain what caused it, and write a corrected version.
celsius_to_fahrenheit(celsius) that takes a Celsius temperature as a parameter and returns the Fahrenheit equivalent. Do not use any global variables. Call it with at least three different values and print the results.apply_raise(salary, percent) that returns a new salary increased by the given percent. The function should not modify any variable in the caller — it should only return the new value. Demonstrate that the original salary variable in the main program is unchanged after the call.area_of_circle(radius) and area_of_square(side), that each use a local variable named area. Show that using the same local variable name in both functions causes no conflict by calling both and printing their results.STORE_NAME and a function print_receipt_header() that reads the constant and prints a formatted header. The function should not modify the constant.global keyword do? When is it needed?global keyword is required.Chapter 13 explained where variables live and how functions keep their working data separate from the rest of the program. You now understand local variables, global variables, lifetime, program state, and side effects. You have seen why functions that communicate through parameters and return values are clearer than functions that reach into global state.
Chapter 14 takes the next step: how do you break a large program into well-designed functions? As programs grow beyond a dozen lines, the real challenge is not syntax — it is organization. Chapter 14 introduces decomposition, helper functions, single responsibility, top-down design, and the practice of refactoring repeated code into reusable pieces. You will move from writing functions one at a time to designing programs with structure.