2. Inception

Every program in Python (among most other programming languages) uses variables, and every variable has a name. This concept is so natural we don't really think too much about it (atleast in Python) --and that's where traps and pitfalls await. In order to avoid these pitfalls, one must understand the scope of a variable.

To define this concept, it's helpful to see it in action first. Consider the following simple example of a function that prints the value of a variable named a.


In [ ]:
a = 42

def my_print():
    print("Printing from inside function", a)
    
my_print()
print("Printing from outside function", a)

There is a variable named a and its value is 42. There is nothing surprising about the output as all occurrences of a in the program will refer to this variable.

But what happens when we create a function which uses a new variable, also named a?


In [ ]:
a = 42 # Line 1

def my_print():
    a = 1  # Line 4
    print("Printing from inside function", a) # Line 5
    
my_print()  # Line 7
print("Printing from outside function", a) # Line 8

Now we appear to have two a variables: one is "inside" the function and the other is "outside." When the program calls my_print() on line 7, it begins executing the my_print() function at line 4, which creates a new variable named a, whose value is 1. Within the function, any reference to a, such as the one on line 5, will refer to this new a not the original a (whose value is 42).

In computing lingo, we say that the scope of the variable named a on line 4 is the function, my_print(). We sometimes also say that the a on line 4 is local to my_print(). We also say that the a on line 1 is in global scope.

When the program finishes executing my_print(), the scope "vanishes" and the name a on line 4 is no longer visible. That's why the print on line 8 now refers back to the a whose value is 42.

If we have the same variable names in global scope and as function parameter (in function scope), they work the same way:


In [ ]:
a = 42

def my_print(a):
    print("Printing from inside function", a)
    
my_print(1)
print("Printing from outside function", a)

However, if we want to change that and access the a variable from global scope, we still can do that using global keyword.


In [ ]:
a = 42

def my_print():
    global a
    print("Printing from inside function", a)
    
my_print()
print("Printing from outside function", a)

However, a function cannot simultaneously reference global and local variables with the same name. That means if you have a global a within a function, none of the parameters may be named a.

Consider the following scenario.


In [ ]:
a = 42

def my_print():
    try:
        print("Printing GLOBAL variable from inside function", a)
    except:
        print ("UnboundLocalError: local variable 'a' referenced before assignment")
        return
    a = 3
    print("Printing from inside function", a)
    
my_print()
print("Printing from outside function", a)

This error is very common case case in Jupyter Notebooks and hence it's important to understand. Quite often when you restart your notebook and run a cell in the middle of the notebook, you'll see a similar type of error whose name is not definied. That happens because when you restart your notebook, it's context is reset; and if you don't have the variable defined in the same cell, the variable does not exist in the scope of the notebook. All the cells have the same scope, but the way variables are populated is determined by the running order of notebook cells.

Exercise. A terrific tool for visualizing the concept of scopes is Phil Guo's Python Tutor. Visit that website and click on the "Start visualizing your code now" link. Enter any of the above programs into the code box you see, and then do a line-by-line execution to see how variables are created and come to exist in different scopes.

Then try visualizing this program. See if you can predict what it will output before you run it.


In [ ]:
x = 1

def foo(x):
    # Q: Can you use `global x` at this part of the program?
    def bar(y):
        global x
        print("bar: x == {}, y == {}".format(x, y))
        
    print("foo: x == {}".format(x))
    x -= 3
    bar(2*x)
    
print("x == {}".format(x))
foo(5)
print("x == {}".format(x))