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:
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:
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:
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:
def print_locals():
x = 2
name = "Erin"
print(locals())
print_locals()
{'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:
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:
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:
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:
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:
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():
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):
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:
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
- Write a function that assigns a variable inside it, then confirm from outside the function that the variable doesn’t exist there.
- Use the
globalkeyword to write a function that increments a counter defined outside it. - 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
globalkeyword. - 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.