---
title: 'Scope Rules'
source: 'https://academia.sh/en/courses/python-fundamentals/scope-rules'
course: 'Python Fundamentals'
language: en
updated: '2026-08-17T18:10:29+00:00'
license: 'CC BY-SA 4.0'
---

# Scope Rules

Name lookup runs through four levels, and what decides which level it stops at is not where the name is read but where it is assigned: adding a single assignment line to a body turns the same read from a valid value into UnboundLocalError.

The previous lesson measured how arguments bind to parameters. Every bound argument
establishes a **name** in the body; but the body does not only use those names. Names
it builds itself, ones coming from the enclosing definition, ones sitting at the
outermost level of the file, and ones the language itself supplies are there too —
and all of them are read with the same notation, a bare name.

The Programming Fundamentals course's scope lesson built this search order as a
language-independent **concept**: start local, go to enclosing then global, end with
built-ins, first match wins. That lesson defined scope through visibility, lifetime
through duration, and explained shadowing. Here the same order is measured as a
**search chain**, focused on one question: what decides which level a name is
searched at? The answer runs against intuition — not where the name is **read**, but
whether it is **assigned** somewhere in the body.

## Four Levels

In Python, name lookup runs through four levels, and their order is fixed:

1. **Local** — the function body currently executing.
2. **Enclosing** — the function bodies surrounding it, inside out.
3. **Global** — the file's outermost level.
4. **Built-in** — names the language itself supplies.

The lookup stops at the first place it finds the name and never looks at the levels
below. If none of the four have it, the result is not a value but `NameError`.

The fourth level differs from the others: the program does not write its names, the
language does. But its place in the search order is not special — because it is
last, any of the three levels above defining the same name **shadows** the built-in
and makes it unreachable in that body.

## What Makes a Name Local

This is the real rule, and it concerns the entire body. Python decides which level a
name in a body belongs to **while reading the body, not while running it**. The rule
is one sentence: **if the name is assigned to anywhere in the body, that name is
local for the entire body.**

"Anywhere" has to be taken literally. Even if the assignment is on the body's very
last line, the name is local from the body's very first line onward. This is why a
read done **before** the assignment does not find the outer value — it finds a local
name that has not yet received a value, and the result is an exception:
`UnboundLocalError`.

This is the measurable form of shadowing. The Programming Fundamentals course defined
shadowing as "the outer name becomes unreachable"; the measurement here says
**where** that unreachability starts — not from the assignment line, from the body's
first line.

## Writing Outward

A direct consequence of the rule: **assigning** to an outer name is impossible with
bare notation. Assignment always establishes a local name; it does not change the
outer one, it shadows it.

The language therefore recognizes two declarations. `global` declares that the name
belongs to the global level and routes assignments in that body there. `nonlocal`
declares that the name belongs to an **enclosing** body and routes assignments there;
it does not look at the global level, and errors if it cannot find one.

Reading needs no declaration; a declaration only changes the **writing** direction,
since the rule governs writing, not reading. The asymmetry is deliberate: reading an
outer value changes it for no one, but writing to it leaves a result visible outside
the body. Requiring a declaration keeps that result from being born by accident —
every body writing outward announces it in a single line.

The measurement's assumptions:

- **CF24** — The oracle is a separate string bound at each level naming that level;
  the returned value directly states which level the lookup stopped at.
- **CF25** — The same name `BOUNDARY` is measured in four states, and the states
  differ only in **which levels define that name**; the reading line in the bodies
  is character-for-character identical.
- **CF26** — The built-in level is measured with a separate name, because the
  program cannot write the names at that level. The language-supplied name `min` is
  used for the measurement and is shadowed at the levels above it.
- **CF27** — The assignment rule is measured with three bodies: one that only reads,
  one that reads and then assigns, one that only assigns. The reading line is
  identical in all three; the only thing that changes is whether the body **contains**
  an assignment, and where.
- **CF28** — The writing direction is measured in three states: undeclared, with a
  `nonlocal` declaration, and with a `global` declaration. Because the global-level
  name changes at the end of the measurement, that line is run last.
- **CF29** — Exceptions are written only by **class name**, their messages are not
  written.
- **CF30** — In the closure measurement, three functions are produced in a loop and
  called **after the loop ends**. Leaving the call for after the loop is deliberate;
  what is measured is **when** the name resolves, not which value it carries.

## Measurement

