Skip to main content

Scope: where Python looks up a name

When Python meets a name it looks in four places in a fixed order: the running function, any enclosing function, the file’s global names, then the built-ins. Learn the order, why assigning inside a function makes a name local, and how hidden globals and reused built-in names cause bugs.
P01.4.4L0 Foundation25 minIn review

Before this lesson

P01.4.3 The call stack

After this lesson you can

  • State the order in which Python looks up a name: local, enclosing, global, built-in.
  • Decide for each name in a function whether it is local, and predict an UnboundLocalError before running the code.
  • Rewrite a function that changes a global name so that it takes the value in and returns the result.
  • Explain how a name such as sum hides a built-in function, and repair the damage.

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

A notebook starts a running total with sum = 0, adds up the five scores, and prints 300. Twenty cells later, sum(scores) stops with TypeError: 'int' object is not callable. Nothing is wrong with sum, and nothing is wrong with scores. What went wrong is a question of where Python looks when it meets a name.

From the last lesson

1. In this course’s picture of the call stack, where are the global frame and the newest call?

Show answer

The global frame is at the top and the newest call at the bottom, the order in which a traceback lists them.

2. What happens to the names in a frame when its call returns?

Show answer

The frame is popped and its names are gone; the returned value goes back to the line that made the call.

Intuition

The course keeps the hours studied in a list, hours = [2, 4, 5, 1, 3], and the model is predict(hours), whose parameter has the same name. Inside predict, 9 * hours + 33 multiplies a number, not the list: Python chose between the two names hours, and took the parameter.

The rule behind that choice is short. A name can live in four places, and Python looks in them in a fixed order, stopping at the first place that has the name:

  1. local: the frame of the function that is running;
  2. enclosing: the frame of any function that this function is written inside;
  3. global: the names at the top level of the file;
  4. built-in: the names that Python provides, such as print, len and sum.

Programmers remember the order by its initials, LEGB. Each place is a namespace, a table from names to objects, and the part of the program where a name can be used is its scope.

Four rounded boxes nested inside one another. Outermost, dashed: 4 built-in names, Python's own, listing print, len, sum, max, min, round and range. Inside it: 3 global names, the file, listing scores, pass_rate and pass_mark, with the note is 70, never reached. Inside that, sky blue: 2 enclosing, the frame of pass_rate, listing values, count, v, passed and pass_mark. Innermost, with a green edge: 1 local, the frame of passed, listing score, and the line being run, return score >= pass_mark, with pass_mark on a yellow band. A green arrow climbs from it out of the local box, past a grey cross and the words not in passed's frame, to pass_mark in the enclosing box, where a green tick says found: 50.
P01.4.4-F1

The four places a name can live, from the innermost outwards. Looking up pass_mark inside passed, Python finds it in the enclosing function at 50 and never reaches the global 70.

The picture shows the worked example: passed is written inside pass_rate, and the file has a global pass_mark too. The search starts in the innermost box and stops at the first box that has the name.

Build it up

Step 1. Local names first. A function’s parameters, and every name its body assigns, are local to it. That is why hours inside predict is the parameter: the local hours is found first, and the global list is never reached.

Step 2. Global names next. A function may read a name that the file assigns at its top level:

pass_mark = 50

def passed(score):
    return score >= pass_mark

pass_mark is not local to passed, so the search goes on and finds the global name: passed(55) is True and passed(40) is False.

Step 3. Built-in names last. print, len, sum, max, min, round and range are not defined anywhere in your file. They live in a place of their own that is searched last, which is why they work everywhere.

Step 4. Enclosing functions, in between. A def may sit inside another function. The inner function can then read the outer function’s names:

def pass_rate(values, pass_mark=50):
    def passed(score):
        return score >= pass_mark
    count = 0
    for v in values:
        if passed(v):
            count = count + 1
    return count / len(values)

Inside passed, the name pass_mark is not local, so Python looks in the enclosing function, pass_rate, where it is a parameter. Enclosing means written inside, not called from: what counts is where the def sits in the file.

Step 5. An assignment decides what is local. Before a function runs, Python reads its whole body and makes a list of the names that are assigned in it, such as the parameters, the targets of =, the names of for loops and the names of any def inside. Those names are local everywhere in the body, even on the lines before the assignment. So assigning to a name inside a function never changes a global name of the same spelling; it makes a new local name.

