{ Function Scope. }

Objectives

By the end of this chapter, you should be able to:

  • Define what scope is
  • Understand what globals and locals are
  • Document functions and specify data types for arguments and return values

Scope

In Python we have function scope, which prohibits us from accessing variables created inside of a function from outside of that function:

def func():
    x = 5
    return x

func() # 5
x # NameError

The global keyword

The global scope includes all variables defined outside of functions. But if we try to use a global variable in a method, we will see
UnboundLocalError: local variable VARIABLE_NAME referenced before assignment. This happens because a method in Python either has local variables or global variables. If variable is defined anywhere in a method and that variable has the same name as a global variable, then the new local variable will be used in the function instead of the global. But if you actually want to assign a global variable from within a function, you need to use the global keyword. Using global variables in general is not best practice:

id = 0
def increment_id():
    id += 1

increment_id() # UnboundLocalError: local variable 'id' referenced before assignment

def increment_id():
    global id
    id += 1

increment_id() # The global id is now 1

In Python you need to explicitly state that a variable should be global, using the global keyword.

Listing locals and globals

In Python we can display all of the local variables and global variables using the locals and globals functions

def print_locals():
    x = 2
    name = "Elie"
    print(locals())

name = "person"
print(globals())
print(locals())

Python Nested Functions (sort of like closures)

In Python we do have support for closures, a feature where an inner function has access to variables in an outer function's scope, even after the outer function has finished executing.

def outer(a):

    def inner(b):
        return a + b
    return inner

outer(3)(4) # 7
x = outer(2)
x(10) # 12

However, closures in Python are "weak" and have some limitations. For example, if you want to change the value of a variable from an outer scope, you'll run in to problems:

def counter():
    x = 0;

    def increment():
        x += 1 
        print(x)

    return increment

counter()() # UnboundLocalError: local variable 'x' referenced before assignment

Again, this is because the x inside of increment is a new variable, bound to the scope of increment. It's not a reference to x coming from the scope of counter.

We can get around the problem with the example above by setting attributes on the inner function, rather than trying to change variables from an outer scope:

# We can get around this by doing
def outer_count():

    def inner_count():
        inner_count.x += 1
        print(inner_count.x)

    inner_count.x = 0

    return inner_count

You can read more about this concept of "read only" closures here.

Documenting our functions

Something that Python offers us is the ability to add what is called a docstring. Let's see what that looks like

def say_hello():
    # we are using three quotes so that this can be a multi-line string if necessary
    """This function returns the string hello when called"""
    return "hello"

We can call this function using

say_hello() # "hello"

say_hello.__doc__ # "This function returns the string hello when called"

help(say_hello) # gives us even more detail with the docstring!

Docstrings are essential when writing methods and can be thought of like an enhanced comment. Docstrings are also very useful when writing tests, as you can see what the docstring is when running the test. You are highly encouraged to write docstrings for your functions, and inside classes as well. You can read more about standards on docstrings here.

Default argument types for Python

Unlike languages like Java and C++, Python is a dynamically typed language. This means that we do not need to explicitly define the data type of a variable when initializing it. This gives us a bit more flexibility around our code, but sometimes we want to clearly indicate that a certain data type is what should be passed as a parameter, or that a function returns a specific value. We can do that in Python! Let's see what that looks like:

def add(a: int, b: int) -> int:
    """This function returns the sum of two numbers"""
    return a + b   

We are specifying that both a and b are ints and the return value from the function is an int as well. We can also combine this with default parameter values!

def add(a: int = 5 ,b: int = 5) -> int:
    """This function returns the sum of two numbers with default values of 5 for a and 5 for b"""
    return a + b

When you're ready, move on to Functions Exercises

Continue