Variable Scope

Variables and Their Scope

Local variables are variables defined inside functions, and are only accessible from inside the function where it is defined:

>>> def hello():
...     statement = "hello world"
...
>>> statement
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'statement' is not defined

Global variables are variables defined outside of functions and are accessible from anywhere in their enclosing scope:

>>> statement = "hello"
>>> def hello():
...     print(statement)
...
>>> hello()
hello
>>> statement = "hi"
>>> hello()
hi

Global vs. Local Variables

When you assign a variable inside a function, it will assign to a local variable, even if there is a global variable of the same name.

>>> statement = "hello"
>>> def hello():
...     statement = "hello world"
...
>>> statement
'hello'
>>> hello()
>>> statement
'hello'

Incrementing an existing variable will actually raise an exception because we can’t both assign to a local variable while reading from a global variable with the same name.

>>> call_count = 0
>>> def hello():
...     call_count += 1
...
>>> hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in hello
UnboundLocalError: local variable 'call_count' referenced before assignment

If you want to assign a variable in the global scope instead of the local scope, you can use the global statement:

>>> def hello(phrase):
...     global statement
...     statement = phrase
...
>>> hello("hello")
>>> statement
'hello'
>>> hello("hello world")
>>> statement
'hello world'

When assigning to a variable, Python always uses the local scope unless you tell it otherwise. Assigning to global variables is discouraged unless absolutely necessary.

Non-local

Let’s define a decorator that counts the number of times a function has been called:

def count_calls(func):
    calls = 0
    def new_func(*args, **kwargs):
        calls += 1
        print(f"Called {calls} times")
        return func(*args, **kwargs)
    return new_func

When we try to call a function that uses this decorator we will see an error:

>>> @count_calls
... def hello():
...     print("hi")
...
>>> hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in new_func
UnboundLocalError: local variable 'calls' referenced before assignment

A local variable is scoped to its function, not its enclosing scope.

There are three scopes in Python: local, enclosing, and global.

To modify a variable from the enclosing scope we can use the nonlocal statement:

def count_calls(func):
    calls = 0
    def new_func(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"Called {calls} times")
        return func(*args, **kwargs)
    return new_func

We can see that this caused our calls variable to assign in the scope just outside of our function as we expected:

>>> @count_calls
... def hello():
...     print("hi")
...
>>> hello()
Called 1 times
hi
>>> hello()
Called 2 times
hi

When Python encounters a variable, it first looks for it in the local scope, then the enclosing scope, then the global scope, then it looks in built-ins. However, if the variable is being assigned to, it always uses the local scope, unless there is a global or nonlocal statement.

Built-ins

I said there are three scopes before. I lied; there are actually four.

The fourth scope is for built-ins.

This scope includes singletons like True, False, and None:

>>> True
True
>>> None

There are also a number of functions and constructors:

>>> str()
''
>>> dict()
{}
>>> type(str)
<class 'type'>
>>> help(str)  

>>> abs(-4)
4
>>> max(4, 2)
4
>>> len("")
0
>>> sorted("hello")
['e', 'h', 'l', 'l', 'o']

There are also some dunder variables:

>>> __name__  
'__main__'
>>> __doc__  
>>> __package__  

And a lot of built-in exceptions:

>>> Exception
<class 'Exception'>
>>> ValueError
<class 'ValueError'>
>>> SystemError
<class 'SystemError'>
>>> SyntaxError
<class 'SyntaxError'>
>>> NameError
<class 'NameError'>
>>> KeyError
<class 'KeyError'>
>>> IndexError
<class 'IndexError'>
>>> TypeError
<class 'TypeError'>

Scope Introspection

We can find all of the built-in variables by importing the builtins module:

>>> import builtins
>>> builtins.KeyError
<class 'KeyError'>
>>> builtins.len
<built-in function len>
>>> builtins.len is len
True

We can use the dir function to look at everything inside the builtins module:

>>> dir(builtins)  
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

All global variables can be found using the built-in globals functions:

>>> globals()  
{'__name__': '__main__', '__spec__': None, 'builtins': <module 'builtins' (built-in)>, '__doc__': None, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>}

Local variables can be found using the built-in locals function:

>>> locals()  
{'__name__': '__main__', '__spec__': None, 'builtins': <module 'builtins' (built-in)>, '__doc__': None, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>}

Outside of any function or class, these two dictionaries are identical:

>>> locals() == globals()
True

Let’s try looking at the values of local and global from inside a function:

>>> def hello(statement):
...     print(locals())
...     print(globals())
...
>>> hello("hi!")  
{'statement': 'hi!'}
{'hello': <function hello at 0x7f7c687f3c80>, '__doc__': None, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__name__': '__main__', '__spec__': None}

Function Scope

One more interesting thing to note about Python’s scope rules: Python uses function-level scope, not block-level scope. That means that a variable defined in a block of code (like an if statement or a for loop) will be accessible outside of that block.

>>> x = 4
>>> for x in range(3):
...     y = x
...     print(x)
...
0
1
2
>>> x
2
>>> y
2