Flevo CFD

Mutable and Immutable Variables and Namespaces in Python

Namespace is a place where a variable is stored. There are four namespaces in Python as local, nested (or enclosing), global and built-in.

What type of programming language is Python?

Is it an interpreted or compiled language? there’s an assumption that Python is an interpreted language meaning an interpreter (like CPython which is the dominant interpreter in the ecosystem) runs the code line by line.

It’s versus compiled language which a compiler compiles the entire program into machine code. I don’t want to get into comparing them.

I said there’s an assumption that Python is an inerpreted language which is not totally correct!

The way Python executes a program is that the interpreter compiles the source code into bytecode which is cached in pyc file (so next time it’s not needed to translate it into bytecode and the program runs faster). afterwards the Python Virtual Machine executes the bytecode. note that this bytecode is only understandable for the Python VM, not the machine.

With that being said how can see what the bytecode looks like. dis module can help us here.

1
2
3
4
5
6
import dis

def main():
    c = 1

dis.dis(main)

Output

121           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (c)
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

Let’s get into Namespaces.

Local

Defining a function creates a local namespace. the variables that defined in the function are only accessible within it and not outside.

1
2
def main():
    c = 1

c is defined in the function and is only accessible in that namespace.

If you try to access c outside the function you’ll get a NameError error. the following code throws that error.

1
2
3
4
def main():
    c = 1

print(c)

Output

1
NameError: name 'c' is not defined` error

Consider the following code

1
2
3
4
5
6
c = [1]

def main():
    print(c)

main()

The bytecode of the main function

 98           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (c)
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

LOAD_GLOBAL loads the variable from the global namespace.

LOAD_GLOBAL(namei)
Loads the global named co_names[namei>>1] onto the stack.

So the interpreter looks for that variable in the global namespace.

To see what names are available in the local namespace you can check output of locals() and globals() for global namespace.

1
2
3
4
5
6
7
8
9
c = [1]

def main():
    a = 1
    assert 'a' in locals()
    assert 'c' in globals()
    print(c)

main()

Let’s modify the variable

1
2
3
4
5
6
7
c = [1]

def main():
    c.append(2)
    print(c)

main()

Output

[1, 2]

The same flow happens when a variable is modified.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 98           0 LOAD_GLOBAL              0 (c)
              2 LOAD_METHOD              1 (append)
              4 LOAD_CONST               1 (1)
              6 CALL_METHOD              1
              8 POP_TOP

 99          10 LOAD_GLOBAL              2 (print)
             12 LOAD_GLOBAL              0 (c)
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Before continuing the article I’d like talk about mutable and immutable variables in Python.

Mutable and Immutable variables

Mutable variables are mutable and can be modified however immutable variables can not be modified. if you modify them, a new variable will be created.

Mutable objects can change their value but keep their id(). #

An object with a fixed value. Immutable objects include numbers, strings and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored. They play an important role in places where a constant hash value is needed, for example as a key in a dictionary. #

Mutable types include dictionary, list, set and user-defined classes

Immutable types include numbers, strings and tuples

id() returns the memory address of an object (In CPython). take the following code

1
2
3
4
5
c = 1
id(c)

c += 1  # or c = c + 1
id(c)

If you run the code, you’ll see different values because changing an immutable objects, produces a new object. but for mutable objects it’s not the case

1
2
3
4
5
c = {'a': 'b'}
print(id(c))

c['d'] = 'f'
print(id(c))

This time the values are same because changing the object didn’t produce a new object.

In Python we have the concept of assignment. unlike other languages, this statement c = 1 is read by Python as point variable c to the memory address that holds value 1

1
2
3
4
a = "randomvalue12345678"
b = "randomvalue12345678"

print(id(a), id(b))

You’ll get same output. let’s see another example

1
2
3
4
a = 898989
b = a
a = 1
print(b) 

We can read b = a as point variable b to the memory address that variable a points to. so b points to THE memory address that a points to. so now b points to that address and it doesn’t have any business with a anymore. in the next line a = 1, a points to another memory address.

Output

898989

Another one

1
2
3
4
a = [1]
b = a
a.append(2)
print(b)

Output

[1, 2]

The same rules apply when a function changes its arguments. if the functions changes an argument that is mutable, the memory gets changed so outside the function, it has the change. if it’s immutable, changing it inside the function, doesn’t change the value that memory address holds,

1
2
3
4
5
6
7
8
c = []

def test(data):
    data.append(1)

test(c)

print(c)

Output

[1]

UnboundLocalError

Let’s use another type for the variable and modify it

1
2
3
4
5
6
7
c = 1

def main():
    c = c + 1
    print(c)

main()

Output

UnboundLocalError: local variable 'c' referenced before assignment

What happened? let’s check the bytecode

 98           0 LOAD_FAST                0 (c)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_FAST               0 (c)

 99           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                0 (c)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

This time it’s LOAD_FAST which loads variable from the local namespace.

LOAD_FAST(var_num)
Pushes a reference to the local co_varnames[var_num] onto the stack.

When the varible is assigned inside the function by c = c + 1, the Python VM reads the right side of the assignment operator first, it load c, add it to 1 and store it in the local variable c. so c is a local variable but it’s not defined in the local scope, and when line is getting executed there’s no value for c, there you get the error!

In the previous code, the variable type was a list, we used append to modify it. the other way would be c = c + [1] this code procudes the same result, because we know that this expression runs the __iadd__ magic method of the list object and this is not assignment. however for example this code c = c + 1 a new address in the memory is used for the variable and it’s not the old one.

To fix it we need to instruct the interpreter to load the variable from the global scope.

1
2
3
4
5
6
7
8
9
c = 1

def main():
    global c

    c = c + 1
    print(c)

main()

Let’s look at the byecode

 99           0 LOAD_GLOBAL              0 (c)
              2 LOAD_CONST               1 (1)
              4 INPLACE_ADD
              6 STORE_GLOBAL             0 (c)

100           8 LOAD_GLOBAL              1 (print)
             10 LOAD_GLOBAL              0 (c)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

It’s more like it!

Nested

Nested namespace is available for nested functions. the local namespace of the outer function is the nested namespace for the inner function.

1
2
3
4
5
6
7
8
9
def outer():
    c = 1

    def inner():
        print(c)
    
    inner()

outer()

The same issue that we had for assigning we have here but to fix it we need to use nonlocal keyword

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def outer():
    c = 1

    def inner():
        nonlocal c
    
        c += 1
        print(c)

    inner()

outer()

Global

The scope outside functions is global. you can see what variables are defined by looking at the globals() output.

Built-in

It includes the built-in functions exceptions and objects. check globals()['__builtins__'] or dir(__builtins__)