Scope

Scope decides where a name can be seen. Variables created inside a function are local to it; variables at the top of a file are global to that module. Python looks names up using the LEGB rule, and the global and nonlocal keywords let you change variables in outer scopes when you really need to.

Local and global

PY local_global.py
greeting = "Hello"

def welcome(name):
    message = f"{greeting}, {name}!"
    return message

print(welcome("Ana"))
print(greeting)
print("message" in globals())
Output
Hello, Ana!
Hello
False

message exists only while welcome runs — it's local. greeting is global (defined at module level) and can be read inside the function.

The LEGB rule

When Python meets a name, it searches four scopes in order and uses the first match:

  1. Local — inside the current function.
  2. Enclosing — inside any functions that contain the current one.
  3. Global — the module's top level.
  4. Built-in — names like print, len, range.
PY legb.py
x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print("inner sees:", x)
    inner()
    print("outer sees:", x)

outer()
print("module sees:", x)
print(len("built-in lookup"))
Output
inner sees: local
outer sees: enclosing
module sees: global
15

Assigning makes a name local

If a function assigns to a name anywhere in its body, Python treats that name as local for the whole function — even lines before the assignment:

PY unbound.py
counter = 0

def increment():
    counter += 1

increment()
Output
Traceback (most recent call last):
  File "main.py", line 6, in <module>
    increment()
    ~~~~~~~~~^^
  File "main.py", line 4, in increment
    counter += 1
    ^^^^^^^
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

Because of counter += 1, counter is local to increment, and reading it before it has a value fails.

global

PY global.py
counter = 0

def increment():
    global counter
    counter += 1

increment()
increment()
print(counter)
Output
2

global counter tells Python to use the module-level variable. It works, but functions that modify globals are hard to test and reason about. Usually it's better to pass values in and return results:

PY better.py
def increment(counter):
    return counter + 1

count = 0
count = increment(count)
count = increment(count)
print(count)
Output
2

nonlocal

nonlocal refers to a variable in an enclosing function, which is how closures keep state:

PY nonlocal.py
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

clicks = make_counter()
clicks()
clicks()
print(clicks())

other = make_counter()
print(other())
Output
3
1

The inner function remembers count even after make_counter has returned — that's a closure. Each call to make_counter creates an independent counter.

Blocks don't create scope

Unlike many languages, if, for, while and with blocks don't have their own scope in Python. A variable created inside them is visible afterwards in the same function:

PY blocks.py
for i in range(3):
    last_square = i * i

if True:
    flag = "set inside if"

print(i, last_square, flag)
Output
2 4 set inside if

Only modules, functions, classes and comprehensions create new scopes.

Shadowing built-ins

PY shadow.py
list = [1, 2, 3]
numbers = list(range(3))
Output
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    numbers = list(range(3))
TypeError: 'list' object is not callable

The global name list now hides the built-in list type. Avoid naming variables list, dict, str, id, sum, input and other built-ins.

Try it yourself

Try it yourself
Console output