```python
"""Four scope levels: at which level is a name found, where does assignment move it."""

BOUNDARY = "global"


def measure(action):
    try:
        return action()
    except Exception as e:
        return f"!{type(e).__name__}"


def none_defined():
    return UNDEFINED_NAME


def only_global():
    return BOUNDARY


def enclosing_too():
    BOUNDARY = "enclosing"

    def inner():
        return BOUNDARY
    return inner()


def local_too():
    BOUNDARY = "enclosing"

    def inner():
        BOUNDARY = "local"
        return BOUNDARY
    return inner()


SEARCH = (
    ("no level", none_defined, "-"),
    ("only global", only_global, "global"),
    ("global + enclosing", enclosing_too, "global, enclosing"),
    ("global + enclosing + local", local_too, "global, enclosing, local"),
)

print(f"{'levels where the name is defined':<30s} {'found':<26s} result")
for name, f, defined in SEARCH:
    print(f"  {name:<28s} {defined:<26s} {measure(f)}")


def uses_builtin():
    return min([3, 1, 2])


def enclosing_shadows_builtin():
    min = "enclosing shadowed"

    def inner():
        return min
    return inner()


def local_shadows_builtin():
    def inner():
        min = "local shadowed"
        return min
    return inner()


print()
print(f"{'builtin name min':<32s} result")
for name, f in (("no level defines it", uses_builtin),
              ("enclosing level defines it", enclosing_shadows_builtin),
              ("local level defines it", local_shadows_builtin)):
    print(f"  {name:<30s} {measure(f)}")


def only_reads():
    return BOUNDARY


def reads_then_assigns():
    before = BOUNDARY
    BOUNDARY = "local"
    return before


def only_assigns():
    BOUNDARY = "local"
    return BOUNDARY


print()
print(f"{'what is done with BOUNDARY in body':<34s} {'reading in body':<24s} result")
for name, f, reading in (("only read", only_reads, "none before assignment"),
                     ("read, then assigned", reads_then_assigns, "before assignment"),
                     ("only assigned", only_assigns, "after assignment")):
    print(f"  {name:<32s} {reading:<24s} {measure(f)}")


def declares_global():
    global BOUNDARY
    BOUNDARY = "written from inside"


def declares_nonlocal():
    BOUNDARY = "enclosing"

    def inner():
        nonlocal BOUNDARY
        BOUNDARY = "written from inside"
    inner()
    return BOUNDARY


def writes_undeclared():
    BOUNDARY = "enclosing"

    def inner():
        BOUNDARY = "written from inside"
    inner()
    return BOUNDARY


print()
print(f"{'writing outward from body':<28s} {'target level':<13s} outer name's value after")
for name, f, target in (("no declaration", writes_undeclared, "enclosing"),
                     ("nonlocal declaration", declares_nonlocal, "enclosing")):
    print(f"  {name:<26s} {target:<13s} {measure(f)}")

before = BOUNDARY
declares_global()
print(f"  {'global declaration':<26s} {'global':<13s} {before} -> {BOUNDARY}")


def late_bound():
    functions = []
    for i in range(3):
        def produce():
            return i
        functions.append(produce)
    return [f() for f in functions]


def captured_by_default():
    functions = []
    for i in range(3):
        def produce(i=i):
            return i
        functions.append(produce)
    return [f() for f in functions]


def name_after_loop():
    for i in range(3):
        pass
    return i


print()
print(f"{'three functions produced in a loop':<34s} returned when called")
print(f"  {'bare name in body':<32s} {late_bound()}")
print(f"  {'name captured by default':<32s} {captured_by_default()}")
print(f"  {'name after loop ends':<32s} {name_after_loop()}")
```

```
levels where the name is defined found                      result
  no level                     -                          !NameError
  only global                  global                     global
  global + enclosing           global, enclosing          enclosing
  global + enclosing + local   global, enclosing, local   local

builtin name min                 result
  no level defines it            1
  enclosing level defines it     enclosing shadowed
  local level defines it         local shadowed

what is done with BOUNDARY in body reading in body          result
  only read                        none before assignment   global
  read, then assigned              before assignment        !UnboundLocalError
  only assigned                    after assignment         local

writing outward from body    target level  outer name's value after
  no declaration             enclosing     enclosing
  nonlocal declaration       enclosing     written from inside
  global declaration         global        global -> written from inside

three functions produced in a loop returned when called
  bare name in body                [2, 2, 2]
  name captured by default         [0, 1, 2]
  name after loop ends             2
```

## The Chain's Four Rungs

The top table builds the search chain rung by rung. The **reading line** of the four
rows is **character-for-character identical**; the only thing that changes is which
levels define that name.

When no level defines it, the result is `NameError`. In the second row the name
exists only at the global level, and that is what is found. In the third row the
enclosing level also defines the same name, and **enclosing wins** — the global-level
name is still sitting there, but never even enters the search. In the fourth, the
local level is added too, and this time **local wins**.

The pattern: every new level shuts out everything **outside** it. The search runs
**inside out**, not outside in, and stops at the first match. When three levels
define the same name, they do not compete — the innermost never even tests the other
two.

The middle table adds the fourth level. In the first row, no level defines `min`, and
the search descends to the built-in level and finds the language-supplied name; the
result is **1**, the smallest of the three-item list. In the next two rows the same
name is defined first at the enclosing, then the local level, and both shadow the
built-in. The built-in level has no privilege in search order; being last makes it
**the easiest to shadow**.

The practical consequence: giving a built-in's name to a local variable makes that
built-in unusable in that body. No error, no warning; the name now points to
something else.

## A Single Assignment Line

The third table is the lesson's main measurement, and its three rows again have the
**same reading line**.

