Skip to content
VLSI Mentor

Python Part I · Module 11 · Functions

Where Names Live, and Why Globals Bite

Chapter 11.4 opened with a function whose answer changed between two identical calls because it read a name it never declared, and promised that the rules making that possible would get their own chapter. This is it. Python's lookup rule is short enough to state in a sentence, and it has one consequence that produces an error message almost nobody can parse on first reading — a function that assigns to a module-level name fails on the line that reads it, complaining about a variable that is plainly sitting right there. Once you know why, the message becomes obvious and the design advice that follows stops being a matter of taste.

Intermediate16 min readPythonPart IFundamentalsFunctionsScope

Part I · Module 11 · Chapter 11.10 · Where Names Live, and Why Globals Bite

1. Names Created Inside a Function Stay There

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def count_errors(log):
    errors = 0
    for line in log:
        if "UVM_ERROR" in line:
            errors += 1
    return errors


print(count_errors(["UVM_ERROR @1: a"]))
print(errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
NameError: name 'errors' is not defined

errors existed while the function ran and does not exist afterwards. Names created inside a function body are local to that call — they are made when the call starts and discarded when it ends.

That is a feature, not a limitation. It is why two functions can both use a counter called errors without colliding, and why you can name a loop variable line in forty places.

2. Two Places a Name Can Live

For everything in this course there are two:

  • Local — names created inside a function body, including its parameters. They belong to one call.
  • Module-level — names created at the top level of the file. Often called global names, which is the keyword Python uses.

The lookup rule is: inside a function, Python looks in the local names first, and falls back to the module-level names.

3. Reading a Module-Level Name Works

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LIMIT = 90


def passed(coverage):
    return coverage >= LIMIT


print(passed(95))
print(passed(50))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
True
False

LIMIT is not local, so the fallback found it at module level. This is the mechanism behind Chapter 11.4's Section 1 problem — and, used carefully, behind the legitimate exception that chapter allowed for genuine constants.

4. Assigning Creates a Local Name

Now assign to a module-level name from inside a function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count = 0


def bump():
    count = 99
    print("inside :", count)


bump()
print("outside:", count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inside : 99
outside: 0

Inside, count is 99. Outside, it is still 0.

The assignment did not change the module-level count — it created a new local name that happens to have the same spelling, which existed for the duration of the call and was then discarded. This is Chapter 11.7's rebinding rule again: binding a name never reaches through to anything else.

5. The Error That Confuses Everyone

Here is the combination that produces the module's strangest message:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
count = 0


def bump():
    count += 1
    return count


bump()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Read it against the code. There is a count = 0 three lines above. Section 3 showed that reading a module-level name works. So why is this one unreadable?

Because count += 1 is an assignment, and Python decides whether a name is local by scanning the whole function body before running it. Seeing an assignment to count anywhere in the body makes count local for the entire body — including the read on that same line, which happens before the assignment can give it a value.

count += 1 means count = count + 1. The right-hand side needs the current value of a local count that has not been assigned yet.

6. global Says You Mean the Outer One

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count = 0


def bump():
    global count
    count += 1


bump()
bump()
print("count:", count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count: 2

The global declaration tells Python that count in this body refers to the module-level name, so the assignment reaches it. The function now works as intended — and this is the point at which the chapter stops being about syntax.

7. What to Write Instead

Almost every global has a direct arguments-and-return equivalent. Side by side:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count = 0


def bump_global():
    global count
    count += 1


bump_global()
bump_global()
print("global version :", count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
global version : 2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def bumped(current):
    return current + 1


count = 0
count = bumped(count)
count = bumped(count)
print("return version :", count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
return version : 2

The second version's function takes what it needs and hands back a result. It has no memory, no dependency on anything outside its parentheses, and no ability to affect anything it was not given.

The same conversion scales to real work. A log checker that accumulates into a global:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def count_errors(log):
    errors = 0
    for line in log:
        if "UVM_ERROR" in line:
            errors += 1
    return errors


logs = [["UVM_ERROR @1: a"], ["UVM_INFO @1: b"], ["UVM_ERROR @1: c", "UVM_ERROR @2: d"]]

running_total = 0
for log in logs:
    running_total += count_errors(log)

print("total errors:", running_total)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total errors: 3

count_errors is pure — same input, same output, every time — and the accumulation happens in the caller, where Chapter 10.10 put it. Nothing is global, and the function can be used by anything.

8. Why This Is a Hard Rule, Not a Preference

Three costs are visible right now, and a fourth arrives later.

Order dependence. A function that updates shared state gives different answers depending on what ran before it. Calling it twice is not the same as calling it once, and neither is the same as calling it after something else touched the same name.

Untestability. Checking what bump() does requires knowing and controlling the current value of count, and resetting it afterwards. Checking what bumped(5) does requires nothing at all.

Invisible coupling. The function's header says it takes nothing and returns nothing. Both statements are false, and the only way to discover that is to read the body — and then to search the file for everything else that touches the same name.

And the one you will meet later. The canonical note on this chapter calls a global counter a race waiting to happen. When a script runs several jobs at the same time, two of them can read the same shared counter, both add one, and both write back — losing an increment with no error anywhere. That kind of failure is not demonstrable with what Part I covers, and it is the reason this is stated as a rule rather than advice: the version that works alone on your machine is the version that silently miscounts in production.

9. When Module-Level Names Are Fine

Two cases, both narrow:

Genuine constants — values set once and never reassigned, usually written in capitals:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR_MARKER = "UVM_ERROR"
FATAL_MARKER = "UVM_FATAL"


def severity(line):
    if FATAL_MARKER in line:
        return "fatal"
    if ERROR_MARKER in line:
        return "error"
    return "info"


print(severity("UVM_FATAL @1: x"), severity("UVM_ERROR @2: y"), severity("UVM_INFO @3: z"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fatal error info

Nothing reassigns these, so Chapter 11.4's test is satisfied: the function's answer depends only on its argument.

The top-level script itself. The names at the bottom of your file that hold results and drive the work are module-level by definition, and that is correct — that is the layer Chapter 11.8 identified as the edge of the program.

The rule is about functions assigning to shared names, not about module-level names existing.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
x = 1
def a():
    return x + 1
print("A:", a(), x)

# B
y = 1
def b():
    y = 99
    return y
print("B:", b(), y)

# C
z = 1
def c():
    global z
    z = 99
    return z
print("C:", c(), z)

# D
def d(n):
    n += 1
    return n
w = 5
print("D:", d(w), w)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 2 1
B: 99 1
C: 99 99
D: 6 5

A — reading the module-level name works and does not change it.

B — the assignment made a local; the outer name is untouched.

C — global made the assignment reach outward, so both are 99.

D — a parameter is a local name, so rebinding it changes nothing outside. This is B with the name arriving as an argument, and Chapter 11.7's rule is the same rule.

11. Exercises

Exercise 1 — local lifetime. Write a function that creates a name and returns, then try to print that name afterwards. Record the error and explain it in one sentence.

Exercise 2 — read versus assign. Write one function that reads a module-level LIMIT and one that assigns to it without global. Print the module-level value after each and explain the difference.

Exercise 3 — the confusing error. Produce UnboundLocalError deliberately. Then move the offending assignment to the last line of a five-line function and show the error still occurs on the first line. Explain what that proves.

Exercise 4 — fix it three ways. Take Exercise 3's function and make it work (a) with global (b) by taking the value as an argument and returning the new one (c) by accumulating in the caller. Say which you would ship.

Exercise 5 — list the costs. For a function def record_failure(): that appends to a global list, name the three demonstrable costs from Section 8 and give a concrete example of each.

Exercise 6 — constant or input. For each module-level name, say whether a function may read it: (a) MAX_RETRIES = 3, never reassigned (b) current_test, reassigned in a loop (c) SEVERITIES = ("info", "error", "fatal") (d) total_errors, incremented by several functions.

Exercise 7 — remove the global. Rewrite this so no function assigns to a shared name, keeping the same final output.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total = 0

def add_log(log):
    global total
    for line in log:
        if "UVM_ERROR" in line:
            total += 1

add_log(["UVM_ERROR @1: a"])
add_log(["UVM_ERROR @1: b", "UVM_ERROR @2: c"])
print(total)

Exercise 8 — predict the pair. Without running, give both printed values for a function that assigns to a name also defined at module level, with and without global. Then verify.

12. Summary

  • Names created inside a function are local to that call and do not exist afterwards.
  • There are two places a name can live in this course: local, and module-level (global).
  • Lookup checks local names first, then module-level.
  • Reading a module-level name from inside a function works.
  • Assigning to one creates a new local name of the same spelling; the outer name is untouched.
  • Python decides locality for the whole body before running it, so one assignment anywhere makes the name local everywhere in that function.
  • That is why count += 1 raises UnboundLocalError — the read on that line needs a local value that does not exist yet.
  • global declares that assignments refer to the module-level name.
  • Almost every global has an arguments-and-return version of the same length.
  • The costs are order dependence, untestability and invisible coupling — and a shared counter becomes a lost-update race the moment anything runs in parallel.
  • Genuine constants are the narrow exception, and the test is whether anything ever reassigns them.

The next chapter writes down what a function promises, for the reader who has to decide whether to call it.

Where this fits

Part of the Python curriculum.