Skip to content

Namespace Internals

Understanding Python's namespace system and the Global Interpreter Lock.

Namespace Access

globals() Function

Access and modify the global namespace:

# View global namespace
print(globals().keys())

# Add to global namespace
globals()['dynamic_var'] = 42
print(dynamic_var)  # 42

# Dynamic variable creation
name = 'my_variable'
globals()[name] = 100
print(my_variable)  # 100

locals() Function

View the local namespace (read-only in functions):

def function():
    x = 10
    y = 20

    # View locals
    print(locals())  # {'x': 10, 'y': 20}

    # CANNOT modify locals this way
    locals()['z'] = 30
    # z is NOT defined

# At module level, locals() == globals()
print(locals() is globals())  # True (at module level)

vars() Function

Access object's __dict__:

class MyClass:
    def __init__(self):
        self.x = 10
        self.y = 20

obj = MyClass()
print(vars(obj))  # {'x': 10, 'y': 20}

# Modify via vars
vars(obj)['z'] = 30
print(obj.z)  # 30

Namespace Manipulation

Dynamic Attribute Access

# setattr/getattr
class Config:
    pass

config = Config()
setattr(config, 'debug', True)
print(getattr(config, 'debug'))  # True
print(getattr(config, 'verbose', False))  # False (default)

# Check existence
print(hasattr(config, 'debug'))  # True

Dynamic Module Imports

import importlib

# Import module by name
module_name = 'json'
module = importlib.import_module(module_name)

# Import from package
submodule = importlib.import_module('os.path')

Exec and Eval

# Execute code string
code = """
x = 10
y = 20
result = x + y
"""
namespace = {}
exec(code, namespace)
print(namespace['result'])  # 30

# Evaluate expression
result = eval('2 + 3 * 4')
print(result)  # 14

# With custom namespace
namespace = {'x': 10, 'y': 20}
result = eval('x + y', namespace)
print(result)  # 30

Warning: exec and eval are security risks with untrusted input.


Global Interpreter Lock (GIL)

What is the GIL?

The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time.

import threading

counter = 0

def increment():
    global counter
    for _ in range(1000000):
        counter += 1

# Even with threads, they don't run truly parallel
threads = [threading.Thread(target=increment) for _ in range(2)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# Result may not be 2000000 due to race conditions
# (counter += 1 is not atomic at bytecode level)

Thread Safety

Most Python operations are thread-safe due to the GIL:

# These are atomic (thread-safe)
x = 42                    # Simple assignment
y = x                     # Simple read
lst.append(item)          # Some built-in operations

# These are NOT atomic
counter += 1              # Read-modify-write
lst[0] = lst[1] + 1       # Multiple operations

Working Around the GIL

For CPU-bound tasks, use multiprocessing:

from multiprocessing import Pool

def cpu_intensive(n):
    return sum(i * i for i in range(n))

# Uses multiple processes (bypasses GIL)
with Pool(4) as pool:
    results = pool.map(cpu_intensive, [1000000] * 4)

For I/O-bound tasks, threading works well:

import threading
import requests

def fetch_url(url):
    return requests.get(url).text

# GIL released during I/O operations
urls = ['http://example.com'] * 10
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]

For async I/O:

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

Memory and Namespace Interaction

Reference Counting

import sys

x = [1, 2, 3]
print(sys.getrefcount(x))  # Reference count (includes temp ref from getrefcount)

y = x  # Another reference
print(sys.getrefcount(x))  # Count increased

del y  # Remove reference
print(sys.getrefcount(x))  # Count decreased

Garbage Collection

import gc

# Force garbage collection
gc.collect()

# Check for circular references
gc.set_debug(gc.DEBUG_LEAK)

# Disable GC temporarily (for performance)
gc.disable()
# ... performance critical code ...
gc.enable()

Summary

Function Purpose Modifiable
globals() Global namespace Yes
locals() Local namespace No (in functions)
vars(obj) Object's __dict__ Yes
dir(obj) List attributes N/A

GIL implications: - Only one thread executes Python bytecode at a time - I/O-bound: Use threading (GIL released during I/O) - CPU-bound: Use multiprocessing (separate processes) - Most simple operations are thread-safe - Compound operations need explicit locking

Key points: - Use globals() for dynamic global variables - locals() is read-only in functions - GIL affects CPU-bound parallelism - Use multiprocessing for CPU-bound tasks - Use threading/asyncio for I/O-bound tasks