Top left: the seven-line program pass_mark = 50; def raise_mark(score): pass_mark = 70, return score >= pass_mark; print(raise_mark(60), pass_mark), with line 5 on a yellow band. Top right: an output box reading False 50, and a note that line 4 assigns pass_mark, so inside raise_mark the name is local. Below, the call stack while line 5 runs: the global frame, waiting at line 7, holds pass_mark pointing to int 50 and raise_mark pointing to function raise_mark; the running frame raise_mark(60) holds score pointing to int 60 and its own pass_mark pointing to int 70. A note: two names pass_mark, two different objects.
P01.4.4-F2

Assigning inside a function makes a new local name. raise_mark compares against its own pass_mark, 70, so 60 fails; the global pass_mark still refers to 50 afterwards.

The formal statement

NoteDefinitions: namespace, scope

A namespace is a table from names to objects: the frame of a call, the global names of a file, or the built-in names. The scope of a name is the part of the program in which it can be used by its bare name.

Lookup. When code uses a name, Python searches the local namespace, then the namespaces of the enclosing functions from the nearest outwards, then the global namespace, then the built-in one, and takes the first match. Code at the top level of a file has no local or enclosing places of its own, so its search starts with the global names.

Binding. A name that is assigned anywhere in a function’s body is local in all of that body. Python decides this from the text of the function, before it runs.

Two errors follow from these rules, both of the NameError family.

  • A name found nowhere raises NameError, “name … is not defined”.
  • A local name used before the line that assigns it raises UnboundLocalError: the search stops in the local namespace, where the name is listed but has no object yet.

The global statement. global total at the start of a function makes every use of total in it mean the global name, so an assignment changes the global one. It works, but it hides an input and an output inside the function.

def is an assignment. A def at the top level binds a global name, so it obeys the same rules: mean = 60.0 after def mean(...) replaces the function with a number, and a global name spelt like a built-in, such as sum or list, hides the built-in from then on.

Go deeper: changing a name of the enclosing function: nonlocal

global reaches the top level of the file. Its partner, nonlocal, reaches the nearest enclosing function: inside a nested function, nonlocal count makes count = count + 1 change the enclosing function’s count instead of making a local one. It is rare in data code: a value that a function needs is better passed in, and a value it makes is better returned.

Worked example

Find where Python finds each name, and what the program prints.

scores = [55, 65, 80, 40, 60]
pass_mark = 70

def pass_rate(values, pass_mark=50):
    def passed(score):
        return score >= pass_mark
    count = 0
    for v in values:
        if passed(v):
            count = count + 1
    return count / len(values)

print(pass_rate(scores), pass_rate(scores, pass_mark))

Start with the first call, pass_rate(scores). scores is used at the top level, so its search starts with the global names and finds the list. Inside pass_rate, the parameter pass_mark takes its default, 50.

The loop calls passed(55). Inside it, score is local: it is the parameter, bound to 55. pass_mark is not local to passed; the enclosing function pass_rate has it, bound to 50, so the search stops there and the global 70 is never reached. So 55 ≥ 50 is True.

Back in pass_rate, passed and count are local, because def and = assign them. len is neither local nor global, and pass_rate has no enclosing function, so the search ends in the built-ins.

A table with the columns name, used in, local, enclosing, global and built-in. score, used in passed(55): a green tick and 55 under local. pass_mark, used in passed(55): a grey cross and not here under local, a green tick and 50 under enclosing, the rest blank. passed, used in pass_rate: a tick and function under local. len, used in pass_rate: not here under local, no such frame under enclosing, not here under global, and a tick and function under built-in. scores, used at the top level: no such frame under local and enclosing, and a tick and list under global. A note below: a search stops at the first place that has the name; the places after it are never looked at, so pass_mark is 50 inside passed: 4 of 5 pass, a rate of 0.8.
P01.4.4-F3

The worked example as a search table. Each search starts where the name is used and stops at the first place that has it; blank cells are places never looked at.

The scores 55, 65, 80 and 60 pass and 40 fails, so count is 4 and the first call returns 4 / 5 = 0.8. The second call passes the global pass_mark in as an argument: the argument is the global’s object, 70, so inside passed the enclosing pass_mark is 70, only 80 passes, and the call returns 1 / 5 = 0.2. The program prints 0.8 0.2.

Code

The worked example, run:

scores = [55, 65, 80, 40, 60]
pass_mark = 70

def pass_rate(values, pass_mark=50):
    def passed(score):
        return score >= pass_mark
    count = 0
    for v in values:
        if passed(v):
            count = count + 1
    return count / len(values)

print(pass_rate(scores), pass_rate(scores, pass_mark))
0.8 0.2

A function that reads a global gives a different answer whenever the global changes, even though its call does not:

pass_mark = 50

def passed(score):
    return score >= pass_mark

print(passed(60))
pass_mark = 70          # a later cell of a notebook, perhaps
print(passed(60))
True
False

