# Scope: Local, Enclosing, Global and Built-in (LEGB)<no value>

## What will we learn?

**Scope** = **the area of the code where a name (a variable or a function) is known**. We will learn:

- **Local** — variables inside a function.
- **Enclosing** — variables of an outer function, when there is a function inside a function.
- **Global** — variables at the top level of the file.
- **Built-in** — Python's built-in names (`print`, `len`...).
- **LEGB** — the order in which Python looks for a name.

## Why do we need this?

If every variable were accessible from everywhere, a variable named `total` in one function would overwrite a variable `total` in another function, and in a program of 10,000 lines that is chaos. Scope **isolates** every function: it can use its own names without fearing collisions.

On the other hand, errors like `NameError` or `UnboundLocalError` are very confusing if you do not understand Scope.

## Explanation from the basics

### Local

A variable created **inside a function** (including its parameters) is **local**:

- It is created **on every call** to the function, and disappears **when it ends**.
- It is **not accessible from outside**.
- Two different functions can have local variables with the same name, with no connection between them.

### Global

A variable created **at the top level of the file** (not inside a function) is **global**:

- It is accessible **for reading** from every function in the file.
- To **change** it (assign it a new value) from inside a function, you need an explicit declaration: `global name`.

### Enclosing

You can define **a function inside a function**. The inner one sees the variables of the outer one. The Scope of the outer one is called **Enclosing** from the inner one's point of view.

### Built-in

The names Python always knows, without `import`: `print`, `len`, `sum`, `max`, `int`, `str`, `list`, `range`...

### The lookup order: LEGB

When the code uses a name, Python looks for it **in this order**, and stops **at the first place** where it found it:

```text
L  Local       → inside the current function
E  Enclosing   → inside outer functions (if nested)
G  Global      → top level of the file
B  Built-in    → Python's built-in names
              → not found anywhere: NameError
```

{{< box "analogy" "Analogy: where are the keys?" >}}
You look for your keys: first **in your pocket** (Local), then **in the bag** you are carrying (Enclosing), then **at home** (Global), and finally **in the building's lobby**, where things are left for everyone (Built-in). The moment you find them, you stop looking. And a key in a friend's pocket you cannot see at all: that is the Local of another function.
{{< /box >}}

## Syntax

```python
counter = 0                  # global

def outer():
    message = "hi"           # local to outer = enclosing for inner

    def inner():
        result = message + "!"   # local to inner; message found in Enclosing
        return result

    return inner()

def increase():
    global counter           # declare: assignments go to the GLOBAL counter
    counter = counter + 1
```

## First code example

`week-2/scope_demo.py`:

```python
course = "Kodkod 6"

def welcome(name):
    message = "Welcome " + name + " to " + course
    return message

print(welcome("Dana"))
print(course)
print(message)
```

**Output:**

```text
Welcome Dana to Kodkod 6
Kodkod 6
Traceback (most recent call last):
  File "scope_demo.py", line 11, in <module>
    print(message)
NameError: name 'message' is not defined
```

## Breaking down the code line by line

| Name | Scope | Explanation |
|---|---|---|
| `course` | **Global** | Created at the top level. `welcome` **reads** it: L (not found) → E (none) → G (found!). |
| `name` | **Local** of `welcome` | A parameter = a local variable. Exists only during the call. |
| `message` | **Local** of `welcome` | Created inside the function and disappears when it returns. |
| `print`, `len`... | **Built-in** | Found at the end of the lookup. |
| `print(message)` outside | — | At the top level there is no `message`. It was Local, and it is already gone, so `NameError`. |

**The right way to get a value out of a function is `return`**, and that is exactly what `welcome` does. The caller got the value (and can store it in a variable of its own).

## Another example

**The same name in several Scopes, and `global`.** `week-2/scope_levels.py`:

```python
total = 100

def local_total():
    total = 5
    print("inside local_total:", total)

def read_global():
    print("inside read_global:", total)

def change_global():
    global total
    total = total + 50
    print("inside change_global:", total)

def outer():
    label = "outer"

    def inner():
        print("inner sees enclosing label:", label)

    inner()

local_total()
print("after local_total:", total)
read_global()
change_global()
print("after change_global:", total)
outer()
```

**Output:**

```text
inside local_total: 5
after local_total: 100
inside read_global: 100
inside change_global: 150
after change_global: 150
inner sees enclosing label: outer
```

**Breakdown:**

- `local_total` — the assignment `total = 5` creates **a new local variable** named `total`, which "hides" the global one. The global one **did not change** (100).
- `read_global` — only **reads** `total`. It has no local with this name, so according to LEGB we reach the global one.
- `change_global` — `global total` tells Python: "in this function, `total` is the global one". Now the assignment changes the global one (150).
- `inner` sees the `label` of `outer`. That is the **E** in LEGB.

## Common mistake

**Mistake 1: `UnboundLocalError` — reading and then changing a global without `global`.**

