Scope is the area of a program where a variable exists. Knowing which variables a function can see — and what it can change — is the foundation of writing predictable code.

Two main scopes — global and local

A variable defined inside a function is local to that function. It exists only while the function is running, and only the function can see it:

def greet() -> None:
    message: str = "Hello"
    print(message)


greet()                # prints "Hello"
print(message)          # NameError — message doesn't exist here

A variable defined outside any function is global. It exists for the whole program and any function can read it:

name: str = "Manikandan"


def greet() -> None:
    print(f"Hello, {name}")     # reads the global name


greet()                          # Hello, Manikandan

Local variables shadow globals

If a function defines a local variable with the same name as a global, the local wins inside the function:

name: str = "Manikandan"


def greet() -> None:
    name: str = "World"        # this is a NEW local variable
    print(name)


greet()                          # World
print(name)                      # Manikandan — the global is untouched

The global name is unchanged. The function created its own name that lives only while it runs.

You can read globals, but not change them (by default)

This catches everyone at least once:

counter: int = 0


def increment() -> None:
    counter += 1     # UnboundLocalError


increment()
UnboundLocalError: local variable 'counter' referenced before assignment

Python sees counter += 1 and decides counter must be a local variable. But there’s no local counter to read from — so it errors.

The right fix in most cases is don’t modify globals. Pass values in, return values out:

def increment(counter: int) -> int:
    return counter + 1


counter: int = 0
counter = increment(counter)
print(counter)   # 1

This is a more honest design — the function’s effect is visible in its signature.

The global keyword (rarely needed)

If you genuinely must change a global, declare it with global:

counter: int = 0


def increment() -> None:
    global counter
    counter += 1


increment()
increment()
print(counter)   # 2

You’ll see global in some scripts. In production code it’s a smell — it usually means the code should be reorganised. Avoid it when you can.

Mutable objects are different

For lists, dictionaries, and other mutable types, you can change their contents without global:

scores: list[int] = []


def add_score(value: int) -> None:
    scores.append(value)   # OK — we're calling a method, not reassigning


add_score(80)
add_score(90)
print(scores)   # [80, 90]

The difference: scores.append(value) doesn’t reassign scores — it changes the list that scores points to. The global name still points to the same list.

This is one of the trickiest distinctions in Python. The rule of thumb: reassignment makes a local; mutation doesn’t.

Functions inside functions

You can define a function inside another function. The inner function can see the outer function’s variables:

def make_greeter(greeting: str):
    def greet(name: str) -> str:
        return f"{greeting}, {name}!"
    return greet


hello = make_greeter("Hello")
howdy = make_greeter("Howdy")

print(hello("Manikandan"))    # Hello, Manikandan!
print(howdy("Friend"))         # Howdy, Friend!

The inner greet “remembers” the greeting it was created with. This pattern is called a closure. It’s not common at the beginner level, but worth recognising when you see it.

The LEGB rule

When Python looks up a name, it searches scopes in this order:

  1. Local — the current function
  2. Enclosing — any function this one is defined inside
  3. Global — the module (the file)
  4. Built-in — print, len, range, etc.

You don’t have to memorise the rule. Just know that inner scopes win over outer ones, and Python falls back outward only if the name isn’t found.

Practical advice

  • Keep functions self-contained. Pass everything they need in as parameters. Return everything they produce.
  • Avoid modifying globals. It makes code unpredictable.
  • Don’t shadow built-ins. Naming a variable list or dict “hides” the real function. Ruff warns about this.

What’s next

You know where variables live. Next, lambda functions — Python’s syntax for tiny one-line functions.

Toggle theme (T)