Study guide

Scope, Shadowing, global, and Local Errors

PCEP Certified Entry-Level Python Programmer Study Guide Exam

Track your progress as you read

Finished sections get a check mark.

1 PCEP Exam Map and Study Strategy

Current PCEP-30-02 Facts How PCEP Tests Code Reading Official Resources and Ethical Prep Four-Week Study Plan and Readiness

2 Computer Programming and Python Fundamentals

1.1 Lexis, Syntax, Semantics, and Indentation 1.2 Identifiers, Keywords, Comments, and Basic Program Structure 1.3 Literals, Types, and Numeral Systems 1.4 Operators, Precedence, and Type Conversion 1.5 print(), input(), and Console Tracing

3 Control Flow

Boolean Expressions, Comparisons, and Truthiness If, Elif, Else, and Decision Trees While Loops and Sentinel Patterns For, Range, and Iteration Patterns Break, Continue, Pass, Loop Else, and Nesting

4 Data Collections

Indexing, Slicing, and Sequence Rules Lists, Mutation, Methods, and Aliasing Tuples, Immutability, and Packing Dictionaries, Keys, Membership, and Iteration Strings, Sets, and Collection Choice

5 Functions and Exceptions

def, return, None, and Call Flow Parameters, Arguments, Defaults, and Keywords Scope, Shadowing, global, and Local Errors Exception Types, try, except, else, and finally Recursion, Debugging, and Final Code Reading

Scope, Shadowing, global, and Local Errors

Key Takeaways Explain This Topic AI

A name assigned anywhere inside a function is local to that function unless a global declaration says otherwise.

Local variables vanish when the call returns, but values can survive by being returned to and stored by the caller.

A local name can shadow a global name of the same spelling without modifying the global object.

Reading an undefined name raises NameError; reading a local name before it is assigned raises UnboundLocalError.

The global statement lets a function rebind a module-level name, and PCEP uses it mainly to test tracing accuracy.

Last updated: June 2026

Names live in scopes

A scope is the region of a program where a name can be found. PCEP tests two scopes: the global (module-level) scope and the local scope created by each function call. Python resolves names using the LEGB order: Local, Enclosing, Global, Built-in. For entry level, focus on Local then Global then Built-in. x = 10

def show():

  • x = 3
  • print(x)
  • show()
  • print(x)

This prints 3 then 10 . The assignment x = 3 inside show creates a brand-new local x ; it never touches the global x . Each call to a function gets a fresh local namespace, so locals from one call cannot leak into another.

Shadowing and read-only global access

Shadowing is when an inner scope reuses the spelling of an outer name. It is legal but makes snippets harder to read because the same word points to different objects on different lines.

  • Where the name is assigned Category Visible where
  • Outside any function Global Module level; readable inside functions
  • Inside a function (no global ) Local That single call only
  • Inside a function with global x Global rebinding Affects module-level x
  • A function may read a global name as long as it never assigns that same name locally: rate = 2

def cost(n):

return n * rate

print(cost(5))

cost only reads rate , so Python looks outward and finds the global value 2 ; the result is 10 . The moment you add rate = ... anywhere in the body, the rules change, as the next block shows.

The local-assignment trap: UnboundLocalError

If a function assigns to a name anywhere in its body, Python treats that name as local for the entire function, even on lines before the assignment, unless a global declaration overrides it. count = 1

def bump():

  • print(count)
  • count = count + 1

bump()

This does not print 1 . Because count is assigned later in bump , Python marks count local throughout. The first line tries to read the local count before it has a value, raising UnboundLocalError (a specialized subclass of NameError). This is one of the most heavily tested scope traps on PCEP.

Contrast with a plain NameError , which occurs when a name exists in no accessible scope at all: def report():

print(total)

report()

If no global total exists, this raises NameError. UnboundLocalError means "the name is local but not yet assigned"; NameError means "the name does not exist anywhere I can look."

  • global, and how values escape a function
  • To rebind a module-level name from inside a function, declare it global first: count = 1

def bump():

  • global count
  • count = count + 1
  • return count
  • print(bump()) # 2
  • print(count) # 2

Both prints show 2 because the assignment now targets the module-level count .

Without global , a local variable disappears when the call ends. The only entry-level way to keep its value is to return it: def make_score():

  • score = 95
  • return score
  • result = make_score()
  • print(result) # 95
  • # print(score) here would raise NameError

The integer 95 survives in result , but the name score was purely local and never created at module level.

Exam approach. Annotate every assignment: mark globals before the first def, mark locals inside each function, route global name assignments to the module level, and check whether any name is read before it exists. Most scope errors on PCEP come from reading code visually instead of following Python's name-resolution rules.

Built-ins, the LEGB chain, and mutation versus rebinding

The final letter of LEGB is Built-in : names like len , print , int , and list live in a built-in scope Python always searches last. Because it is searched last, you can accidentally shadow a built-in by assigning to its name: list = [1, 2, 3]

  • print(list) # works, prints [1, 2, 3]
  • # print(list(range(3))) # now raises TypeError: 'list' object is not callable

Once list names your data, the built-in constructor is hidden in that scope, and calling list(...) fails. PCEP uses this to test whether you understand that built-ins are just names, not protected keywords.

Mutation does not require global. A function may freely change the contents of a mutable object it received, because that is reading the name then mutating the object, not rebinding the name: def add_item(bag):

bag.append('x') # mutates, no global needed

  • stuff = []
  • add_item(stuff)
  • print(stuff) # ['x']

The global stuff changed because both names point to the same list object. Contrast with bag = [1] inside the function, which would rebind the local name and leave stuff untouched. The distinction is mutation versus assignment : mutation affects the shared object; assignment creates or rebinds a name.

Operation inside function Needs global ? Affects caller's object?

  • bag.append(1) (mutate) No Yes
  • bag = [1] (rebind) Yes, to affect a global No, otherwise local
  • Read rate (no assign) No n/a (read-only)

This table resolves the most common scope confusion on the exam.