A function that assigns to a global name instead makes a local one. Saved as running_total.py and run with Python 3.15 (the wording varies a little between versions):

total = 0

def add_score(score):
    total = total + score
    return total

add_score(55)
Traceback (most recent call last):
  File "running_total.py", line 7, in <module>
    add_score(55)
    ~~~~~~~~~^^^^
  File "running_total.py", line 4, in add_score
    total = total + score
            ^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a value

Line 4 assigns total, so it is local in all of add_score, and the right side of the = finds a local name with no object yet. The global statement makes it work:

total = 0

def add_score_global(score):
    global total
    total = total + score

for s in scores:
    add_score_global(s)
print(total)
300

The better way passes the total in and returns the new total out, so every input and output is in the call:

def add_score(total, score):
    return total + score

total = 0
for s in scores:
    total = add_score(total, s)
print(total)
300

Last, the bug of the hook. As a file, running_sum.py, it fails on its last line:

scores = [55, 65, 80, 40, 60]
sum = 0
for s in scores:
    sum = sum + s
print(sum)
print(sum(scores))
300
Traceback (most recent call last):
  File "running_sum.py", line 6, in <module>
    print(sum(scores))
          ~~~^^^^^^^^
TypeError: 'int' object is not callable

The global name sum refers to the int 300, and the search for sum stops there. In a notebook, del sum removes the global name, and the built-in shows through again:

sum = 0
for s in scores:
    sum = sum + s
print(sum)
del sum                 # remove the global name sum
print(sum(scores))      # found among the built-ins again
300
300

Explore

This module’s interactive, the call-stack stepper, lives in lesson P01.4.3. Step through squared_error(2, 55) with this lesson’s rule in mind: each name a line uses is looked up in the newest frame first, then among the global names, and last among the built-ins, which is where line 9 finds print.

Pitfalls

Two rows. Top row, marked with a cross and titled total is assigned inside, so it is local everywhere inside: the program total = 0, def add_score(score): total = total + score, return total, then add_score(55), with line 4 on a yellow band. Beside it the stack: the global frame with total pointing to int 0 and add_score pointing to a function, and the running frame add_score(55) with score pointing to int 55 and total marked no object yet in reddish purple. Underneath: UnboundLocalError: cannot access local variable 'total' where it is not associated with a value. Bottom row, marked with a tick and titled pass the total in, return the new total out: def add_score(total, score): return total + score, then total = 0 and total = add_score(total, 55). Beside it the global frame with add_score pointing to a function and total pointing to int 55, and the note after line 5: total is 55, and nothing was changed in secret.
P01.4.4-F4

Wrong beside right. Assigning to total inside the function makes it local, so it cannot be read first; taking the total in and returning it out needs no global name at all.

“A function can update a global name by assigning to it.”

Assigning inside a function makes a new local name; it never touches the global one. If the body also reads the name before assigning it, as total = total + score does, the call fails with UnboundLocalError. The global statement would allow the change, but then the function has an input and an output that its call does not show. Pass the value in and return the new value out.

“Reading a global inside a function is harmless.”

It works, which is the danger. passed(60) gave True, then False after a later line changed pass_mark, though the call never changed. In a notebook, where cells run in any order, such a function answers according to which cells ran last: the classic notebook bug of P02.9. Make the value a parameter with a default, as pass_rate(values, pass_mark=50) does.

Two rows. Top row, marked with a cross and titled a global sum hides the built-in: the program sum = 0, for s in scores: sum = sum + s, print(sum), print(sum(scores)), with the last line on a yellow band, and under it TypeError: 'int' object is not callable. Beside it two boxes joined by a green arrow labelled search: global names, searched first, where sum points to int 300 and a green tick shows the search stops; and built-in names, searched last, where sum points to function sum, marked never reached. Bottom row, marked with a tick and titled another name leaves sum alone: the same program with the running total called total, and the note sum(scores) is 300. Its global box holds total pointing to int 300, marked no sum here, and the search goes on to the built-in sum, marked with a tick.
P01.4.4-F5

A global name spelt like a built-in hides it. With another name for the running total, the search for sum passes the global names and reaches the built-in function.

“Any name will do for a variable.”

A global name spelt like a built-in, such as sum, list, max or round, hides it for the rest of the file, and the error appears far from its cause. Choose names that say what the value is, such as total; in a notebook where it has already happened, del the name.

Check yourself

Recall

1. In what order does Python look for a name used inside a function?

Show answer

Local (the running function’s frame), then enclosing functions from the nearest outwards, then global (the top level of the file), then built-in: LEGB.

2. What makes a name local to a function?

Show answer

