RizTech Academy logo
RizTech Academy
FunctionsLesson 4 of 725 min

Scope: local, global, and why your variable is not changing

You change a variable inside a function, print it outside, and it has not changed. Or you get a NameError for a variable that is clearly right there. Both come from scope — the rules about where a name is visible — and ten minutes here saves a long, confusing afternoon.

Local names

A name created inside a function exists only inside it:

def calculate():
    result = 42
    print(result)

calculate()
print(result)
42
NameError: name 'result' is not defined

result was created when the function ran and destroyed when it finished. From outside, it never existed.

This is a feature, not a limitation. It means you can use total or i inside a function without wondering whether some other part of the program uses the same name. Each function gets its own private space.

Global names

A name defined at the top level of a file is visible everywhere, including inside functions:

TAX_RATE = 0.18

def apply_tax(amount):
    return amount + (amount * TAX_RATE)

print(apply_tax(100))
118.0

The function reads TAX_RATE without being passed it. Convenient, and the main legitimate use is exactly this: constants.

Reading is allowed, assigning creates a local

Here is the rule that causes the confusion:

count = 0

def increment():
    count = count + 1

increment()
UnboundLocalError: cannot access local variable 'count' where it is not
associated with a value

That error looks absurd. count is right there, with a value.

What happened: Python scans a function before running it. Seeing count = ... anywhere inside, it decides count is a local name for the whole function. Then the right-hand side runs first and tries to read local count, which has no value yet. Hence the error.

Assigning to a name anywhere in a function makes it local everywhere in that function, including on lines above the assignment.

Without the assignment it reads the global fine:

count = 0

def show():
    print(count)      # works

show()

So this works and that fails, and the only difference is whether you assign.

The global keyword, and why to avoid it

You can override the rule:

count = 0

def increment():
    global count
    count = count + 1

increment()
print(count)        # 1

It works. You should still almost never use it.

A function that modifies a global can change the behaviour of code far away from itself. Debugging means finding every function that touches that name. Testing becomes awkward because tests affect each other. And you cannot tell from a call site that anything outside was modified.

Pass values in and return values out instead:

def increment(count):
    return count + 1

count = 0
count = increment(count)

Slightly longer, and completely predictable. Everything the function needs arrives through its parameters; everything it produces leaves through its return value.

Use global for essentially nothing. Constants read-only at the top of a file are fine. Mutable global state is a habit worth never forming.

The exception you will actually hit

global is about rebinding a name. Modifying an object is different:

items = []

def add(value):
    items.append(value)       # no error, no global needed

add("apple")
print(items)                  # ['apple']

No assignment to items here — you are reading it and calling a method on it. The name still points at the same list; the list's contents changed.

So a function can modify a global list or dictionary without global, which is precisely why mutable global state is hard to reason about. Nothing in the function signature warns you.

Parameters are local too

def double(n):
    n = n * 2
    return n

value = 5
print(double(value))    # 10
print(value)            # 5

n is a local name. Rebinding it does nothing to the caller's value. This is the same point as the arguments lesson, from the other direction: immutable values are safe, and mutable ones are not.

The lookup order

When Python meets a name it looks in four places, in order:

  1. Local — inside the current function
  2. Enclosing — inside any function wrapped around it
  3. Global — the top level of the file
  4. Built-in — print, len, sum and the rest

First match wins. Which is why this is a bad idea:

list = [1, 2, 3]
print(list("abc"))
TypeError: 'list' object is not callable

Your global list shadowed the built-in, so the function is gone for the rest of the program. The names worth avoiding: list, dict, set, str, int, sum, max, min, type, id, input, file.

Your editor usually colours built-ins differently. If a variable name is coloured like print, choose another.

Nested functions

A function inside a function can read the outer one's names:

def outer():
    message = "hello"

    def inner():
        print(message)      # reads the enclosing scope

    inner()

outer()

You will not write these often yet. They matter because it is how closures work, and closures are why the decorators in later frameworks behave as they do. For now, knowing the enclosing scope exists is enough.

Practice

  1. Create a variable inside a function and try to print it outside. Read the NameError.
  2. Define TAX_RATE at the top level and read it inside a function. Confirm it works without passing it.
  3. Reproduce the UnboundLocalError exactly. Then explain, in one sentence, why removing the assignment makes the error disappear.
  4. Fix that function twice: once with global, once by passing and returning. Say which you would rather see in a codebase and why.
  5. Write a function that appends to a global list without global. Confirm it works, then explain why it does when the counter version did not.
  6. Write double(n) that reassigns n, and show the caller's variable is unchanged.
  7. Create a variable called list and then try to use list(). Read the error. Restart your REPL to recover.
  8. Take a function of yours that reads a global and rewrite it to take that value as a parameter. Notice it is now testable with any value you like.

Next: functions that accept any number of arguments.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship