When we declare variables and functions, there is something called scope associated with them. We will use the term identifier to encompass function names, variable names, and parameter names because the same scope rules apply to all of these. Scope determines in what parts of a program an identifier can be used. Intuitively, scope is the context in which a variable or function “lives.” This is connected to, but not equivalent to, where the identifier is valid.
We have seen two levels of scope, even though we have not talked about this concept yet:
global
local
Global scope: When we assign to a variable outside of a function, the variable belongs to the global scope. The variable is created the first time that it is assigned to. When running in CodeLens, these variables appear in the global frame. A global variable exists from the time it is created until the program ends. Similarly, we declare functions in the global scope.
Local scope: When we assign to a variable inside a function, the variable’s scope is local to the function. The variable is created the first time it is assigned to inside the function. When running in CodeLens, these parameters and variables appear in the frame for that function. A local variable no longer exists when the function returns.
Parameters have local scope. A parameter’s scope is also local to the function it is a parameter for. It is created when the argument is bound to the parameter when the function is called. A parameter no longer exists when the function returns. In CodeLens, a parameter is part of the function’s frame and disappears when the function returns.
Why does this matter? When we use an identifier in Python, it needs to find the entity that is being referred to. This could be a variable, a parameter, or a function. The scope rules determine what entity is being referred to. This becomes particularly important if there is more than one entity in our program with the same name.
This analogy might help: a campground often has “public” shared facilities, such as bathrooms, picnic benches, a parking area, even vending machines if you’re lucky. These facilities are available to everyone on the campground, so we might say they have global scope. However, once you put up your tent, the items inside are somehow shielded from other campgoers. For instance, your sleeping bag, light, books, etc. are now in your local scope and are unavailable to anyone outside the tent. Of course, you still have access to the shared facilities (global scope), but there is this notion that your belongings are hidden from others by being inside your tent. Similarly, there are other tents with other things inside those tents. You do not have access to those things. Notice that local is defined with respect to some context, whereas global does not need any extra information.
In this first example, the function numberOfUnassigned and the variables numStudents and groupSize are created in the global scope.
Visualizing the code in Python Tutor.
def numberOfUnassigned( n:int ) -> int:
"""Returns the number of unassigned students after dividing the class into groups of 3."""
return (n % 3) # after dividing, the remainder tells us how many are unassigned
numStudents:int = 23
groupSize:int = 3
print( f"{numberOfUnassigned( numStudents )} students will be unassigned.")
Visualize the program in Python Tutor and you will see n and numStudents appear in the getInfo frame, and disappear when numberOfUnassigned returns.
def numberOfUnassigned( n:int ) -> int:
"""Returns the number of unassigned students after dividing the class into groups of 3."""
groupSize:int = 3
return (n % groupSize) # after dividing, the remainder tells us how many are unassigned
numStudents:int = 23
print( f"{numberOfUnassigned( numStudents )} students will be unassigned.")
Scope moves out! If a function uses a name that does not match anything in the local scope, then Python will look in the global scope. In fact, this is critical to allow functions to call other functions.
When getGroupInfo calls the numberOfUnassigned function, it first looks to see if there is something named numberOfUnassigned in its local scope. There is not. So, it then looks in the global scope, finds numberOfUnassigned and calls the function defined there.
Functions can also use variables that belong to the global scope, although this can often lead to poorly written code. It is generally better for functions to use just their parameters and local variables. We will generally avoid using global variables within functions in this course.
Visualize this example in Python Tutor.
def getGroupInfo( n:int ) -> str:
"""Gives information about groups for the class"""
groupSize:int = 3
numberOfGroupsInfo:str = f"There will be {n // groupSize} groups."
unassignedInfo = f"{numberOfUnassigned( n )} students will be unassigned."
return f"{numberOfGroupsInfo}\n{unassignedInfo}"
def numberOfUnassigned( n:int ) -> int:
"""Returns the number of unassigned students after dividing the class into groups of 3."""
groupSize:int = 3
return (n % groupSize) # after dividing, the remainder tells us how many are unassigned
numStudents:int = 23
print( getGroupInfo( numStudents ) )
Global code can only use global identifiers.
If you try to run the following code, you will get an error since numStudents is local to the function numberOfUnassigned.
def numberOfUnassigned( n:int ) -> int:
"""Returns the number of unassigned students after dividing the class into groups of 3."""
numStudents:int = 23
return (n % 3) # after dividing, the remainder tells us how many are unassigned
print( f"The number of students is {numStudents}.")
The rules listed are pretty easy to understand if everything in a program has a unique name. Often this is not the case, and we need to know the rules to understand what will happen.
Both functions have parameters named s, and local variables named spacePos. When getFirstWord is executing, it uses its versions of s and spacePos, while getLastWord uses its versions.
Visualize this example in Python Tutor.
def getFirstWord( s:str ) -> str:
'''Find the first word in s and return it.'''
spacePos:int = s.find (' ')
if spacePos == -1:
return s
else:
return s [:spacePos]
def getLastWord( s:str ) -> str:
'''Find the last word in s and return it.'''
spacePos = s.rfind(' ')
if spacePos == -1:
return s
else:
return s[spacePos+1:]
founder:str = "Mary Lyon"
firstName:str = getFirstWord ( "Mary Lyon" )
lastName:str = getLastWord ( "Mary Lyon" )
print( f"{founder} is made up of {firstName} and {lastName}" )
This holds even when a function calls another function, as shown here.
Visualize this example in Python Tutor. Notice that both the square and squaredSquare functions have x in their frames. Also notice that when the square function changes the value of x, it does not affect the value that squaredSquare has for x.
def square( x:int ) -> int:
"""Takes in a number x and returns the square of x."""
xSquared:int = x * x
x:int = x + 1
return xSquared
def squaredSquare( x:int ) -> int:
"""Takes in a number x and returns the x^4."""
return square(x)*square(x)
n:int = 2 # assign a variable named n the value 2
# invoke the function square on the variable n
squaredValue:int = squaredSquare( n )
print( f"squaredSquare(n) is {squaredSquare(n)}" )
The global code will use the global entity, and the function will use the local entity.
In this example, there is a global variable named s and the double function has a parameter named s. These are two distinct entities.
Visualize this with Python Tutor. You will see s in the global frame and a separate s parameter in double’s frame. They happen to have the same value because the global variable s is passed as the argument to the double function, setting the s parameter.
def makeDouble(s:str) -> str:
return s + s
s:str = 'Hello'
s = makeDouble(s)