CodingNic

Functions

Function Scope

Functions 25 min read

Function Scope

Objectives

By the end of this chapter, you should be able to:

  • Define what scope is
  • Explain the difference between global and local variables
  • Document functions with docstrings and type hints

💡 Why this matters: Not understanding scope is one of the most common sources of confusing bugs for new Python developers. Knowing what a function can and can’t see saves you from chasing errors that don’t make sense yet.

Function Scope

A variable created inside a function only exists inside that function:

python
def func():
    x = 5
    return x

func()  # 5
x       # NameError, x doesn't exist out here

The global Keyword

Variables defined outside any function live in the global scope. If you try to modify one from inside a function without saying so explicitly, Python raises an error. That’s because assigning to a name anywhere inside a function makes Python treat it as a new local variable, not the global one:

python
id = 0

def increment_id():
    id += 1

increment_id()  # UnboundLocalError: local variable 'id' referenced before assignment

The global keyword tells Python you mean the global variable:

python
def increment_id():
    global id
    id += 1

increment_id()  # the global id is now 1

Relying heavily on global variables is generally considered poor practice. It makes code harder to reason about, since any function can silently change shared state.

Inspecting Scope: locals() and globals()

Python can show you every variable currently in scope. locals() returns a dictionary of everything local to the current function:

python
def print_locals():
    x = 2
    name = "Erin"
    print(locals())

print_locals()
text
{'x': 2, 'name': 'Erin'}

globals() does the same for the module-level (global) scope. It’s less useful to print in full since it includes everything defined at the top of the file, but it’s handy for a quick check like this:

python
city = "Nairobi"
print("city" in globals())  # True

Nested Functions and Closures

Python supports closures: an inner function can access variables from the function that contains it, even after that outer function has finished running:

python
def outer(a):
    def inner(b):
        return a + b
    return inner

outer(3)(4)  # 7
x = outer(2)
x(10)        # 12

Python’s closures have a real limitation, though: they can read an outer variable, but reassigning it from inside the inner function breaks, for the same reason as the global example above:

python
def counter():
    x = 0
    def increment():
        x += 1
        print(x)
    return increment

counter()()  # UnboundLocalError: local variable 'x' referenced before assignment

A common workaround is to store the value as an attribute on the inner function itself, rather than trying to reassign a variable from the outer scope:

python
def outer_count():
    def inner_count():
        inner_count.x += 1
        print(inner_count.x)
    inner_count.x = 0
    return inner_count

Documenting Functions with Docstrings

A docstring is a string placed right inside a function, describing what it does:

python
def say_hello():
    """This function returns the string hello when called"""
    return "hello"

Triple quotes let a docstring span multiple lines if needed. You can access it directly, or view it with help():

python
say_hello()             # "hello"
say_hello.__doc__       # "This function returns the string hello when called"
help(say_hello)         # shows the docstring, with extra formatting

Docstrings are worth writing for any function whose purpose isn’t immediately obvious from its name. Think of them as a built-in, structured comment.

Type Hints

Python is dynamically typed, so it never requires you to declare a variable’s type, but you can optionally hint at expected argument and return types, purely for documentation (Python doesn’t enforce them):

python
def add(a: int, b: int) -> int:
    """This function returns the sum of two numbers"""
    return a + b

Type hints combine naturally with default values too:

python
def add(a: int = 5, b: int = 5) -> int:
    """This function returns the sum of two numbers, defaulting both to 5"""
    return a + b

Try It

  1. Write a function that assigns a variable inside it, then confirm from outside the function that the variable doesn’t exist there.
  2. Use the global keyword to write a function that increments a counter defined outside it.
  3. Add a docstring and type hints to a function you’ve already written in this module, then check it with help().

Recap

  • Variables created inside a function are local to it; accessing them from outside raises NameError.
  • Modifying a global variable from inside a function requires the global keyword.
  • Docstrings ("""...""" inside a function) document what it does; type hints (def f(a: int) -> int:) document expected types without enforcing them.

Next lesson: recursion, functions that call themselves.