First row: the body only reads the name `BOUNDARY`, never assigns to it anywhere.
The search chain runs, finds it at the global level, result **global**.

Second row: the body does the same read first, **then** makes an assignment to the
name. The reading line comes before the assignment and is character-for-character
identical to the one in the first row. But the result is no longer a value — it is
**`UnboundLocalError`**.

The only difference is that an assignment exists somewhere **further down** in the
body. That assignment makes the name local for the entire body; by the time the
reading line is reached, the search never goes out to the global level at all, it
stops at the local level and finds a name there that has not received a value yet.
The chain does not break — **it is never built at all**.

The order relationship here is critical: what decides a name is local is not
execution order, it is a decision made by **looking at the whole body**. This is why
the error-producing line does not contain the cause; the cause is the assignment
line below it. The third row completes this — when the body only assigns and then
reads, there is no problem, the result is **local**.

## Three Ways to Write

The bottom table measures the writing direction. In the first row the inner body
makes an undeclared assignment, and the outer name **does not change**: the result
is still `enclosing`. The assignment did not update the outer one, it established a
new name belonging to the inner body, and that name vanished once the body ended. The
line written looks like an update; what it does is a shadowing.

In the second row the same body writes with a `nonlocal` declaration, and the outer
name **does** change. In the third row a `global` declaration changes the
global-level name; the value that was `global` before the measurement becomes
`written from inside` afterward.

What the three rows say: writing outward from a body needs **a declaration**, not
bare assignment. The declaration adds no new capability; it only states which level
the assignment targets. With no declaration, the target is always local — the other
face of the same rule as the previous table's `UnboundLocalError`: one shows up in
reading, the other in writing.

This distinction also explains the difference between **mutating** an outer object
and **rebinding** an outer name. The shared bucket in the previous lesson grew with
no declaration at all, because what happened there was not assignment to a name, it
was mutating the object the name pointed to. Scope rules govern the binding of names,
not the content of objects.

## When a Name Resolves

The last table measures **when** the chain runs, and gathers everything up to here
into a single result. Three functions are produced in a loop; each reads the loop
variable in its body with a bare name. All three are called after the loop ends.

The expected result is `[0, 1, 2]`, the measured result is **`[2, 2, 2]`**. All three
functions return the same value.

The reason is the scope rule itself. The name in the functions' bodies does not
resolve when the function is **produced**; it resolves when it is **called**. What
gets written into the body during production is not a value but a name — and that
name points to a single variable in the enclosing body. The three functions do not
carry three separate values, they carry **the same name**. Called, the search chain
runs, finds the name at the enclosing level, and finds there the last value the loop
left behind.

The third row shows what that last value is: the name is still alive after the loop
ends, and its value is **2**. In Python a loop does not open its own scope; the loop
variable belongs to the enclosing body and does not vanish when the loop ends. The
Programming Fundamentals course said the loop variable is **local**, and that is
true — the measurement narrows it: the unit it is local to is not the loop, it is
**the entire function body**. In languages where a brace- or keyword-closed block
opens its own scope, this line would raise an error; here it gives a value.

The second row gives the resolution, and the resolution is the previous lesson's
rule. When the name is written as a default value, the result becomes **`[0, 1,
2]`**. A default expression is evaluated once **while the definition is read**;
because the definition sits inside the loop body here, it reruns on every turn and
captures that turn's value. Rather than being left to the search chain, the value
gets copied at definition time.

The two lessons' rules meet here: scope says **where** a name will be searched for,
while a default's evaluation timing changes **when** it will be searched for. The
same three-line loop gives two different results with a single change to a
signature.

## Summary

- Name lookup runs through four levels in a fixed order — local, enclosing, global,
  built-in — stopping at the first match, and gives `NameError` if none of the four
  have it.
- Every level shuts out everything outside it: if the same name is defined at three
  levels, the innermost wins and the others never enter the search.
- The built-in level has no privilege in search order; being last, the same name
  defined at any level above makes the built-in unreachable in that body.
- What decides a name is local is not where it is read, but that it is assigned to
  **somewhere** in the body; even if the assignment is last, the name is local from
  the body's start, and a read before the assignment produces `UnboundLocalError`.
- Writing outward from a body needs a declaration: `nonlocal` routes the assignment
  to the enclosing level, `global` to the global level. An undeclared assignment does
  not update the outer name, it establishes a new local one that shadows it.
- A name in a body resolves not when the function is **produced** but when it is
  **called**: three functions produced in a loop share the same name, so all three
  return **`[2, 2, 2]`**; capturing the name as a default value makes the result
  **`[0, 1, 2]`**.

## Next Step

Up to here, the names at the search chain's last rung only ever showed up as
something to be shadowed. Yet the names sitting at that level are the language's
most heavily used functions, and each one connects directly to the protocols the
first two lessons measured: one asks for length, one converts to text, one requests
an iterator, one tests truthiness, one sorts. The next lesson measures these
built-ins by their **contracts** — which special method each one calls, and what
happens when that method is not defined. What emerges is that built-ins are not
functions doing the work themselves, but **thin shells calling a protocol**.
