---
title: 'Scope and Lifetime'
source: 'https://academia.sh/en/courses/programming-fundamentals/scope-and-lifetime'
course: 'Programming Fundamentals'
language: en
updated: '2026-08-17T18:08:25+00:00'
license: 'CC BY-SA 4.0'
---

# Scope and Lifetime

Name lookup order, shadowing, writing to global state, the lifetime of local variables, and closures.

The previous lesson showed that a function can modify objects from outside. This raises
a natural question: which variable does a name written inside a function body refer
to, and are names defined there visible from outside?

The subject of this lesson is **scope** — the region where a name is valid — and
**lifetime** — the interval during which a value exists. The two are often confused;
one concerns visibility, the other duration.

## Local Scope

Names bound inside a function body are **local**: they are visible only within that
body and disappear when the function returns.

```python
def summarize(measurements: list[int]) -> float:
    total = 0                     # local name
    for measurement in measurements:         # the loop variable is local too
        total += measurement
    return total / len(measurements)

print(summarize([12, 18]))            # 15.0
# print(total)                        -> error: 'total' is not defined here
```

This is a direct consequence of the memory layout lesson in the previous course: local
variables live in the call frame, released on return. The benefit of locality is
isolation — two separate functions can use the same name without affecting each other.

## Name Lookup Order

When a name is used in a body, the language searches for it in a specific order:

1. **Local scope** — the body of this function.
2. **Enclosing scope** — the body of the outer function, in nested definitions.
3. **Global scope** — the outermost level of the module.
4. **Built-in names** — the names the language itself provides.

The search stops at the first place the name is found; if a name is defined both
locally and globally, the local one wins inside the body.

```python
LIMIT = 20                          # global

def count_over(measurements: list[int]) -> int:
    count = 0                       # local
    for measurement in measurements:
        if measurement > LIMIT:           # read from the global name
            count += 1
    return count

print(count_over([12, 25, 30]))   # 2
```

**Reading** from a global name is unrestricted; writing to one is different, and is the
subject of the next section.

## Shadowing

Using the same name locally as an outer name is called **shadowing**. The outer name
becomes inaccessible; it does not disappear, it is merely not visible in that region.

```python
LIMIT = 20

def incorrect_use(measurements: list[int]) -> int:
    LIMIT = 5                       # shadows the outer name
    return len([m for m in measurements if m > LIMIT])

print(incorrect_use([12, 25, 30]))   # 3   — not the expected 2
print(LIMIT)                          # 20  — the global value did not change
```

Shadowing is not always a mistake; it can be used deliberately in short bodies. It
becomes a source of errors when it goes **unnoticed**: the reader assumes the outer
value while the code uses the local one. The criterion from the naming lesson applies
here too — distinct concepts get distinct names.

## Writing to Global State

A function modifying a global variable falls into the side effect category from the
previous lesson and is generally avoided. Most languages make this deliberately
awkward: writing to a global name requires an explicit declaration; otherwise the
assignment creates a new local name.

The rationale for avoiding this has three parts. The function's behavior stops
depending only on its arguments; the same call can return a different result depending
on the global state's history. Testing becomes harder, since a test must reset state on
every run. When two threads concurrently modify the same global value, the result
depends on ordering.

The alternative is to **take** the needed value **as an argument** and **return** the
result. This makes the function independent and testable.

Constants are an exception to this rule: global values that do not change can be shared
and improve readability.

## Lifetime and Closures

Scope specifies visibility, lifetime specifies duration. The two usually coincide: a
local variable arrives with the frame and leaves with it.

There is one case where this coincidence breaks down: if a function defined inside
another accesses a local variable of the outer function, that variable must continue
to exist even after the outer function returns. This structure is called a **closure**.

```python
def limit_checker(limit: int):
    """Produces a function that decides based on the given limit."""
    def exceeds(measurement: int) -> bool:
        return measurement > limit        # the outer 'limit' was captured
    return exceeds

over_twenty = limit_checker(20)
over_ten = limit_checker(10)

print(over_twenty(25), over_twenty(15))   # True False
print(over_ten(15))                       # True
```

When the call to `limit_checker` ends, its frame is released, but the value of `limit`
continues to exist: the produced function accesses it. In the terms of the previous
course, the captured value lives on the heap, not the stack, and it is not collected as
long as the accessing function exists.

**Modifying** the captured value from the inner function requires a separate
declaration. The rule is the same as for global names: an assignment without the
declaration creates a new local name instead of updating the outer one. This is a
common source of errors in closures that keep a counter — the counter appears to
increase, but starts from zero on every call.

Closures are a simple way of producing behavior through a parameter; the
object-oriented approach meets the same idea by storing state in an object. The
comparison of the two will be made in the paradigms topic.

## Modules and Namespaces

Global scope is not as broad as it appears: every source file has its own namespace. A
name defined in one file is not automatically visible in another; it must be imported.

Importing establishes a new binding to the name; there are two forms, and their effect
on readability differs:

```python
import math                      # access through the module name
print(math.sqrt(16))             # 4.0

from math import sqrt            # bring the name directly into our own namespace
print(sqrt(16))                  # 4.0
```

In the first form, where `sqrt` comes from is visible in the call. In the second, the
call is shorter, but the name's source is no longer visible, and two sources carrying
the same name can shadow each other.

Namespaces are the structural way of preventing collisions: two different modules can
use the same name with different meanings. The larger-scale version is package and
namespace hierarchies, detailed in the language curricula.

The rule concerning global scope applies here too: mutable state at the module level is
accessible from anywhere in the file, producing the same testing and concurrency
problems.

## Keeping a Variable's Lifetime Short

One practical rule summarizes this entire lesson: **a name is defined in the narrowest
region it needs.** A variable used inside a loop is defined inside the loop; a variable
belonging to a function is defined inside the function.

This rule turns scope into a design tool. Narrow scope has three benefits: fewer names
for the reader to keep in mind, a bounded region where a name can change, which makes
debugging easier, and a lower probability of shadowing and name collisions.

## Summary

- Names bound in a function body are local and disappear along with the call frame.
- Name lookup starts at local scope, continues through enclosing and global scope, and
  ends with built-in names; the first one found wins.
- Shadowing makes the outer name inaccessible; its danger lies in going unnoticed.
- Writing to global state makes a function independent of its arguments and complicates
  testing; the value is taken as an argument and the result is returned.
- A closure is an inner function that captures a local variable from the outer
  function; the captured value continues to exist after the outer call ends.
- Names are defined in the narrowest region they need.

## Next Step

A function calling itself creates an interesting situation on the call stack: multiple
frames of the same function exist at the same time. The next lesson takes up
recursion — the base case, the reduction step, and stack depth.
