Skip to main content

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​

  1. 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
  2. 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:

  • global is only needed for assignment/modification
  • Multiple variables can be declared global in one statement
  • Reading global variables doesn't require global keyword
  • Modifying contents of mutable objects (lists, dicts) doesn't require global

Common Student Mistakes to Watch For​

  1. Using global when not needed

    # BAD - unnecessary global declaration
    PI = 3.14159

    def circle_area(radius):
    global PI # not needed - just reading PI
    return PI * radius * radius
  2. Forgetting global when 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 var and :=
  • Python requires global keyword 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."