Python Scopes, Closures and the LEGB Rule

E.Y.
4 min readJan 10, 2021
Photo by miram Oh on Unsplash

If you are like me who have a few months experience of Python, then you might be in the same position as me — know something about Python, but not enough. So I spent sometime to gather bits and pieces of scattered knowledge to dig deeper into Python. In this blog, the piece will be scopes and closures.

In computer programming, the scope of a name binding — an association of a name to an entity, such as a variable — is the part of a program where the name binding is valid, that is where the name can be used to refer to the entity. In other parts of the program the name may refer to a different entity (it may have a different binding), or to nothing at all (it may be unbound). — wiki

In programming languages, a closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope. — wiki

Sounds confusing? Let’s start with scope. Think of it as the scope is like a Russian Doll where variables are stored and from which they are referenced. In example below,

  • the variable b is the outer most doll, and
  • a — sitting in the scope of function outer , is the middle doll, and
  • c , the innermost doll in the scope of inner
>>> b= 1
>>> def outer():
... a= 3
... print("outer: b=",b)
... def inner():
... c = a + b
... print("inner: b=",b)
... print("inner: a=",a)
... print("inner: c=",c)
... return inner()
============================
>>> outer()
outer: b= 3
inner: b= 3
inner: a= 1
inner: c= 4

As you can see variables declared inside another function are hidden from the outer scopes but variables within an outer scope is accessible within the inner scope, just as b is accessible from both inner and outer and a is accessible from its nested scope inner .

Note that the fact that c can access variables that are defined outside its own scope, i.e., b and a is what closure is — accessing variables that are defined outside the current scope.

Let’s refactor the code to make it clearer:

>>> a = 1
>>> def outer(d):
... b = 3
... def inner():
... c = a + b + d
... print(c)
... return inner
>>> inner_value = outer(2)
>>> inner()
=========================
6

You may guess that after outer function is executed, the variable b will be garbage-collected, but it seems that the inner scope of`outer() is still accessible to inner(),aka. inner() has a lexical closure over the inner scope of outer(), which is alive and to be referenced by inner() .

But let’s say the global scope and outer scope has a variable with the same identifier. For an inner scope that wants to reference this variable, which one will be use?

>>> a = 1
>>> def outer():
... a = 3
... def inner():
... c = a * 2
... print(c)
... return inner
>>> inner_value = outer()
>>> inner()
=========================
6 # a = 3

So how is the order of the scope decided?

Python resolves names using the so-called LEGB rule, which stands for Local, Enclosing, Global, and Built-in:

python LEGB
  • Local (or function) scope is the code block or body of any Python function or lambda expression.
  • Enclosing (or nonlocal) scope is a scope that only exists for nested functions.
  • Global (or module) scope is the outer-most scope in Python script, or module. Internally, the compiler turns main script into a module called __main__ for the main program’s execution. The namespace of this module is the main global scope of your program.
  • Built-in scope is a special scope that’s automatically loaded by Python whenever you run a script or open an interactive session. It’s implemented as a standard library module named builtins in Python 3.x. All of Python’s built-in objects live in this module, e.g. >>> dir(__builtins__)
    ['ArithmeticError', 'AssertionError',..., 'tuple', 'type', 'vars', 'zip']

So when a variable is referenced, the compiler first searches within the innermost scope, it then goes one level up one by one, until reach out to the outermost scope. It will stops when it finds the first match. So the problem can arise when the identifier in the inner scope “block”s that of the outer scope, and this is called “shadowing” .

That’s pretty much of it!

Happy Reading!

--

--