Python Essentials for AI Engineer

Course Content

Python Essentials for AI Engineer

6 sections · 48 lessons

What is the global keyword?


What you need to know

When global is and isn't needed

Python
count = 0seen = []def bump():    global count    count += 1            # rebinds the module-level namedef remember(x):    seen.append(x)        # mutation, not rebinding: no 'global' neededbump(); bump(); remember("d1")print(count, seen)        # 2 ['d1']

Without the global line, count += 1 raises UnboundLocalError, because assignment makes count local. seen.append never assigns the name seen, so no declaration is needed.

Why it is a code smell

  • Hidden inputs. bump() depends on state that is not in its signature, so you cannot understand it by reading it.
  • Hard tests. Each test must reset the global, and tests can pass or fail depending on run order.
  • Threads. count += 1 is a read, an add and a write. Two threads can interleave and lose updates.

nonlocal — the closure version

Python
def make_counter():    n = 0    def inc():        nonlocal n        # rebind n from make_counter, not a global        n += 1        return n    return incc = make_counter()print(c(), c(), c())      # 1 2 3

The acceptable pattern: a lazy, module-level resource

Loading a model or a tokenizer can take seconds, so you want it once per process:

Python
_model = Nonedef get_model():    global _model    if _model is None:        _model = {"name": "sentiment-v2"}   # stands in for an expensive load    return _modelprint(get_model() is get_model())            # True -> loaded once

functools.cache on a no-argument function achieves the same thing without global.

A real-life example

A FastAPI service tracks how many LLM calls it has made so it can stop before the daily budget:

Python
import threadingclass Budget:    def __init__(self, limit):        self.limit, self.used = limit, 0        self._lock = threading.Lock()    def spend(self, n=1):        with self._lock:                     # one thread at a time            if self.used + n > self.limit:                return False            self.used += n            return Truebudget = Budget(limit=3)print([budget.spend() for _ in range(4)])     # [True, True, True, False]

The first version used global calls_made. Under load with many worker threads, the count drifted below the real number, and the team overspent by about 8% in one day. Moving state into a class with a lock fixed it and made the budget easy to unit-test with a fresh Budget(limit=3). With several processes you would move the counter to Redis or a database.

Follow-up questions to expect

  • "What is the difference between global and nonlocal?" — global rebinds a module-level name; nonlocal rebinds a name in the nearest enclosing function.
  • "Do you need global to append to a global list?" — No. Appending mutates the object; only rebinding the name needs global.
  • "Are module-level constants bad too?" — No. Constants in UPPER_CASE that never change, like MAX_TOKENS = 4096, are fine; the problem is mutable shared state.