Python Programming from First Principles: Learn to Code, Think Computationally, and Solve Real-World Problems
PDFEPUB
BOOK 000 · CHAPTER 5 · Scope and Decorators

Scope and Decorators

On this pageUnderstanding Variable ScopeThe global KeywordThe nonlocal Keyword and Nested FunctionsFunction DecoratorsDecorators with Arguments

As your programs grow larger, understanding where and how variables exist becomes important.

This idea is known as scope — the part of a program where a variable can be seen or used.

Once you understand scope, we’ll move to decorators, a feature that lets you extend or modify the behavior of functions without changing their code.

Understanding Variable Scope

A namespace maps names to objects. Scope determines where Python looks for a name.

We begin with local and global scope, then examine an enclosing function’s scope: - Local scope: variables created inside a function, available only there. - Global scope: variables created outside any function, available throughout the file.

Let’s see a simple example:

message = "Hello from the global scope"

def greet():
    local_message = "Hello from inside the function"
    print(local_message)    # accessible only inside the function
    print(message)          # accessing the global variable

greet()
print(message)  # accessible anywhere
Listing 213 — Understanding Variable Scope

Output

Hello from inside the function
Hello from the global scope
Hello from the global scope

If you try to print local_message outside the function, Python will raise an error because it only exists within greet().

The global Keyword

Sometimes, you may want to modify a global variable inside a function.

To do this, you must declare it as global:

count = 0

def increase_count():
    global count
    count += 1
    print("Inside function:", count)

increase_count()
print("Outside function:", count)
Listing 214 — The global Keyword

Output

Inside function: 1
Outside function: 1

Without the global keyword, Python would assume count is a new local variable and raise an error.

The nonlocal Keyword and Nested Functions

A function can also contain another function.

An inner function can read names from an enclosing function without nonlocal. Use nonlocal when the inner function needs to assign to an existing name in that enclosing function.

def outer():
    x = "outer value"

    def inner():
        nonlocal x
        x = "inner value"
        print("Inside inner:", x)

    inner()
    print("Inside outer:", x)

outer()
Listing 215 — The nonlocal Keyword and Nested Functions

Output

Inside inner: inner value
Inside outer: inner value

Here,nonlocal allows the inner function to modify a variable that belongs to the enclosing function.

Function Decorators

Now that you know about scope, decorators will make more sense.

A decorator is a function that takes another function as input, adds some extra behavior, and returns it. They are often used for logging, access control, timing, or formatting output.

Let’s start with a simple example.

def simple_decorator(func):
    def wrapper():
        print("Before the function runs")
        func()
        print("After the function runs")
    return wrapper
Listing 216 — Function Decorators

You can apply the decorator to another function using the @ symbol:

@simple_decorator
def greet():
    print("Hello, Python!")

greet()
Listing 217 — Function Decorators

Output

Before the function runs
Hello, Python!
After the function runs

Here’s what happens:

  • The @simple_decorator line tells Python to pass the function greet to simple_decorator and replace greet with the returned wrapper.
  • The wrapper() function adds code before and after the original function.
  • When greet() is called, it runs inside the decorator.

Decorators with Arguments

You can make decorators more flexible by allowing them to work with any function arguments using *args and **kwargs.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Running {func.__name__} with arguments {args} {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} finished with result {result}")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

add(3, 5)
Listing 218 — Decorators with Arguments

Output

Running add with arguments (3, 5) {}
add finished with result 8

This pattern is often used in real-world applications — for example, in web frameworks, decorators can check user permissions, handle errors, or measure performance.