While reading Python’s Execution model documentation, I realized that Python’s free variables do not seem to have a strict late binding property where a name binding occurring in any code block can be used for name resolution. Indeed, executing:
def f(): return x def g(): x = 0 return f() print(g())
raises:
NameError: name 'x' is not defined
They have rather a loose late binding property where only a name binding occurring in an outer code block of the code block introducing the free variable can be used for name resolution. Indeed executing
def f(): return x x = 0 print(f())
prints:
0
What are the benefits and drawbacks of the loose late binding property compared to the strict late binding property?
Advertisement
Answer
This is generally known as dynamic scoping and static scoping. Roughly speaking, dynamic scoping determines scope by call nesting and static scoping determines scope by declaration nesting.
In general, dynamic scoping is very easy to implement for any language with a call stack – a name lookup simply searches the current stack linearly. In contrast, static scoping is more complex, requiring several distinct scopes with their own lifetime.
However, static scoping is generally easier to understand, since the scope of a variable never changes – a name lookup has to be resolved once and will always point to the same scope. In contrast, dynamic scoping is more brittle, with names being resolved in different or no scope when calling a function.
Python’s scoping rules are mainly defined by PEP 227 introducing nested scoping (“closures”) and PEP 3104 introducing writable nested scoping (nonlocal
). The primary use-case of such static scoping is to allow higher-order functions (“function-producing-function”) to automatically parameterise inner functions; this is commonly used for callbacks, decorators or factory-functions.
def adder(base=0): # factory function returns a new, parameterised function def add(x): return base + x # inner function is implicitly parameterised by base return add
Both PEPs codify how Python handles the complications of static scoping. In specific, scope is resolved once at compile time – every name is thereafter strictly either global, nonlocal or local. In return, static scoping allows to optimise variable access – variables are read either from a fast array of locals, an indirecting array of closure cells, or a slow global dictionary.
An artefact of this statically scoped name resolution is UnboundLocalError
: a name may be scoped locally but not yet assigned locally. Even though there is some value assigned to the name somewhere, static scoping forbids accessing it.
>>> some_name = 42 >>> def ask(): ... print("the answer is", some_name) ... some_name = 13 ... >>> ask() UnboundLocalError: local variable 'some_name' referenced before assignment
Various means exist to circumvent this, but they all come down to the programmer having to explicitly define how to resolve a name.
While Python does not natively implement dynamic scoping, it can in be easily emulated. Since dynamic scoping is identical to a stack of scopes for each stack of calls, this can be implemented explicitly.
Python natively provides threading.local
to contextualise a variable to each call stack. Similarly, contextvars
allows to explicitly contextualise a variable – this is useful for e.g. async
code which sidesteps the regular call stack. A naive dynamic scope for threads can be built as a literal scope stack that is thread local:
import contextlib import threading class DynamicScope(threading.local): # instance data is local to each thread """Dynamic scope that supports assignment via a context manager""" def __init__(self): super().__setattr__('_scopes', []) # keep stack of scopes @contextlib.contextmanager # a context enforces pairs of set/unset operations def assign(self, **names): self._scopes.append(names) # push new assignments to stack yield self # suspend to allow calling other functions self._scopes.pop() # clear new assignments from stack def __getattr__(self, item): for sub_scope in reversed(self._scopes): # linearly search through scopes try: return sub_scope[item] except KeyError: pass raise NameError(f"name {item!r} not dynamically defined") def __setattr__(self, key, value): raise TypeError(f'{self.__class__.__name__!r} does not support assignment')
This allows to globally define a dynamic scope, to which a name can be assign
ed for a restricted duration. Assigned names are automatically visible in called functions.
scope = DynamicScope() def print_answer(): print(scope.answer) # read from scope and hope something is assigned def guess_answer(): # assign to scope before calling function that uses the scope with scope.assign(answer=42): print_answer() with scope.assign(answer=13): print_answer() # 13 guess_answer() # 42 print_answer() # 13 print_answer() # NameError: name 'answer' not dynamically defined