Python: Scope

Scope is the term used to define the location(s) in which Python searches for a name to object mapping (e.g a variable).

As described in this StackOverflow post, Python uses the LEGB Rule to locate a definition. LEGB stands for:

  • L, Local — Names assigned in any way within a function ( def or lambda)), and not declared global in that function.
  • E, Enclosing-function locals — Name in the local scope of any and all statically enclosing functions ( def or lambda), from inner to outer.
  • G, Global (module) — Names assigned at the top-level of a module file, or by executing a global statement in a def within the file.
  • B, Built-in (Python) — Names preassigned in the built-in names module open,range,SyntaxError,...

In a nutshell, Python will first look at the local scope for a name to object mapping (e.g people = 5). If it cannot find one, it will continue going up the hierarchy until it finds one. If it doesn’t find a mapping, it will raise an exception.

To shed some more light on this let’s take a step back and analyse each of the points listed above. I’ll do so in reverse order because that is the way we write Python code, as you’ll see in a moment.

Built-in

As the name suggests, Built-in refer to mappings which are built into Python. Because they’re built-in, we don’t have to do anything but call the name in order to use it.

Here’s an example of using the  range() built-in:

Global

Let’s now add a global name called range and see what happens:

We can see that the global name overrode the built-in name given that the global string –  this is a global "range" – was printed. Looking at the LEGB rule tells us that this is expected behavior given that the global scope has a higher precedence than the built-in scope.

Let’s just double check that we’re right though. Let’s move the built-in below the global and see what happens:

We encounter an error when the interpreter reaches the  print range(5) line. This is because the  range()  built-in function no longer exists because it has been replaced by our global  range string.

Side note: This is known as Shadowing. I’ll expand on Shadowing in another post but suffice to say that you should never shadow built-ins otherwise you’ll run into issues such as this. Also note that built-ins are available in all namespaces, not just global.

Enclosing

The below code has been expanded to include an enclosing scope example:

What I’ve done here is enclosed the enclosing() function inside of the main() function. This is also known as nesting.

Note that the enclosing() function does not have a definition for range in its scope and therefore it needs to go up the hierarchy to see if one exists there. Let’s see what it finds:

Because main() is the next level above enclosing() in the hierarchy, main()’s   range mapping is used.

But what would happen if   main() didn’t have a mapping for range? As per the code below,  enclosing() would have no choice but to look higher in the hierarchy for a mapping:

This would result in enclosing() printing the global mapping instead, like so:

Local

Local, as the name suggests, is a name to object mapping that is local to the scope. In other words, Python does not need to search the hierarchy for the mapping, it is locally defined like so:

This code results in the following output:

Note that the enclosing definition is no longer used because, as per the order of operation listed at the top of this post, local scope has precedence over an enclosing scope.

Knowledge Base

See the Coding section of my Knowledge Base for more information.
As always, if you have any questions or have a topic that you would like me to discuss, please feel free to post a comment at the bottom of this blog entry, e-mail at will@oznetnerd.com, or drop me a message on Twitter (@OzNetNerd).

Note: This website is my personal blog. The opinions expressed in this blog are my own and not those of my employer.

Leave a Reply

Your email address will not be published. Required fields are marked *