Being assigned anywhere in the function’s body, as a parameter, by =, by a for loop or by a def. It is then local in all of the body, even before the line that assigns it.

3. Why does total = total + score inside a function raise UnboundLocalError when there is a global total?

Show answer

The assignment makes total local everywhere in the function, so reading it on the right finds the local name before it has an object; the global total is never consulted.

4. What is better than the global statement for a function that updates a running total?

Show answer

Passing the total in as a parameter and returning the new total, as in total = add_score(total, s).

5. Why does sum(scores) fail after sum = 0 at the top level?

Show answer

The global name sum now refers to an int. The search finds it before reaching the built-in function, and an int cannot be called.

Exercises

1. ★ For each name used inside bonus, say whether Python finds it among the local, enclosing, global or built-in names. Then work out bonus(60) and bonus(80).

rate = 0.5

def bonus(score):
    extra = score * rate
    return min(score + extra, 100)
Show solution

score is local (a parameter) and extra is local (assigned on the first line of the body). rate is not assigned in bonus and there is no enclosing function, so it is global. min is found only among the built-ins.

bonus(60): extra is 60 × 0.5 = 30.0, and min(90.0, 100) is 90.0. bonus(80): extra is 40.0, and min(120.0, 100) is 100, so the bonus never takes a score past 100.

2. ★★ Predict what each program prints, or the error it raises. (a) A global count = 0, then def add_one(): with the body count = count + 1, then the call add_one(). (b) A global count = 0, then def show(): with the body print(count), then show(). (c) The program below.

def outer():
    label = 'inside'
    def inner():
        return label
    return inner()

print(outer())
Show solution

(a) UnboundLocalError. count is assigned in add_one, so it is local there, and the right side reads it before it has an object.

(b) It prints 0. show only reads count, so the name is not local; the search finds the global.

(c) It prints inside. label is not local to inner, so Python looks in the enclosing function, outer, where it is bound to 'inside'.

3. ★★★ The count_passes of the exercises of P01.4.1 has the pass mark 50 written into passed. Rewrite it as count_passes(values, pass_mark=50), with passed inside it, so that it reads no global name of yours. What are count_passes(scores) and count_passes(scores, 70), and does a global pass_mark = 70 change the first answer?

Show solution
def count_passes(values, pass_mark=50):
    def passed(score):
        return score >= pass_mark
    count = 0
    for v in values:
        if passed(v):
            count = count + 1
    return count

count_passes(scores) is 4 (55, 65, 80 and 60 reach 50) and count_passes(scores, 70) is 1 (only 80). A global pass_mark = 70 changes nothing: inside passed, the search for pass_mark stops in the enclosing function, whose parameter is 50 unless the call says otherwise. Every input is now in the call, so the answer depends on the call alone.

Summary card

Python looks for a name in four places, in order, and takes the first it finds.

The anchor figure again: four nested boxes, built-in names outermost, then global names, then the enclosing frame of pass_rate, then the local frame of passed, with the search for pass_mark stopping in the enclosing frame at 50.
P01.4.4-F1

Local, enclosing, global, built-in.

  • Lookup: local, then enclosing, then global, then built-in (LEGB); the first match wins.
  • Binding: a name assigned anywhere in a function is local in all of it; reading it before then is an UnboundLocalError.
  • Prefer passing values in and returning results out to global.
  • Never reuse a built-in name such as sum or list; def binds a name like = does.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Python Software Foundation, The Python Language Reference, section 4.2, Naming and binding. 4.2.1 lists the constructs that bind a name (parameters, function definitions, assignment, the for header) and says a name bound in a block is local to it. 4.2.2 gives the resolution of names by the nearest enclosing scope, NameError and UnboundLocalError (a subclass of NameError), the rule that a binding anywhere in a block makes every use in the block local, and the global statement, with the global namespace searched before the built-in one.
  • Python Software Foundation, The Python Tutorial, section 9.2, Python Scopes and Namespaces: the three or four nested scopes searched in order, local, enclosing functions, the module’s global names and the built-in names, and “scopes are determined textually”. Section 4.8, Defining Functions gives the same order for variable references in a function.
  • Python Software Foundation, Programming FAQ, Why am I getting an UnboundLocalError when the variable has a value? and the entry after it on the rules for local and global variables: a name only referenced inside a function is global, an assigned one is local unless declared global, and requiring global for assignment is “a bar against unintended side-effects”.

Next. P01.5 Modules, packages and environments: a module is a file with global names of its own, found by a search much like this one.

This lesson unlocks

P01.5 Modules, packages and environments

Your progress is saved in this browser only. No account, no tracking.
Back to top