Scope and lifetime
Predict which variable a name refers to, understand shadowing, and see when a variable is created and destroyed.
What you will learn
- Distinguish local, global and block scope
- Predict which variable a name resolves to when names are reused (shadowing)
- Explain a variable's lifetime and how it differs from its scope
- Describe in outline how a closure keeps a variable alive
On this page
In the last lesson a function used its own parameters and variables. But what if two functions both use a variable called total? Do they share it? Scope is the set of rules that decides which parts of a program can see a given name. Getting it right is what lets a thousand-line program reuse short names like i and result without chaos.
Scope: where a name is visible
A name is in scope in the region of code where it can be used.
- Local scope (function scope): names created inside a function exist only inside it. When the function ends they are gone.
- Global scope: names created at the top level of the file are visible everywhere in it, including inside functions (for reading, at least).
- Block scope: in some languages, a name created inside a pair of braces, such as an
ifbody or a loop body, is visible only within those braces.
When a name is used, the language looks for it in the innermost scope first, then moves outward one level at a time until it finds a match. If nothing matches, you get an error such as NameError, ReferenceError or “cannot find symbol”.
lookup of x inside show():
show() ---- has its own x? yes -> use it, stop | v no file ---- has an x? yes -> use it, stop | v no error: the name is not definedShadowing
If an inner scope creates a variable with the same name as one outside, the inner one shadows the outer: inside the function, the name means the local variable, and the outer one is unreachable by that name. Crucially, the outer variable is not changed. They are two different variables that happen to share a label.
Python
x = "global"
def show(): x = "local" # a new variable that shadows the outer x print("inside:", x)
show()print("outside:", x)JavaScript
let x = "global";
function show() { let x = "local"; // a new variable that shadows the outer x console.log("inside:", x);}
show();console.log("outside:", x);Java
public class ScopeBasic { static String x = "global";
static void show() { String x = "local"; // a new variable that shadows the field System.out.println("inside: " + x); }
public static void main(String[] args) { show(); System.out.println("outside: " + x); }}C
#include <stdio.h>
const char *x = "global";
void show(void) { const char *x = "local"; /* a new variable that shadows the global */ printf("inside: %s\n", x);}
int main(void) { show(); printf("outside: %s\n", x); return 0;}All four print:
inside: localoutside: global(Java has no free-standing global variables; the closest equivalent is a static field of the class, which is what x is here.)
Shadowing is legal but easy to misread. Prefer distinct names unless the reuse is deliberate.
Block scope: languages disagree
Python has only function scope. A variable created inside an if or a loop is still there afterwards. In JavaScript’s let, Java and C, the variable dies at the closing brace.
Python
if True: inner = 42print(inner)42Python has no block scope, only function scope, so inner is still there after the if.
JavaScript
if (true) { let inner = 42;}console.log(typeof inner);undefinedlet is confined to its block. Using typeof on a name that does not exist is the one safe way to ask about it; any other use would throw a ReferenceError.
Java
public class ScopeBlock { public static void main(String[] args) { if (true) { int inner = 42; System.out.println(inner); } // System.out.println(inner); // would not compile: inner is out of scope }}42The only legal use of inner is inside its block. The commented-out line shows what the compiler would reject.
C
#include <stdio.h>
int main(void) { if (1) { int inner = 42; printf("%d\n", inner); } /* printf("%d\n", inner); would not compile: inner is out of scope */ return 0;}42As in Java, inner exists only inside its braces, and the commented-out line would not compile.
JavaScript has a historical oddity: the older var keyword is function-scoped, not block-scoped, like Python. Modern code uses let and const, which are block-scoped.
Assigning to a global from inside a function
Reading a global from a function is fine. Assigning is where the languages differ. In JavaScript, Java and C, an assignment to a name that has no local declaration changes the global. In Python, an assignment inside a function creates a new local variable unless you say otherwise.
Python
counter = 0
def increment(): global counter # without this line, "counter += 1" raises UnboundLocalError counter += 1
increment()increment()print(counter)Without the global line, counter += 1 would fail, since Python sees that the function assigns to counter, decides it is local, and finds it has no value yet.
JavaScript
let counter = 0;
function increment() { counter += 1; // no local declaration, so this changes the global}
increment();increment();console.log(counter);Java
public class ScopeGlobal { static int counter = 0;
static void increment() { counter += 1; // no local declaration, so this changes the static field }
public static void main(String[] args) { increment(); increment(); System.out.println(counter); }}C
#include <stdio.h>
int counter = 0;
void increment(void) { counter += 1; /* no local declaration, so this changes the global */}
int main(void) { increment(); increment(); printf("%d\n", counter); return 0;}All four print:
2Even when a language allows it, modifying globals from inside functions makes code hard to follow, because any function might change them at any time. Prefer passing values in as arguments and returning results.
Lifetime: when a variable exists
Scope is about where in the source code a name can be used. Lifetime is about when, while the program runs, the variable’s storage exists.
Usually they line up. A local variable is created when the function is called (its frame is pushed on the call stack) and destroyed when the function returns (the frame is popped). Two calls to the same function get two separate sets of locals, which is also what makes recursion work.
They can come apart. A global variable lives for the whole program. And a variable can outlive its function’s execution if something still refers to it, as in the next section.
Closures (a first look)
A function defined inside another can use the outer function’s variables. When the inner function is returned or stored, it takes those variables with it. That bundle, a function plus the variables it captured, is a closure. Each program below makes two independent counters, calls the first three times and the second once.
Python
def make_counter(): count = 0
def next_value(): nonlocal count count += 1 return count
return next_value
a = make_counter()b = make_counter()print(a(), a(), a()) # 1 2 3print(b()) # 1: each counter has its own countJavaScript
function makeCounter() { let count = 0; return function () { count += 1; return count; };}
const a = makeCounter();const b = makeCounter();console.log(a(), a(), a()); // 1 2 3console.log(b()); // 1: each counter has its own countJava
import java.util.function.IntSupplier;
public class Closure { static IntSupplier makeCounter() { int[] count = {0}; // a one-slot array, because a lambda cannot change a plain local return () -> { count[0] += 1; return count[0]; }; }
public static void main(String[] args) { IntSupplier a = makeCounter(); IntSupplier b = makeCounter(); System.out.println(a.getAsInt() + " " + a.getAsInt() + " " + a.getAsInt()); System.out.println(b.getAsInt()); }}Java lambdas can capture only variables that never change after being set, so the changing count lives inside a small array that the lambda holds on to.
C
#include <stdio.h>
typedef struct { int count;} Counter;
int next_value(Counter *counter) { counter->count += 1; return counter->count;}
int main(void) { Counter a = {0}; Counter b = {0}; int first = next_value(&a); int second = next_value(&a); int third = next_value(&a); printf("%d %d %d\n", first, second, third); printf("%d\n", next_value(&b)); return 0;}C has no closures. The same effect is achieved by keeping the state in a struct and passing it to the function explicitly every time. (The calls are made in separate statements because C does not promise in what order the arguments of one printf are evaluated.)
All four print:
1 2 31In the languages with closures, count is local to make_counter, and make_counter has long since returned, yet a still increments it. The captured variable lives as long as some function can still reach it, so scope and lifetime are not the same thing. Each call to make_counter creates a fresh count, which is why b starts from 1. (Python’s nonlocal plays the role for the enclosing function that global plays for the top level.)
Common pitfalls
- Assuming an assignment inside a function changes the outer variable. In Python it creates a new local one (shadowing).
- Using a variable outside the block that created it, in a language with block scope.
- Using a variable before it is set. Python raises
UnboundLocalError; C silently reads garbage from uninitialised locals. - Reusing a loop variable name like
iin a nested loop and clobbering the outer one. - Relying on globals for communication between functions.
- Assuming a local still exists after the call. Returning a pointer to a local in C is a classic bug, since the frame is gone.
Predict what each of these six puzzles prints in your language, then run the file to check. Puzzle 4 and puzzle 5 try to use a name outside the scope that created it, so the languages react differently, and in Java and C those lines would not compile and are commented out. Puzzle 6 needs nested functions, which C does not have. Expected: your prediction for every puzzle that prints a line.
Python
# Puzzle 1x = 1
def f1(): x = 2
f1()print(x)
# Puzzle 2y = 1
def f2(): print(y)
f2()
# Puzzle 3z = 1
def f3(): global z z = 2
f3()print(z)
# Puzzle 4def f4(): secret = 5
f4()try: print(secret)except NameError: print("NameError")
# Puzzle 5for i in range(3): passprint(i)
# Puzzle 6name = "a"
def outer(): name = "b"
def inner(): print(name)
inner()
outer()JavaScript
// Puzzle 1let x = 1;function f1() { let x = 2;}f1();console.log(x);
// Puzzle 2let y = 1;function f2() { console.log(y);}f2();
// Puzzle 3let z = 1;function f3() { z = 2;}f3();console.log(z);
// Puzzle 4function f4() { let secret = 5;}f4();try { console.log(secret);} catch (error) { console.log(error.name);}
// Puzzle 5for (let i = 0; i < 3; i++) {}console.log(typeof i);
// Puzzle 6let name = "a";function outer() { let name = "b"; function inner() { console.log(name); } inner();}outer();Java
public class ScopePuzzles { static int x = 1; static int y = 1; static int z = 1; static String name = "a";
static void f1() { int x = 2; }
static void f2() { System.out.println(y); }
static void f3() { z = 2; }
static void f4() { int secret = 5; }
static void outer() { String name = "b"; Runnable inner = () -> System.out.println(name); inner.run(); }
public static void main(String[] args) { // Puzzle 1 f1(); System.out.println(x);
// Puzzle 2 f2();
// Puzzle 3 f3(); System.out.println(z);
// Puzzle 4 f4(); // System.out.println(secret); // would not compile
// Puzzle 5 for (int i = 0; i < 3; i++) {} // System.out.println(i); // would not compile
// Puzzle 6 outer(); }}C
#include <stdio.h>
int x = 1;int y = 1;int z = 1;
void f1(void) { int x = 2; (void)x;}
void f2(void) { printf("%d\n", y);}
void f3(void) { z = 2;}
void f4(void) { int secret = 5; (void)secret;}
int main(void) { /* Puzzle 1 */ f1(); printf("%d\n", x);
/* Puzzle 2 */ f2();
/* Puzzle 3 */ f3(); printf("%d\n", z);
/* Puzzle 4 */ f4(); /* printf("%d\n", secret); would not compile */
/* Puzzle 5 */ for (int i = 0; i < 3; i++) {} /* printf("%d\n", i); would not compile */
/* Puzzle 6: C has no nested functions, so there is no puzzle here. */ return 0;}Show solution
Each language prints a different list, because of the scope rules above.
| Puzzle | Python | JavaScript | Java | C |
|---|---|---|---|---|
| 1 | 1 |
1 |
1 |
1 |
| 2 | 1 |
1 |
1 |
1 |
| 3 | 2 |
2 |
2 |
2 |
| 4 | NameError |
ReferenceError |
does not compile | does not compile |
| 5 | 2 |
undefined |
does not compile | does not compile |
| 6 | b |
b |
b |
not applicable |
- The function assigns to
x, which creates a local (in Python, JavaScript, Java and C alike). The global is untouched, soxprints 1. - The function only reads
y. There is no localy, so the lookup moves out to the global and finds 1. - In Python the function must declare
zas global; in the other languages plain assignment already reaches the global. Either wayzbecomes 2. secretexists only whilef4runs. Outside it, the name is unknown: Python and JavaScript fail at run time, and Java and C refuse to compile.- Python has no block scope: after the loop,
istill exists and holds its last value, 2. In JavaScriptletconfinesito the loop, sotypeofreportsundefined. Java and C reject the use. innerlooks upname, finds none locally, and checks the enclosing function next, where it finds"b", before it would reach the global"a".
Output by language:
Python: 1 1 2 NameError 2 b (one per line)JavaScript: 1 1 2 ReferenceError undefined bJava: 1 1 2 bC: 1 1 2Each program runs an empty loop three times and then asks about the loop variable i. What does each print, and how would the JavaScript result change if let were replaced by var? Expected: one line per program that prints.
Python
for i in range(3): passprint(i)JavaScript
for (let i = 0; i < 3; i++) {}console.log(typeof i);Java
public class LoopVar { public static void main(String[] args) { for (int i = 0; i < 3; i++) {} // System.out.println(i); // does this compile? }}C
#include <stdio.h>
int main(void) { for (int i = 0; i < 3; i++) {} /* printf("%d\n", i); does this compile? */ return 0;}Show solution
Python prints 2, because the loop variable belongs to the whole function and keeps its last value. JavaScript prints undefined, because let confines i to the loop. With var instead, the variable belongs to the whole enclosing function, so it would print number. In Java and C, removing the comment makes the program fail to compile, because i ends at the closing brace of the loop.
Write a counter that starts at 0 and goes up by 1 each time it is used, and make two independent counters. Use the first counter twice and the second once, and print each result on its own line. Expected output: 1, 2, 1. Use whichever tool your language gives you.
Show solution
The closure example above is the model: each counter needs its own hidden state. These versions print each value on its own line.
Python
def make_counter(): count = 0
def next_value(): nonlocal count count += 1 return count
return next_value
first = make_counter()second = make_counter()print(first())print(first())print(second())JavaScript
function makeCounter() { let count = 0; return () => { count += 1; return count; };}
const first = makeCounter();const second = makeCounter();console.log(first());console.log(first());console.log(second());Java
import java.util.function.IntSupplier;
public class TwoCounters { static IntSupplier makeCounter() { int[] count = {0}; return () -> ++count[0]; }
public static void main(String[] args) { IntSupplier first = makeCounter(); IntSupplier second = makeCounter(); System.out.println(first.getAsInt()); System.out.println(first.getAsInt()); System.out.println(second.getAsInt()); }}C
#include <stdio.h>
typedef struct { int count;} Counter;
static int next_value(Counter *counter) { counter->count += 1; return counter->count;}
int main(void) { Counter first = {0}; Counter second = {0}; printf("%d\n", next_value(&first)); printf("%d\n", next_value(&first)); printf("%d\n", next_value(&second)); return 0;}All four print:
121Summary
- Scope decides where a name can be used: local to a function, global to the file, or (in most languages) local to a block.
- A name is looked up from the innermost scope outward; an inner variable with the same name shadows the outer one without changing it.
- Lifetime is about when the variable exists; usually it ends with its scope, but a closure can keep a variable alive longer.
- Keep scopes small, pass data through parameters and return values, and avoid assigning to globals.
Next: Recursion, where a function calls itself.