Accessing variables defined in enclosing scope

In first case, you are referring to a nonlocal variable which is ok because there is no local variable called a.

def toplevel():
    a = 5
    def nested():
        print(a + 2) # theres no local variable a so it prints the nonlocal one
    nested()
    return a

In the second case, you create a local variable a which is also fine (local a will be different than the nonlocal one thats why the original a wasn't changed).

def toplevel():
    a = 5 
    def nested():
        a = 7 # create a local variable called a which is different than the nonlocal one
        print(a) # prints 7
    nested()
    print(a) # prints 5
    return a

In the third case, you create a local variable but you have print(a+2) before that and that is why the exception is raised. Because print(a+2) will refer to the local variable a which was created after that line.

def toplevel():
    a = 5
    def nested():
        print(a + 2) # tries to print local variable a but its created after this line so exception is raised
        a = 7
    nested()
    return a
toplevel()

To achieve what you want, you need to use nonlocal a inside your inner function:

def toplevel():
    a = 5
    def nested():
        nonlocal a
        print(a + 2)
        a = 7
    nested()
    return a

For anyone stumbling across this question, in addition to the accepted answer here, it is answered concisely in the Python docs:

This code:

>>> x = 10
>>> def bar():
...     print(x)
>>> bar()
10

works, but this code:

>>> x = 10
>>> def foo():
...     print(x)
...     x += 1

results in an UnboundLocalError.

This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. Since the last statement in foo assigns a new value to x, the compiler recognizes it as a local variable. Consequently when the earlier print(x) attempts to print the uninitialized local variable and an error results.

In the example above you can access the outer scope variable by declaring it global:

>>> x = 10
>>> def foobar():
...     global x
...     print(x)
...     x += 1
>>> foobar()
10

You can do a similar thing in a nested scope using the nonlocal keyword:

>>> def foo():
...    x = 10
...    def bar():
...        nonlocal x
...        print(x)
...        x += 1
...    bar()
...    print(x)
>>> foo()
10
11