Skill 7: Variable Scope (Pyret & Python)
1. Function Scope vs Global Scope
What to Teach
Variables have different accessibility depending on where they're defined. Understanding scope is crucial for debugging and writing maintainable code.
Basic Scope Concepts
# Pyret - Global scope
global-value = 10
fun example-function(param :: Number) -> Number:
# Function scope - only accessible inside this function
local-value = 5
param + local-value + global-value
where:
example-function(3) is 18
example-function(0) is 15
example-function(-2) is 13
end
# local-value is not accessible here
# param is not accessible here
# global-value is accessible here
# Python - Global scope
global_value = 10
def example_function(param):
# Function scope - only accessible inside this function
local_value = 5
return param + local_value + global_value
# local_value is not accessible here
# param is not accessible here
# global_value is accessible here
Teaching Examples
Good Example - Clear Scope Usage:
# Pyret
ALMOST-PI = 3.14159 # global constant
fun circle-area(radius :: Number) -> Number:
doc: "calculates area of circle"
# radius and area are local to this function
area = ALMOST-PI * radius * radius
area
where:
circle-area(1) is-roughly 3.14159
circle-area(2) is-roughly 12.56636
circle-area(0) is 0
circle-area(5) is-roughly 78.53975
end
fun circle-circumference(radius :: Number) -> Number:
doc: "calculates circumference of circle"
# different local variable, same name is fine
circumference = 2 * ALMOTS-PI * radius
circumference
where:
circle-circumference(1) is-roughly 6.28318
circle-circumference(2) is-roughly 12.56636
circle-circumference(0) is 0
circle-circumference(5) is-roughly 31.4159
end
PI = 3.14159 # global constant
def circle_area(radius):
"""calculates area of circle"""
# radius and area are local to this function
area = PI * radius * radius
return area
def circle_circumference(radius):
"""calculates circumference of circle"""
# different local variable, same name is fine
circumference = 2 * PI * radius
return circumference
What to Point Out:
- Global variables are accessible everywhere
- Local variables only exist within their function
- Parameters are local to their function
- Same variable names can be used in different functions
- Functions can read global variables without special syntax
Common Student Mistakes to Watch For
-
Trying to access local variables outside their function
# BAD - trying to use local variable globally
def calculate():
result = 10 * 5
return result
calculate()
print(result) # result is not defined -
Assuming all variables are global
# BAD - expecting local changes to affect global scope
x = 10
def modify_x():
x = 20 # creates new local variable, doesn't change global x
modify_x()
print(x) # still 10, not 20
2. Variable Shadowing
What to Teach
Shadowing occurs when a variable in an inner scope has the same name as a variable in an outer scope. This can hide the outer variable and cause confusing bugs. Pyret prevents shadowing entirely, while Python allows it.
Pyret Prevents Shadowing
# Pyret - shadowing is not allowed
name = "Alice"
fun greet-user(name :: String) -> String:
# ERROR - parameter 'name' shadows global 'name'
# Pyret will reject this code
doc: "greets a user by name"
"Hello, " + name
end
# Also not allowed - nested scope shadowing
counter = 0
fun process-items(items :: List<String>) -> Number:
for each(counter from items): # ERROR - 'counter' shadows global counter
print(counter)
end
counter # which counter? global or loop variable?
end
Python Allows Shadowing
# Python - shadowing is allowed but dangerous
name = "Alice"
def greet_user(name):
# This parameter shadows global 'name', Python allows it
return f"Hello, {name}" # uses parameter, not global
print(name) # still "Alice" - global is shadowed but not changed
greet_user("Charlie") # prints "Hello, Charlie"
print(name) # still "Alice"
# Shadowing with loop variables
counter = 0
def process_items(items):
for counter in items: # shadows global counter
print(counter)
return counter # returns last item, not global counter!
Teaching Examples
Pyret - Clear Error Messages:
radius = 10
fun circle-area(radius :: Number) -> Number:
# ERROR: "radius shadows an identifier from an outer scope"
doc: "calculates area of circle"
ALMOST-PI * radius * radius
end
Python - Hidden Bug:
total = 0
def process_scores(scores):
for total in scores: # accidentally shadows global total
if total > 90:
print(f"High score: {total}")
# Intended to return global total, but returns last score instead
return total
# Bug: function returns last score (e.g. 85) instead of global total (0)
result = process_scores([95, 88, 92, 85])
print(result) # prints 85, not 0
What to Point Out:
- Pyret's shadowing prevention catches bugs at compile time
- Python's shadowing can create subtle runtime bugs
- Shadowing makes code harder to understand and debug
- Good variable naming prevents accidental shadowing
- When reading Python code, always check for shadowing
3. Python Variable Definition vs Update
What to Teach
Python distinguishes between creating new variables and updating existing ones. This affects scope behavior significantly.
Variable Assignment Behavior
# global variable
counter = 0
def increment_local():
# this creates a NEW local variable named counter
counter = counter + 1 # can't read local variable before assignment
return counter
def increment_global():
# this reads the global variable
global counter
counter = counter + 1 # updates the global variable
return counter
What to Point Out:
- Any assignment in a function creates a local variable
- Python scans the entire function for assignments before executing
- You can't read a local variable before assigning to it
- Reading without assignment accesses outer scopes
4. Python's global Keyword
What to Teach
The global keyword tells Python to use the global variable instead of creating a local one. It's necessary when you want to modify global variables from within functions.
Basic global Syntax
# global variable
counter = 0
def increment():
global counter # declares intent to modify global variable
counter = counter + 1 # this modifies the global counter
def reset():
global counter
counter = 0
When global is Needed
# 1) Modifying global variables
game_score = 0
def add_points(points):
global game_score
game_score = game_score + points # modifies global variable
# 2) Reassigning global variables
current_level = 1
def next_level():
global current_level
current_level = current_level + 1 # changes global variable
# 3) When assignment is conditional
lives = 3
def lose_life():
global lives
if lives > 0:
lives = lives - 1 # assignment requires global declaration
When global is NOT Needed
PI = 3.14159
def circle_area(radius):
# no global needed - just reading PI
return PI * radius * radius
# modifying mutable objects
high_scores = [100, 95, 90]
def add_score(score):
# no global needed - modifying list contents, not reassigning variable
high_scores.append(score)
high_scores.sort(reverse=True)
# using global variables in expressions
tax_rate = 0.08
def calculate_total(price):
# No global needed - just reading tax_rate
return price * (1 + tax_rate)
What to Point Out:
globalis only needed for assignment/modification- Multiple variables can be declared global in one statement
- Reading global variables doesn't require
globalkeyword - Modifying contents of mutable objects (lists, dicts) doesn't require
global
Common Student Mistakes to Watch For
-
Using
globalwhen not needed# BAD - unnecessary global declaration
PI = 3.14159
def circle_area(radius):
global PI # not needed - just reading PI
return PI * radius * radius -
Forgetting
globalwhen modifying# BAD - missing global declaration
total = 0
def add_to_total(amount):
total = total + amount # missing global declaration
5. Comparing Pyret and Python Scope
What to Teach
While both languages have similar scope concepts, their syntax and behavior differ in important ways.
Scope Similarities
outer-value = 100
fun inner-function() -> Number:
outer-value + 50 # can read outer-value
where:
inner-function() is 150
end
outer_value = 100
def inner_function():
return outer_value + 50 # can read outer_value
Key Differences
Pyret Variable Mutability:
# explicit mutability with var
var counter = 0
fun increment() -> Number:
counter := counter + 1 # uses := for mutable variable update
counter
where:
block:
counter := 0 # reset for test
increment() is 1
increment() is 2
increment() is 3
end
end
# regular bindings cannot be changed
fixed-value = 10
# fixed-value := 20 # error
Python Variable Assignment:
# assignment behavior depends on scope
counter = 0
def increment():
global counter # must declare global to modify
counter = counter + 1
return counter
def increment_local():
counter = 1 # creates new local variable
return counter
What to Point Out:
- Pyret makes mutability explicit with
varand:= - Python requires
globalkeyword for assignment to global variables - Both languages allow reading outer scope without special syntax
- Pyret prevents accidental mutation of immutable bindings
- Python's assignment always creates local variables unless declared global
Common Teaching Scenarios
When Students Ask "Why can't I just make everything global?"
"Global variables make code harder to understand and debug. Functions should receive what they need as parameters and return what they produce."
When Students Confuse Pyret var and Python global
"In Pyret, var creates a mutable binding, which you can change with :=. In Python, global tells the function to use the global variable instead of creating a local one when you assign."
When Students Overuse Global Variables
"Think about whether the function really needs to modify global state, or if it could just return a value that the caller can use to update state. Functions with no global modifications are easier to test and understand."