Modules and Variable Scope#
Note
Source: Contributed by PhD students in COMP 501 at Loyola University Chicago.
As programs grow larger, fitting everything into a single file quickly becomes
unmanageable. Python’s solution is modules: separate .py files that
organize related functions, classes, and constants into logical units. Along with
modules comes a formal understanding of scope — the rules that determine where
a variable exists and where it can be accessed.
Why Modules Matter#
A module is simply a Python file that contains code you want to reuse elsewhere.
# math_utils.py
def add(a, b):
return a + b
You can then import and use it in another file:
import math_utils
print(math_utils.add(3, 4))
Output:
7
The benefits of modular programming are:
Reusability — write a function once, use it in many files.
Maintainability — when something breaks, you know exactly which file to open.
Readability — smaller files are easier to read and understand.
Teamwork — different developers can work on separate modules independently.
Abstraction — hide implementation details and expose only the interface (functions and classes) that others need.
Importing Modules#
Python provides several import patterns:
Syntax |
Example |
|---|---|
|
|
|
|
|
|
|
(avoid this — it clutters the namespace) |
When you run import something, Python looks in:
The current directory.
The Python standard library.
Any directories listed in
sys.path.
If it cannot find the module you will get an ImportError.
The __name__ Variable#
Every Python file has a special variable called __name__.
When a file is run directly,
__name__equals"__main__".When a file is imported,
__name__equals the module’s name.
# greetings.py
def hello():
print("Hello!")
if __name__ == "__main__":
hello() # runs only when executed directly, not when imported
This pattern lets you write code that behaves differently depending on whether it is the entry point or a library being imported.
Understanding Scope#
Scope determines where a variable exists and where it can be accessed. Python resolves variable names using the LEGB rule, searching in this order:
Local (L) — inside the current function.
Enclosing (E) — inside any enclosing (outer) functions.
Global (G) — at the top level of the module.
Built-in (B) — Python’s built-in names such as
lenandprint.
x = 10 # global
def outer():
x = 20 # enclosing
def inner():
x = 30 # local
print(x)
inner()
outer()
Output:
30
Python finds x = 30 at the local level and stops searching.
Variable Shadowing#
If a variable in an inner scope shares a name with one in an outer scope, it shadows the outer variable within that scope.
x = 5
def demo():
x = 10
print(x) # prints 10, not 5
demo()
print(x) # prints 5 — the global x is unchanged
Use distinct variable names to avoid accidental shadowing.
Global and Nonlocal Variables#
Sometimes a function needs to modify a variable that lives outside its local scope.
Global Variables#
count = 0
def increment():
global count
count += 1
increment()
print(count)
Output:
1
Without the global keyword, Python would treat count inside the function
as a new local variable — and the assignment would raise an UnboundLocalError.
Nonlocal Variables#
nonlocal is used in nested functions to modify a variable from the enclosing
(not global) scope.
def outer():
x = 5
def inner():
nonlocal x
x += 1
inner()
print(x)
outer()
Output:
6
As a general rule, prefer returning values from functions rather than modifying external state — it makes code easier to reason about.
Variable Lifetime#
Every variable has a lifetime — the period it exists in memory:
Local variables — created when a function is called, destroyed when it returns.
Global/module variables — live as long as the program runs.
Imported module variables — remain cached in memory until the interpreter exits.
Python automatically frees memory for objects that are no longer referenced (garbage collection), so you rarely manage memory manually. However, be aware that mutating a shared list inside a function affects every module that holds a reference to that list.
Organizing Multi-File Projects#
In larger projects, data structures and utilities are typically split across multiple
files inside a package — a directory that contains an __init__.py file.
datastructures/
__init__.py
node.py
linked_list.py
stack.py
utils.py
node.pydefines theNodeclass.linked_list.pyimportsNodeand implementsLinkedList.stack.pyreusesLinkedListinternally.utils.pyprovides helper functions.
You import from the package like this:
from datastructures.linked_list import LinkedList
Avoiding Circular Imports#
A circular import occurs when module A imports module B, and module B also
imports module A. Python cannot finish loading either file and raises an
ImportError. The fix is to move shared code into a third utility module, or to
place the import inside a function rather than at the top of the file.
Common Errors#
Error |
Meaning |
|---|---|
|
Variable is not defined anywhere in scope. |
|
Variable is used before being assigned; add |
|
Python cannot find the module on its search path. |
Exercises#
Create two files:
math_utils.pywith anaddfunction, andmain.pythat imports and uses it. Observe how the__name__ == "__main__"pattern works when you run each file directly vs. import it.Write a function that has a local variable with the same name as a global variable. Demonstrate that modifying the local variable does not affect the global one.
Write a function that correctly uses
globalto increment a counter each time it is called. Then rewrite it withoutglobalby returning the new value instead. Which version is cleaner?Describe a scenario where
nonlocalis necessary. Write a small example that demonstrates it.Design a package layout for a simple student gradebook system with at least three modules. Describe what each module would contain.