Skip to content

Build1 publisher2 min readPublished

Python decides count is local before the function runs a single line

A scan of the whole function body marks any assigned name local, and count += 1 counts as an assignment, so the read fails in a scope decided before the function ran. Declaring global changes that scan, and only a function that assigns needs it.

The Engineer · Build desk

What happened

  • In the post's example a module-level count = 0 sits above a function whose body is count += 1, and the first call raises UnboundLocalError: local variable 'count' referenced before assignment.
  • The augmented assignment is what does it, because count += 1 is shorthand for count = count + 1 and therefore counts as an assignment to count inside the function.
  • Writing global count changes that advance scan, so every reference to count in the function means the module-level name and no local shadow is created.
  • The post says the more common variant involves no module scope at all: a label assigned only inside an if branch, then read after the block, which fails on the calls where the branch was skipped.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision The first choice is whether the function should own shared mutable state at all, before any keyword comes into it, and the post's pass-in-and-return form removes the question while leaving the function testable on its own.
  • constraint Because only an assigning function needs the declaration, global added to readers buys nothing and, per the post, widens how far mutable module state travels through the program.
  • capability One check settles both variants: whether the function assigns to that name anywhere in its body, which tells you if you are looking at a shadowed module global or a conditionally assigned local.
  • cost Misreading the message as a definition-order problem costs a search through imports and file ordering, which the post says are fine.

Placement inside the function does not save you. A name assigned on the last line is local on the first, because the advance scan covers the whole body and does not care whether that assignment ever runs [2]. The dev.to post, adapted from the Python Essentials Companion Guide [16], restates the error precisely: "you're reading a local variable, at a point in the function where it hasn't been given a value yet" [6]. The message is literally true and still sounds like Python misplaced something. The post says the scan happens in advance and stops short of how the interpreter stores that decision [1].

In the counter example, one statement does both jobs: the write that marks count local, and the read of that same local, which has nothing in it yet [17]. The count = 0 above the function is a separate module-level variable. The local name shadows it, and Python does not consult it [5]. The post's confirmation test is a good one. If the traceback names a variable you expected to come from outside the function, that is almost always what happened [18].

The repair the post puts first drops the shared state entirely: def increment(n): return n + 1, called as count = increment(count) [7]. Its stated reason is testability, since the function can then be exercised on its own [8].

global is narrower than most people use it. You need it only in a function that assigns to the name somewhere in its body, and a function that merely reads a module-level variable sees it with no declaration at all [10]. Adding it to a reader declares nothing Python needed, and the post says the habit tends to spread mutable shared state further than the program actually needs [11].

The conditional case wants a different repair. Python cannot know which branch ran until the function runs [13], so the fix is a default assignment before the branch, or an else [14]. A declaration would have no module-level label here to rebind.

The expensive misreading is treating the message as "not defined yet" and going after an import order problem or a definition placed too late in the file [15]. The name is usually defined fine at module scope, well before the function runs [15]. In my view passing the value in and returning it is the right default; the exception is a module that genuinely owns one piece of mutable state, and there the declaration on the single writing function documents the write.

What to watch

  • Whether your linter or type checker reports possibly-unbound names, since the conditional case only fails on the branch that was skipped.
  • How the same rule behaves in nested functions: the post works through module scope and conditional assignment only.
  • Whether codebases that added global for safety can drop it from the read-only functions without changing behaviour.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories