Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Nested Functions and Nonlocal Scope

Recommended resources

Hello! Welcome back to our course on advanced Python.

In our previous lesson, we established that functions in Python are first-class objects. This means they can be assigned to variables, stored in data structures, and passed as arguments to other functions. This flexibility is the foundation for many powerful Python features.

Today, we'll build directly on that foundation by exploring nested functions—functions that are defined inside other functions. Our main focus will be on how these inner functions can access variables from their surrounding, or "enclosing," scope. This concept is a crucial stepping stone toward understanding closures and decorators, which are central to your goal of mastering idiomatic Python code.

By the end of this lesson, you will be able to define nested functions and describe how they access variables from their enclosing (nonlocal) scope.

1. What Are Nested Functions?

A nested function (or inner function) is simply a function defined inside another function.

def outer_function():
    print("This is the outer function.")

    def inner_function():
        print("This is the inner function.")

    # The inner function is called from within the outer function
    inner_function()

outer_function()
# Output:
# This is the outer function.
# This is the inner function.

At first glance, this might seem like a simple organizational tool. One of its primary benefits is encapsulation. The inner_function is local to outer_function and cannot be called from outside of it. This is analogous to a private helper method in a Java class, hiding implementation details from the outside world.

To see a clear example of how inner functions can access variables from their parent function, let's start with a short reading.

Python Inner Functions: What Are They Good For?

The article 'Python Inner Functions: What Are They Good For?' from Real Python provides a great introduction to the syntax and a key feature of nested functions: accessing the enclosing scope.

Please read the section 'Creating Python Inner Functions'. Pay attention to how inner_func accesses the who variable from outer_func.

As you saw, the inner function has access to the variables and parameters of the outer function. But how does Python manage this? To understand that, we need to talk about scope.


2. How Python Finds Variables: The LEGB Rule

When you use a variable name in your code, Python has a set of rules to find what that name refers to. This is known as the LEGB rule, which stands for Local, Enclosing, Global, and Built-in. Python searches for a name in this specific order.

This diagram illustrates the nested structure of Python's scopes. The `nonlocal` scope is the "Enclosing" scope we are focusing on today.

To get a formal definition of these four scopes, please read the following section from Real Python.

Python Scope and the LEGB Rule: Resolving Names in ...

The article 'Python Scope and the LEGB Rule' clearly defines the four scope levels.

Read the sections 'Resolving Names in Python: The LEGB Rule' and 'The Enclosing Scope'. Focus on the definitions of each scope level, especially the Enclosing scope, which is new to us.

The key takeaway is the Enclosing scope. It's the scope of the outer function, and it's what allows a nested function to see the variables around it.


3. A Deeper Look: Compile-Time vs. Runtime Scoping

A common point of confusion is when Python decides which scope a variable belongs to. Does it happen when the function is defined or when it's run? The answer is a bit of both, and understanding this distinction is key to mastering Python's scope rules. Given your background in informatics, you'll likely appreciate this more formal explanation.

The following video from the mCoding channel provides an excellent, detailed breakdown.

Functions within functions, closures, and variable scopes in Python

This video explains the crucial rule that where Python looks for a variable is determined at compile time, but the value is retrieved at runtime.

Watch from 02:11 to 06:52. The video will first state the rule (compile-time vs. runtime) and then walk through several examples that demonstrate how Python determines if a variable is local, global, or from an enclosing scope.

This rule explains why an inner function can access a variable in the enclosing scope even if that variable is defined after the inner function itself. The compiler scans the entire outer function and notes that the variable is part of the enclosing scope. The value is then looked up when the inner function is actually executed.


4. Modifying Variables in the Enclosing Scope

You've seen that an inner function can read a variable from the enclosing scope. But what happens if you try to change it?

This image shows a simple case of an inner function reading the `text` variable from its outer function's scope.

Consider this code:

