While preparing my talk for PyCascades 2023 on this very blog post series of Python's syntactic sugar, I had an inkling that I could unravel the
global statement. After talking to some folks after my talk, I realized that I could, in fact, unravel it! The trick was realizing what made globals (and built-ins) different from locals.
nonlocal and closures, Python had a relatively simple set of scoping rules that grouped everything into 3 namespaces (i.e. groupings of names, which is a somewhat technical name for "variables"):
- Any name created in a block (i.e.
def), unless specified by a
globalstatement, was local
- Anything at the top of a module or named in a
globalstatement was global
builtinsmodule contained everything built-in
builtins module was introduced, but that's just historical context.
__builtins__, it's actually an implementaiton detail of CPython, so I'm leaving it out of this discussion.
This was known at the LGB rule ( Local, Global, Built-ins). To make the rest of this blog post easier to follow, assume that when I say "local" I am including
nonlocal and closures are just fancy locals (you can read the actual scoping rules if you want the full details).
There is one key thing to notice about my outline of the LGB rule that makes locals unique compared to globals and built-ins: they must be created in the block where they reside. What that means is a local always comes into existence thanks to an assignment, which makes
:= very obvious syntax to signal what is a local name (and it's a piece of syntax I don't think we can get rid of). Since we can look at an entire file's contents, we can also deduce what all the local names are with complete confidence and consider them taken care of by
= (this is actually how Python itself decides what's local and what isn't).
Thus any name we come across which isn't a local is either a global or built-in name. Since you can't assign to the built-in namespace directly, we can disambiguate between globals and built-ins as by assignment; anything that's assigned to that isn't to a local is implicitly a global name. It's also important to note that all the
global statement is doing is instructing Python to explicitly treat a name as a global instead of as a local when it comes to assignment. So if we can unravel assigning to a global name then we are done!
Unravelling global assignment
A very important tool we are going to use for this unvravelling is the
globals() built-in function. What makes this such an important function for what we want to accomplish is that it "return[s] the dictionary implementing the current module namespace." Getting to treat the global namespace as a dictionary means that assigning to a global can be treated just like assigning to a dictionary key! That makes a direct unravelling of
A = 42 be
globals()["A"] = 42. But since we already unravelled subscription, we can unravel all the way down to just function calls:
getattr(dict, "__setitem__")(globals(), "A", 42).
Unravelling the reading of a global name
But it turns out we can push things a bit farther and even unravel reading a global name (although this isn't really syntactic, so this is just an academic exercise)! Things get a little tricky when you try to read from a global name thanks to us having no syntactic way to tell a global name from a built-in name like we can for assignment. But since we have a distinct way to get both the globals and built-in namspaces via
globals() and the
builtins module, respectively, it's straightforward to write code which looks things up appropriately. One way to do that in a single line would be
globals()["A"] if "A" in globals() else builtins.A.
One little detail we do need to make sure to take care of, though, is to raise
NameError if the name doesn't exist anywhere. So our one-liner is a bit too simplistic. Luckily, the full unravelling isn't tricky if we try to read a name of