Scopes of visibility

Namespaces are just dictionaries with some names (what we call variable name) mapped to objects (actual data in memory). This mapping allows to access target object by a name that we've assigned to it. So:

some_string = "Hello World"

creates a reference to the "Hello World" object, and makes it accessible by variable name some_string.

In this case our namespace will be:

{"some_string": "Hello World"}

In Python it's possible to have multiple namespaces (for example: each function has it's own context). When we trying to get some variable inside some namespace Python firstly looks at so-called local namespace and if it is not found it goes "upper". Such local contextual namespaces in Python called "scopes".

Scopes of visibility

All variables in a program may not be accessible at all locations in that program. This depends on where you have declared a variable.

The scope of a variable determines the portion of the program where you can access a particular identifier.

Scopes of visibility in Python:

  • Builtin variables

  • Global variables

  • Enclosed variables

  • Local variables

The search is goes only in those 4 places!

Local variables can be accessed only inside the function in which they are declared, whereas global variables can be accessed throughout the program body by all functions. When you call a function, the variables declared inside it are brought into scope. Use globalto access global variable.

If a name is bound in a block, it is a local variable of that block, unless declared as nonlocal or global. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

🪄 Code:

print("builtin:", all)
all = [1, 2, 3]
print("global:", all)
def foo():
    all = [4, 5, 6]
    
    def inner1():
        nonlocal all 
        all = [1, 2, 3, "changed!"]
        print("enclosed:", all)
        
    def inner2():
        global all
        all = [7, 8, 9]
        print("changing global:", all)
    
    def inner3():
        print("inner3 says:", all)
        
    inner1()
    print("checking 'all':", all)
    inner2()
    print("checking 'all':", all)
    inner3()
foo()
print("now global is", all)

📟 Output:

builtin: <built-in function all>
global: [1, 2, 3]
enclosed: [1, 2, 3, 'changed!']
checking 'all': [1, 2, 3, 'changed!']
changing global: [7, 8, 9]
checking 'all': [1, 2, 3, 'changed!']
inner3 says: [1, 2, 3, 'changed!']
now global is [7, 8, 9]

🪄 Code:

a = 1
b = 3.1415926
c = "old string"

def f():
    print(a)  
    global b

    b = 2
    c = "new string"

f()
print(a, b, c)

📟 Output:

1
1 2 old string
  • a - free variable

  • b - global variable

  • c - local variable

In case above we don't have variable a in local scope so we go upper - and take it from global scope.

If the nearest enclosing scope for a free variable contains a global statement, the free variable is treated as a global.

🪄 Code:

# Global variables
a = 0 
b = 30

def do_job( ): 
    a = 100    # Creating local variable which won't affect global one
    global b   # Marking absolute variable as global 
    b = 888    # Changing it will change global variable
    print("Inside the function -> a:", a)
    print("Inside the function -> b:", b)

do_job()

print("Outside the function -> a:", a)
print("Outside the function -> b:", b)

📟 Output:

Inside the function -> a: 100
Inside the function -> b: 888
Outside the function -> a: 0
Outside the function -> b: 888

Get all locals, globals:

  • locals()

  • globals()

Note: in global scope locals and globals are the same.

🪄 Code:

a = 5
b = 10
def f():
    c = 25
    global b
    print(locals())
f()

📟 Output:

{'c': 25}

Enclosed scope

Scope that is between global and local in nested functions

🪄 Code:

a = 'global variable'

def outer():
    a = 'enclosed variable'
    
    def inner():
        a = 'local value'
        print(a)
    
    inner()

outer()

📟 Output:

local value

🪄 Code:

a = 'global variable'

def outer():
    a = 'enclosed variable'
    
    def inner():
        print(a)
    
    inner()

outer()

📟 Output:

enclosed variable

Introducing nonlocal statement which marking variable as enclosed (just like global does for global scope)

🪄 Code:

a = 'global variable'

def outer():
    a = 'enclosed variable'
    print(a)
    
    def inner():
        nonlocal a
        a = "changed enclosed variable"
        print(a)
    
    inner()
    print(a)
outer()
print(a)

📟 Output:

enclosed variable
changed enclosed variable
changed enclosed variable
global variable

Assign operation creates a local variable by default (if not global or nonlocal used for that variable).

🪄 Code:

a = 25
def foo():
    # global a
    a += 30  # a = a + 30
    
    b = a + 25
    return a, b
foo()

📟 Output:



UnboundLocalErrorTraceback (most recent call last)

<ipython-input-3-591bdaeb3bef> in <module>
      6     b = a + 25
      7     return a, b
----> 8 foo()


<ipython-input-3-591bdaeb3bef> in foo()
      2 def foo():
      3     # global a
----> 4     a += 30  # a = a + 30
      5 
      6     b = a + 25


UnboundLocalError: local variable 'a' referenced before assignment

🪄 Code:

a = 25
def foo(a=a):
    a += 30  # a = a + 30
    return a
foo()

📟 Output:

55

Closures

🔥

Functions can use variables from outer scopes.

Also it's worth to mention that those variables are searched only when function is called.

🪄 Code:

def foo():
    print(i)
    
i = 5
foo()

📟 Output:

5

🪄 Code:

def foo():
    a = 5
    
    def inner():
        return a
    
    print("Running inner:", inner())
    
    return inner

#print(foo())
result = foo()
print(result)
print("Result of running inner function:", result())

📟 Output:

Running inner: 5
<function foo.<locals>.inner at 0x7f3e881fe488>
Result of running inner function: 5

How can inner know about a if foo is already returned and all we can't access to it's local variables normally?

🪄 Code:

print("Free vars:", result.__code__.co_freevars)
print(result.__closure__)
cell = result.__closure__[0]
print("Free var #1 value:", cell.cell_contents)

📟 Output:

Free vars: ('a',)
(<cell at 0x7f3e88272258: int object at 0x5594948e94a0>,)
Free var #1 value: 5

Last updated