def outer():
    message = "original message"

    def inner():
        # We want to change the outer 'message' variable
        message = "new message" 
        print(f"Inner: {message}")

    inner()
    print(f"Outer: {message}")

outer()

What do you expect the output to be? You might think it would be:
Inner: new message
Outer: new message

However, the actual output is:
Inner: new message
Outer: original message

Why? Because the line message = "new message" inside inner() creates a new local variable named message that is only visible within the inner function. It shadows the variable from the outer scope but does not change it.

To explicitly tell Python that you want to modify a variable in the enclosing (but not global) scope, you must use the nonlocal keyword.

Python Scope and the LEGB Rule: Resolving Names in ...

Let's return to the Real Python article on scope to see how the nonlocal statement works.

Read the section titled 'The nonlocal Statement'. It provides a clear explanation and examples of how to use nonlocal to modify variables in the enclosing scope.

With nonlocal, our previous example now works as intended:

def outer():
    message = "original message"

    def inner():
        nonlocal message  # Declare that we are modifying the enclosing scope's 'message'
        message = "new message"
        print(f"Inner: {message}")

    inner()
    print(f"Outer: {message}")

outer()
# Output:
# Inner: new message
# Outer: new message

The nonlocal keyword is the bridge that allows an inner function to write back to its parent's state.


5. Practice Exercise

Let's apply these concepts. Your task is to create a function that generates personalized greetings.

  1. Define an outer function create_greeter(greeting_word). This function will take a string like "Hello" or "Hi" as an argument.
  2. Inside create_greeter, define a variable greeting that stores the initial greeting_word.
  3. Inside create_greeter, define a nested function greet(name) that takes a person's name and prints a formatted greeting (e.g., "Hello, Alex"). This function will need to access the greeting variable from its enclosing scope.
  4. Inside create_greeter, define another nested function update_greeting(new_greeting_word) that uses the nonlocal keyword to change the greeting variable.
  5. In the body of create_greeter, call greet("World") to test the initial greeting. Then, call update_greeting("Howdy"). Finally, call greet("World") again to show that the greeting has changed.
  6. Call your top-level create_greeter function with "Hello" to run the test.
Click here to see a possible solution
def create_greeter(greeting_word):
    """
    Creates and tests a set of greeting functions.
    """
    # This variable is in the enclosing scope for the functions below
    greeting = greeting_word

    def greet(name):
        """Prints a greeting using the current greeting word."""
        print(f"{greeting}, {name}")

    def update_greeting(new_greeting_word):
        """Updates the greeting word in the enclosing scope."""
        nonlocal greeting
        greeting = new_greeting_word
        print(f"(Greeting word updated to '{greeting}')")

    # --- Test sequence ---
    print("--- Initial Test ---")
    greet("World")  # Should use the initial greeting_word

    print("\n--- Updating Greeting ---")
    update_greeting("Howdy")

    print("\n--- Final Test ---")
    greet("World")  # Should use the new, updated greeting

# Run the main function
create_greeter("Hello")

# Expected Output:
# --- Initial Test ---
# Hello, World
#
# --- Updating Greeting ---
# (Greeting word updated to 'Howdy')
#
# --- Final Test ---
# Howdy, World

Conclusion

In this lesson, we've delved into the world of nested functions and their relationship with scope. This is a fundamental concept that distinguishes Python from languages like Java and is essential for writing clean, modular, and powerful Python code.

Here are the key takeaways:

  • Nested functions are functions defined inside other functions, providing a way to encapsulate logic.
  • Python resolves variable names using the LEGB rule (Local, Enclosing, Global, Built-in).
  • The enclosing scope allows an inner function to read variables from its outer function.
  • The nonlocal keyword is required for an inner function to modify a variable in its enclosing scope.

We've seen how an inner function operates while its outer function is still executing. In our next lesson, we will explore a fascinating question: what happens when the outer function finishes, but the inner function lives on? This will lead us directly to closures, a powerful pattern where an inner function "remembers" its enclosing scope.