```python
count = 0

def add_one():
    count = count + 1

add_one()
```

```text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
```

**Why?** Because the function has an **assignment** to `count`, Python decides (in advance, before running) that `count` is **local** in the whole function. And then `count + 1` tries to read a local that has not received a value yet.

**Mistake 2 (the big one): using `global` as a permanent solution.** `global` "works", but:

- Every function can change the variable, and when there is a bug, there is no way to know who changed it.
- The function depends on something "from outside", so it is hard to test it and reuse it.

**The clean way: a parameter goes in, `return` comes out.**

```python
def add_one(count):
    return count + 1

count = 0
count = add_one(count)
print(count)   # 1
```

Now it is completely clear what goes in and what comes out, and the function touches nothing it did not receive. (In the OOP topic we will learn another way to manage "state" that changes: objects.)

**Mistake 3: overwriting a Built-in name.**

```python
list = [1, 2, 3]
numbers = list(range(5))
```

```text
TypeError: 'list' object is not callable
```

Our global `list` comes **before** the Built-in in LEGB, so it "hid" the Built-in function. The same with `sum = 0`, `max = 10`, `input = ...`, `str = ...`. **Never name variables with the names of Built-ins.**

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

### Exercise 1: The variable that disappeared

```python
def welcome(name):
    message = "Welcome " + name
    return message

print(welcome("Dana"))
print(message)
```

1. What will happen on the last line, and why?
2. Fix it so that the message is printed twice.

{{< solution >}}
1. `NameError: name 'message' is not defined`. `message` is a **Local** variable of `welcome` — it is created on every call and disappears when the function ends.
2. Save the returned value in a variable of our own:

```python
def welcome(name):
    message = "Welcome " + name
    return message


result = welcome("Dana")
print(result)
print(result)
```

```text
Welcome Dana
Welcome Dana
```
{{< /solution >}}

### Exercise 2: Predict the output

```python
x = "global"

def first():
    x = "local in first"
    print("1:", x)

def second():
    print("2:", x)

first()
second()
print("3:", x)
```

{{< solution >}}
```python
x = "global"


def first():
    x = "local in first"
    print("1:", x)


def second():
    print("2:", x)


first()
second()
print("3:", x)
```

```text
1: local in first
2: global
3: global
```

- `first` creates a **local** `x` of its own — the global one was not touched.
- `second` does not define `x`, so according to **LEGB** we reach the Global one.
{{< /solution >}}

### Exercise 3: UnboundLocalError

```python
count = 0

def add_one():
    count = count + 1

add_one()
```

1. Which error will you get, and why?
2. Fix it without `global`.

{{< solution >}}
1. **`UnboundLocalError`**. Because of the assignment `count = ...`, Python decides that `count` is a **local** variable in the whole function — and then `count + 1` tries to read a local that has no value yet.
2. A parameter goes in, `return` comes out:

```python
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
```

```text
2
```
{{< /solution >}}

### Exercise 4: Overwriting a built-in name

```python
list = [1, 2, 3]
numbers = list(range(5))
```

1. Which error will you get?
2. Explain using LEGB why it happens, and fix it.

{{< solution >}}
1. `TypeError: 'list' object is not callable`.
2. According to **LEGB**, Python looks for `list` in **Global** before **Built-in**. Our variable named `list` "hid" the built-in function. The fix — do not give variables the names of Built-ins:

```python
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
```

```text
[1, 2, 3] [0, 1, 2, 3, 4]
```
{{< /solution >}}

### Exercise 5: Without global

```python
cart = []
total = 0

def add(price):
    global total
    cart.append(price)
    total = total + price

add(100)
add(50)
print(cart, total)
```

Refactor it so that no function uses `global` or changes outside variables: every function receives what it needs through parameters and returns a result.

**Expected output:**

```text
[100, 50] 150
```

{{< solution >}}
```python
def add(cart, price):
    return cart + [price]


def cart_total(cart):
    return sum(cart)


cart = []
cart = add(cart, 100)
cart = add(cart, 50)
print(cart, cart_total(cart))
```

```text
[100, 50] 150
```

- `total` is no longer "state" that has to be maintained — it is **calculated** from the cart.
- `add` returns a **new** list (`cart + [price]`) and does not change the existing one.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Scope** = where a name is known. A variable created in a function (including parameters) is **Local**, and disappears at the end of the call.
- **LEGB**: Local → Enclosing → Global → Built-in. Stop at the first place where it is found. If it is not found, `NameError`.
- An assignment inside a function creates a **local** variable, even if there is a global one with the same name.
- `global x` lets you change a global from inside a function, but **it is better to avoid it**. A parameter goes in and `return` comes out.
- Do not give variables the names of Built-ins (`list`, `sum`, `max`, `str`...).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Scopes and Namespaces](https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces)
- [Resolution of names](https://docs.python.org/3/reference/executionmodel.html#resolution-of-names)
- [The global statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement)

<!-- docs:end -->
