๐ญ Lesson 4.3: Scope and Closures
Every variable has a "lifespan" โ it exists somewhere, and it's invisible everywhere else. Understanding scope is key to avoiding mysterious bugs, and understanding closures unlocks a powerful pattern for creating private state and reusable logic.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between local scope and global scope
- Understand block scope and why
let/constbehave differently fromvarin JavaScript - Use Python's
globalandnonlocalkeywords correctly - Trace how the scope chain resolves variable lookups
- Define what a closure is and build practical examples
Estimated Time: 60 minutes
Project: Build a counter factory and a private configuration manager using closures
๐ In This Lesson
Local vs. Global Scope
Variables defined inside a function are local โ they only exist while that function runs. Variables defined outside all functions are global โ they're accessible everywhere in the file.
# Global variable โ accessible everywhere
greeting = "Hello"
def say_hello(name):
# Local variable โ only exists inside this function
message = f"{greeting}, {name}!"
print(message)
say_hello("Alice") # Hello, Alice!
print(greeting) # "Hello" โ global, still accessible
# print(message) # โ NameError โ message is local to say_hello
# Each call creates fresh local variables
def count_up():
counter = 0 # brand new each time
counter += 1
print(counter)
count_up() # 1
count_up() # 1 (not 2 โ counter is re-created each call)
// Global variable
let greeting = "Hello";
function sayHello(name) {
// Local variable
let message = `${greeting}, ${name}!`;
console.log(message);
}
sayHello("Alice"); // Hello, Alice!
console.log(greeting); // "Hello" โ global
// console.log(message); // โ ReferenceError โ message is local
// Each call creates fresh local variables
function countUp() {
let counter = 0;
counter += 1;
console.log(counter);
}
countUp(); // 1
countUp(); // 1 (counter is re-created each call)
// In C#, "global" typically means class-level (static fields)
class Program
{
// Class-level field โ accessible to all methods in this class
static string greeting = "Hello";
static void SayHello(string name)
{
// Local variable โ only exists in this method
string message = $"{greeting}, {name}!";
Console.WriteLine(message);
}
static void Main()
{
SayHello("Alice"); // Hello, Alice!
Console.WriteLine(greeting); // "Hello"
// Console.WriteLine(message); // โ Compile error โ not in scope
}
}
๐ก Think of Scope Like Rooms in a House
A global variable is like an item in the hallway โ everyone can see it. A local variable is like something inside a room with the door closed โ only the person in that room can use it. When the function ends, the "room" is cleaned out.
๐ Instructor Note: Delivery Guidance
Start by demonstrating the error when you try to access a local variable outside its function. That "NameError" or "ReferenceError" is the most tangible proof of scope. The "rooms in a house" analogy works well. Note that C# doesn't have true global variables โ its equivalent is static class fields. For beginners, just say "class-level variables work like globals for now" and defer the full explanation to Module 6 (OOP).
Block Scope
A block is any code between curly braces { } โ inside an if, for, while, etc. In some languages, variables created inside a block are trapped there. In others, they leak out.
# Python does NOT have block scope!
# Variables inside if/for/while are still part of the enclosing function
if True:
block_var = "I'm visible outside the if!"
print(block_var) # Works! "I'm visible outside the if!"
for i in range(3):
loop_var = i
print(loop_var) # Works! 2 (the last value)
# Python only creates new scope for:
# - Functions (def)
# - Classes (class)
# - Comprehensions (list/dict/set comprehensions, generator expressions)
# This means if/for/while DON'T create new scope โ be aware!
x = 10
if True:
x = 99 # This modifies the outer x, not a new local x!
print(x) # 99
// let and const ARE block-scoped
if (true) {
let blockLet = "I'm trapped in this block";
const blockConst = "Me too";
}
// console.log(blockLet); // โ ReferenceError
// console.log(blockConst); // โ ReferenceError
// var is NOT block-scoped โ it leaks out!
if (true) {
var leaked = "I escaped the block!";
}
console.log(leaked); // "I escaped the block!" โ var ignores blocks
// This is a major reason to prefer let/const over var
for (let i = 0; i < 3; i++) {
// i is scoped to this loop
}
// console.log(i); // โ ReferenceError โ i is gone
for (var j = 0; j < 3; j++) {
// j leaks out
}
console.log(j); // 3 โ j is still alive!
// C# has strict block scope โ variables stay in their block
if (true)
{
string blockVar = "I'm trapped here";
Console.WriteLine(blockVar); // Works inside the block
}
// Console.WriteLine(blockVar); // โ Compile error โ not in scope
for (int i = 0; i < 3; i++)
{
// i is scoped to this loop
}
// Console.WriteLine(i); // โ Compile error
// Each block is its own scope:
{
int x = 10;
Console.WriteLine(x); // 10
}
{
int x = 20; // Different x โ no conflict
Console.WriteLine(x); // 20
}
Block Scope Quick Reference
| Feature | ๐ Python | โก JavaScript | ๐ท C# |
|---|---|---|---|
| if/for/while create scope? | No | let/const: Yesvar: No |
Yes |
| Functions create scope? | Yes | Yes (all declaration types) | Yes |
| Common gotcha | Loop vars leak out | var ignores blocks |
None โ it's strict |
โ ๏ธ Python's Missing Block Scope
Python's lack of block scope catches many developers off guard, especially those coming from JavaScript or C#. A variable assigned inside an if or for is accessible after the block ends. This is by design, but it means you should be intentional about where you assign variables โ accidental overwrites are easy.
Modifying Outer Variables
Functions can read variables from outer scopes. But writing to them is trickier โ each language handles it differently.
count = 0
def increment():
# โ This creates a NEW local variable, not modifying the global
# count = count + 1 # UnboundLocalError!
pass
# To modify a global, you must declare it explicitly:
def increment_global():
global count
count += 1
increment_global()
print(count) # 1
# For nested functions, use 'nonlocal':
def outer():
score = 0
def add_points(points):
nonlocal score # Refers to outer()'s score
score += points
add_points(10)
add_points(5)
print(f"Score: {score}") # Score: 15
outer()
let count = 0;
function increment() {
// JavaScript lets you modify outer variables directly
count += 1;
}
increment();
console.log(count); // 1
// Nested functions can also modify enclosing variables
function outer() {
let score = 0;
function addPoints(points) {
score += points; // Directly modifies outer's score
}
addPoints(10);
addPoints(5);
console.log(`Score: ${score}`); // Score: 15
}
outer();
// โ ๏ธ If you accidentally use let inside, you create a NEW variable:
let name = "Alice";
function changeName() {
let name = "Bob"; // New local โ doesn't touch outer
console.log(name); // "Bob"
}
changeName();
console.log(name); // "Alice" โ unchanged
// C# uses class fields for shared state between methods
class Program
{
static int count = 0;
static void Increment()
{
count += 1; // Modifies the class field directly
}
static void Main()
{
Increment();
Console.WriteLine(count); // 1
}
}
// C# also supports closures with local functions and lambdas:
static void ScoringExample()
{
int score = 0;
// Local function captures 'score'
void AddPoints(int points)
{
score += points;
}
AddPoints(10);
AddPoints(5);
Console.WriteLine($"Score: {score}"); // Score: 15
}
// With lambdas:
static void LambdaCapture()
{
int score = 0;
Action<int> addPoints = (points) => score += points;
addPoints(10);
addPoints(5);
Console.WriteLine($"Score: {score}"); // Score: 15
}
๐ก Summary of Rules
Python: Reading outer variables is automatic, but writing requires global (for module-level) or nonlocal (for enclosing function).
JavaScript: Reading and writing outer variables both work automatically โ just don't accidentally shadow with let.
C#: Class fields are shared across methods. Local functions and lambdas can capture and modify enclosing variables.
๐ Instructor Note: Delivery Guidance
The Python global/nonlocal distinction is the trickiest part here. Show the UnboundLocalError first without the global keyword and let students see the error before providing the fix. Emphasize that while these keywords exist, overusing global variables is a bad habit โ the real solution is usually to pass values as arguments and return results. This section sets up closures, which are the "right" way to manage shared state.
The Scope Chain
When your code references a variable, the language looks for it in a specific order โ from the innermost scope outward. This lookup path is called the scope chain.
(current function)"] B -->|Not found| C["2. Enclosing Scope
(outer function, if nested)"] C -->|Not found| D["3. Global / Module Scope
(top level of file)"] D -->|Not found| E["4. Built-in Scope
(language built-ins)"] E -->|Not found| F["โ Error!
NameError / ReferenceError"]
# Python's lookup: LEGB (Local โ Enclosing โ Global โ Built-in)
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local" โ found in local scope first
inner()
print(x) # "enclosing" โ inner's x was its own
outer()
print(x) # "global" โ untouched by outer/inner
# Remove local x from inner to see the chain in action:
def outer2():
x = "enclosing"
def inner2():
# No local x โ Python looks up the chain
print(x) # "enclosing" โ found in enclosing scope
inner2()
outer2()
// JavaScript follows the same inside-out chain
let x = "global";
function outer() {
let x = "enclosing";
function inner() {
let x = "local";
console.log(x); // "local"
}
inner();
console.log(x); // "enclosing"
}
outer();
console.log(x); // "global"
// Remove local x from inner:
function outer2() {
let x = "enclosing";
function inner2() {
console.log(x); // "enclosing" โ walks up the chain
}
inner2();
}
outer2();
// C# scope chain: inner block โ outer block โ method โ class
class Program
{
static string x = "class-level";
static void Outer()
{
string x = "method-level";
void Inner()
{
// string x = "local"; // If uncommented, this would be used
Console.WriteLine(x); // "method-level" โ captured from Outer
}
Inner();
Console.WriteLine(x); // "method-level"
}
static void Main()
{
Outer();
Console.WriteLine(x); // "class-level"
}
}
โ ๏ธ Shadowing
When an inner scope creates a variable with the same name as an outer one, the inner variable "shadows" the outer. The outer variable still exists โ it's just hidden. This is a common source of subtle bugs. Tip: use distinct variable names across scopes to avoid confusion.
Hoisting (JavaScript)
JavaScript has a quirk called hoisting: declarations are "moved" to the top of their scope before code runs. This only applies to JavaScript โ Python and C# don't do this.
# Python: no hoisting โ code runs top to bottom
# Using a variable before it's defined is always an error
# print(name) # โ NameError: name 'name' is not defined
name = "Alice"
print(name) # โ
"Alice"
# Functions must be defined before they're called
# say_hello() # โ NameError
def say_hello():
print("Hello!")
say_hello() # โ
"Hello!"
# Python is straightforward: define it first, then use it
// var is hoisted (declaration, not value)
console.log(name); // undefined (not an error!)
var name = "Alice";
console.log(name); // "Alice"
// What JavaScript actually does:
// var name; โ declaration hoisted to top
// console.log(name); โ undefined (exists but no value yet)
// name = "Alice"; โ assignment stays in place
// let and const are hoisted but NOT accessible before declaration
// console.log(age); // โ ReferenceError: Cannot access before init
let age = 25;
// Function declarations ARE fully hoisted (name + body)
sayHello(); // โ
Works! "Hello!"
function sayHello() {
console.log("Hello!");
}
// Function expressions are NOT fully hoisted
// greet(); // โ TypeError: greet is not a function
var greet = function() {
console.log("Hi!");
};
greet(); // โ
Now it works
// C#: no hoisting โ variables must be declared before use
// Console.WriteLine(name); // โ Compile error
string name = "Alice";
Console.WriteLine(name); // โ
"Alice"
// However, method ORDER in a class doesn't matter:
class Program
{
static void Main()
{
SayHello(); // โ
Works even though SayHello is defined below
}
static void SayHello()
{
Console.WriteLine("Hello!");
}
}
// C# compiles the whole class at once, so method order is flexible.
// But local variables inside a method must be declared before use.
โ ๏ธ One More Reason to Avoid var in JavaScript
Hoisting with var can cause confusing bugs โ a variable exists but is undefined before its assignment line. With let and const, you get a clear error if you try to use a variable too early. This is called the Temporal Dead Zone (TDZ) and it's a feature, not a bug โ it catches mistakes.
๐ Instructor Note: Delivery Guidance
Hoisting is JavaScript-specific, so spend most of the time on the JS tab. The key takeaway isn't memorizing hoisting rules โ it's "always use let/const and declare variables before you use them." If students ask "why does var exist then?" โ explain it's a legacy feature from JavaScript's early days before block scope existed. Modern JS code should use let/const exclusively.
Closures
A closure is a function that "remembers" the variables from the scope where it was created, even after that scope has finished executing. It's one of the most powerful concepts in programming.
# A closure: inner function "remembers" outer function's variables
def make_greeter(greeting):
# This variable belongs to make_greeter's scope
def greet(name):
# But greet() can still access it after make_greeter returns!
return f"{greeting}, {name}!"
return greet # Return the inner function itself
# Create specialized greeters
hello = make_greeter("Hello")
howdy = make_greeter("Howdy")
# The inner function remembers its greeting
print(hello("Alice")) # Hello, Alice!
print(howdy("Bob")) # Howdy, Bob!
print(hello("Charlie")) # Hello, Charlie!
# make_greeter has already returned, but the greeting
# variable lives on inside each closure!
// A closure: inner function "remembers" outer function's variables
function makeGreeter(greeting) {
// This variable belongs to makeGreeter's scope
return function(name) {
// But the returned function still has access!
return `${greeting}, ${name}!`;
};
}
// Create specialized greeters
const hello = makeGreeter("Hello");
const howdy = makeGreeter("Howdy");
// Each function remembers its own greeting
console.log(hello("Alice")); // Hello, Alice!
console.log(howdy("Bob")); // Howdy, Bob!
console.log(hello("Charlie")); // Hello, Charlie!
// Arrow function version (same concept):
const makeGreeter2 = (greeting) => (name) => `${greeting}, ${name}!`;
// C# closures use lambdas or local functions
static Func<string, string> MakeGreeter(string greeting)
{
// The lambda captures 'greeting' from this scope
return (name) => $"{greeting}, {name}!";
}
// Create specialized greeters
var hello = MakeGreeter("Hello");
var howdy = MakeGreeter("Howdy");
Console.WriteLine(hello("Alice")); // Hello, Alice!
Console.WriteLine(howdy("Bob")); // Howdy, Bob!
Console.WriteLine(hello("Charlie")); // Hello, Charlie!
// Local function version:
static Func<string, string> MakeGreeter2(string greeting)
{
string Greet(string name)
{
return $"{greeting}, {name}!";
}
return Greet;
}
โ The Key Insight
A closure is a function bundled together with its surrounding state. When makeGreeter("Hello") returns, the greeting variable would normally be gone. But because the returned function references it, the variable is kept alive. Each call to makeGreeter creates a separate closure with its own copy of greeting.
Practical Closure Patterns
Closures aren't just an academic concept โ they solve real problems. Here are three patterns you'll use constantly.
Pattern 1: Counter Factory
Create independent counters that each maintain their own count:
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
def get():
return count
def reset():
nonlocal count
count = start
# Return multiple operations as a dict
return {"next": counter, "get": get, "reset": reset}
# Two independent counters
tickets = make_counter()
orders = make_counter(1000)
print(tickets["next"]()) # 1
print(tickets["next"]()) # 2
print(orders["next"]()) # 1001
print(orders["next"]()) # 1002
print(tickets["get"]()) # 2 โ tickets is independent of orders
function makeCounter(start = 0) {
let count = start;
return {
next() { return ++count; },
get() { return count; },
reset() { count = start; }
};
}
// Two independent counters
const tickets = makeCounter();
const orders = makeCounter(1000);
console.log(tickets.next()); // 1
console.log(tickets.next()); // 2
console.log(orders.next()); // 1001
console.log(orders.next()); // 1002
console.log(tickets.get()); // 2 โ independent of orders
// Return a tuple of functions (or use a small class)
static (Func<int> Next, Func<int> Get, Action Reset) MakeCounter(int start = 0)
{
int count = start;
return (
Next: () => ++count,
Get: () => count,
Reset: () => { count = start; }
);
}
var tickets = MakeCounter();
var orders = MakeCounter(1000);
Console.WriteLine(tickets.Next()); // 1
Console.WriteLine(tickets.Next()); // 2
Console.WriteLine(orders.Next()); // 1001
Console.WriteLine(orders.Next()); // 1002
Console.WriteLine(tickets.Get()); // 2
Pattern 2: Private State (Data Hiding)
Closures can create variables that nothing outside the closure can access directly โ a form of privacy without classes:
def create_wallet(owner, initial_balance=0):
balance = initial_balance # Private โ no direct access from outside
def deposit(amount):
nonlocal balance
if amount <= 0:
return "Amount must be positive"
balance += amount
return f"Deposited ${amount:.2f}. Balance: ${balance:.2f}"
def withdraw(amount):
nonlocal balance
if amount <= 0:
return "Amount must be positive"
if amount > balance:
return f"Insufficient funds (balance: ${balance:.2f})"
balance -= amount
return f"Withdrew ${amount:.2f}. Balance: ${balance:.2f}"
def check_balance():
return f"{owner}'s balance: ${balance:.2f}"
return {"deposit": deposit, "withdraw": withdraw, "balance": check_balance}
wallet = create_wallet("Alice", 100)
print(wallet["deposit"](50)) # Deposited $50.00. Balance: $150.00
print(wallet["withdraw"](30)) # Withdrew $30.00. Balance: $120.00
print(wallet["balance"]()) # Alice's balance: $120.00
# print(balance) # โ NameError โ balance is private!
function createWallet(owner, initialBalance = 0) {
let balance = initialBalance; // Private!
return {
deposit(amount) {
if (amount <= 0) return "Amount must be positive";
balance += amount;
return `Deposited $${amount.toFixed(2)}. Balance: $${balance.toFixed(2)}`;
},
withdraw(amount) {
if (amount <= 0) return "Amount must be positive";
if (amount > balance) return `Insufficient funds (balance: $${balance.toFixed(2)})`;
balance -= amount;
return `Withdrew $${amount.toFixed(2)}. Balance: $${balance.toFixed(2)}`;
},
checkBalance() {
return `${owner}'s balance: $${balance.toFixed(2)}`;
}
};
}
const wallet = createWallet("Alice", 100);
console.log(wallet.deposit(50)); // Deposited $50.00. Balance: $150.00
console.log(wallet.withdraw(30)); // Withdrew $30.00. Balance: $120.00
console.log(wallet.checkBalance()); // Alice's balance: $120.00
// console.log(balance); // โ ReferenceError โ private!
// C# typically uses classes for private state (see Module 6),
// but closures can do it too:
static (Func<decimal, string> Deposit,
Func<decimal, string> Withdraw,
Func<string> CheckBalance)
CreateWallet(string owner, decimal initialBalance = 0)
{
decimal balance = initialBalance;
return (
Deposit: (amount) => {
if (amount <= 0) return "Amount must be positive";
balance += amount;
return $"Deposited {amount:C}. Balance: {balance:C}";
},
Withdraw: (amount) => {
if (amount <= 0) return "Amount must be positive";
if (amount > balance) return $"Insufficient funds (balance: {balance:C})";
balance -= amount;
return $"Withdrew {amount:C}. Balance: {balance:C}";
},
CheckBalance: () => $"{owner}'s balance: {balance:C}"
);
}
var wallet = CreateWallet("Alice", 100);
Console.WriteLine(wallet.Deposit(50));
Console.WriteLine(wallet.Withdraw(30));
Console.WriteLine(wallet.CheckBalance());
Pattern 3: Function Factories
Create customized functions on the fly:
# Multiplier factory
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
to_cents = make_multiplier(100)
print(double(5)) # 10
print(triple(5)) # 15
print(to_cents(9.99)) # 999.0
# Tax calculator factory
def make_tax_calculator(rate):
def calculate(price):
tax = price * (rate / 100)
return round(price + tax, 2)
return calculate
nevada_tax = make_tax_calculator(8.375)
oregon_tax = make_tax_calculator(0)
print(nevada_tax(100)) # 108.38
print(oregon_tax(100)) # 100.0
// Multiplier factory
const makeMultiplier = (factor) => (x) => x * factor;
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
const toCents = makeMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toCents(9.99)); // 999
// Tax calculator factory
function makeTaxCalculator(rate) {
return (price) => {
let tax = price * (rate / 100);
return Math.round((price + tax) * 100) / 100;
};
}
const nevadaTax = makeTaxCalculator(8.375);
const oregonTax = makeTaxCalculator(0);
console.log(nevadaTax(100)); // 108.38
console.log(oregonTax(100)); // 100
// Multiplier factory
static Func<double, double> MakeMultiplier(double factor)
{
return (x) => x * factor;
}
var doubleIt = MakeMultiplier(2);
var tripleIt = MakeMultiplier(3);
var toCents = MakeMultiplier(100);
Console.WriteLine(doubleIt(5)); // 10
Console.WriteLine(tripleIt(5)); // 15
Console.WriteLine(toCents(9.99)); // 999
// Tax calculator factory
static Func<decimal, decimal> MakeTaxCalculator(decimal rate)
{
return (price) => Math.Round(price * (1 + rate / 100), 2);
}
var nevadaTax = MakeTaxCalculator(8.375m);
var oregonTax = MakeTaxCalculator(0m);
Console.WriteLine(nevadaTax(100)); // 108.38
Console.WriteLine(oregonTax(100)); // 100.00
๐ก Closures vs. Classes
If a closure with private state sounds a lot like a class with private fieldsโฆ you're right! In fact, closures and classes are two ways to solve the same problem: bundling data with behavior. We'll explore classes in Module 6. For now, closures give you a lightweight way to do this without the full class machinery.
๐ Instructor Note: Delivery Guidance
The wallet example is the highlight of this lesson. Walk through it step by step: (1) createWallet runs and creates balance, (2) it returns functions that reference balance, (3) createWallet finishes but balance survives inside the closure, (4) each method can read and modify balance but outside code cannot. This "aha moment" โ that a finished function's variables can live on โ is the core of understanding closures. The tax calculator factory is a great real-world tie-in for Nevada students!
Exercises
๐๏ธ Exercise 1: Scope Detective
Objective: Without running the code, predict the output. Then run it to check.
x = "global"
def func_a():
x = "a"
def func_b():
print(x)
func_b()
def func_c():
print(x)
func_a() # What prints?
func_c() # What prints?
print(x) # What prints?
let x = "global";
function funcA() {
let x = "a";
function funcB() {
console.log(x);
}
funcB();
}
function funcC() {
console.log(x);
}
funcA(); // What prints?
funcC(); // What prints?
console.log(x); // What prints?
string x = "global";
void FuncA()
{
string x = "a";
void FuncB()
{
Console.WriteLine(x);
}
FuncB();
}
void FuncC()
{
Console.WriteLine(x); // captures outer x
}
FuncA(); // What prints?
FuncC(); // What prints?
Console.WriteLine(x); // What prints?
โ Answer
func_a() prints "a" โ func_b finds x in its enclosing scope (func_a).
func_c() prints "global" โ it has no local x, so it uses the global one.
The final print(x) prints "global" โ func_a's x = "a" was local and didn't affect the global.
๐๏ธ Exercise 2: Build a Counter
Objective: Create a make_counter function that returns an object/dict with three operations:
increment()โ adds 1 and returns the new countdecrement()โ subtracts 1 and returns the new countvalue()โ returns the current count without changing it
The count should start at 0. Create two independent counters and verify they don't interfere with each other.
โ Solution
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
def decrement():
nonlocal count
count -= 1
return count
def value():
return count
return {"increment": increment, "decrement": decrement, "value": value}
a = make_counter()
b = make_counter()
print(a["increment"]()) # 1
print(a["increment"]()) # 2
print(b["increment"]()) # 1 (independent!)
print(a["decrement"]()) # 1
print(a["value"]()) # 1
print(b["value"]()) # 1
function makeCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; }
};
}
const a = makeCounter();
const b = makeCounter();
console.log(a.increment()); // 1
console.log(a.increment()); // 2
console.log(b.increment()); // 1
console.log(a.decrement()); // 1
console.log(a.value()); // 1
console.log(b.value()); // 1
static (Func<int> Increment, Func<int> Decrement, Func<int> Value)
MakeCounter()
{
int count = 0;
return (
Increment: () => ++count,
Decrement: () => --count,
Value: () => count
);
}
var a = MakeCounter();
var b = MakeCounter();
Console.WriteLine(a.Increment()); // 1
Console.WriteLine(a.Increment()); // 2
Console.WriteLine(b.Increment()); // 1
Console.WriteLine(a.Decrement()); // 1
Console.WriteLine(a.Value()); // 1
Console.WriteLine(b.Value()); // 1
๐๏ธ Exercise 3: Configuration Manager
Objective: Create a createConfig function that stores key-value settings privately. Return operations to:
set(key, value)โ store a settingget(key)โ retrieve a setting (returnNone/nullif not found)get_all()โ return a copy of all settings (so the internal data can't be modified from outside)
โ Solution
def create_config():
settings = {}
def set_val(key, value):
settings[key] = value
def get_val(key):
return settings.get(key, None)
def get_all():
return dict(settings) # Return a copy!
return {"set": set_val, "get": get_val, "get_all": get_all}
config = create_config()
config["set"]("theme", "dark")
config["set"]("language", "en")
config["set"]("font_size", 16)
print(config["get"]("theme")) # "dark"
print(config["get"]("missing")) # None
print(config["get_all"]()) # {'theme': 'dark', 'language': 'en', ...}
# The copy means outside changes don't affect internal state:
snapshot = config["get_all"]()
snapshot["theme"] = "hacked"
print(config["get"]("theme")) # Still "dark"!
function createConfig() {
const settings = {};
return {
set(key, value) { settings[key] = value; },
get(key) { return settings[key] ?? null; },
getAll() { return { ...settings }; } // Spread = copy!
};
}
const config = createConfig();
config.set("theme", "dark");
config.set("language", "en");
config.set("fontSize", 16);
console.log(config.get("theme")); // "dark"
console.log(config.get("missing")); // null
console.log(config.getAll()); // {theme: 'dark', ...}
// The copy protects internal state:
const snapshot = config.getAll();
snapshot.theme = "hacked";
console.log(config.get("theme")); // Still "dark"!
static (Action<string, string> Set,
Func<string, string?> Get,
Func<Dictionary<string, string>> GetAll)
CreateConfig()
{
var settings = new Dictionary<string, string>();
return (
Set: (key, value) => settings[key] = value,
Get: (key) => settings.TryGetValue(key, out var val) ? val : null,
GetAll: () => new Dictionary<string, string>(settings) // Copy!
);
}
var config = CreateConfig();
config.Set("theme", "dark");
config.Set("language", "en");
config.Set("fontSize", "16");
Console.WriteLine(config.Get("theme")); // "dark"
Console.WriteLine(config.Get("missing")); // (null)
var snapshot = config.GetAll();
snapshot["theme"] = "hacked";
Console.WriteLine(config.Get("theme")); // Still "dark"!
๐ Instructor Note: Delivery Guidance
Exercise 1 is a great whiteboard/group activity โ have students trace the scope chain on paper before running the code. Exercise 2 reinforces the counter factory pattern from the lesson. Exercise 3 is the capstone โ the "return a copy" requirement teaches an important data-integrity concept. Challenge fast students: add a delete operation, or make the config emit a "change event" (a callback) whenever a setting is modified.
Summary
๐ Key Takeaways
- Local scope: variables inside a function are invisible outside it
- Global scope: variables at the top level are accessible everywhere (but avoid overusing them)
- Block scope: C# and JS (
let/const) have it; Python does not โ Python only scopes to functions and classes - Modifying outer variables: Python needs
global/nonlocal; JS does it directly; C# captures variables in closures and lambdas - Scope chain: variables are looked up from innermost scope outward (LEGB in Python)
- Hoisting: JavaScript-specific โ
vardeclarations are moved to the top; uselet/constto avoid surprises - Closures: a function that remembers variables from its creation scope โ used for counters, private state, and function factories
๐ What's Next?
That wraps up Module 4: Functions! You now know how to define, call, parameterize, and scope your functions โ and you've seen how closures give functions a "memory." In Module 5, we shift from single values to collections: arrays, lists, dictionaries, and objects โ the data structures that make real programs possible.
๐ฏ Quick Check
Question 1: What is a closure?
Question 2: In Python, which keyword lets a nested function modify a variable from its enclosing function?
Question 3: Why should you prefer let/const over var in JavaScript?