Lab 1: The “Global” Keyword Switch Try to modify a configuration setting from inside a function.
config_mode = "DEFAULT"
def switch_to_debug():
global config_mode # Unlock access to the global variable
config_mode = "DEBUG"
print("Inside function: Mode switched to", config_mode)
print("Before:", config_mode)
switch_to_debug()
print("After:", config_mode)
# Output: After: DEBUG (It permanently changed)
Lab 2: The “Shadowing” Experiment See how local variables protect themselves from the outside world.
user = "Admin" # Global
def login_guest():
user = "Guest" # This is a NEW local variable. It does not touch the global 'user'.
print(f"Logged in as: {user}")
login_guest() # Output: Logged in as: Guest
print(f"Global User is still: {user}") # Output: Global User is still: Admin
Lab 3: The nonlocal Keyword (Nested Functions)
Scenario: You are writing a “Retry Logic” wrapper for a shaky API connection. You need a counter that tracks retries, but this counter lives inside a helper function, not globally.
Concept: The nonlocal keyword allows a nested function to modify a variable in the outer function (not the global scope).
def connection_manager():
# Enclosing scope variable
attempts = 0
def try_connect():
nonlocal attempts # Unlock access to the outer variable
attempts += 1
print(f"Attempt #{attempts}: Connecting to AWS...")
return try_connect
# Create the specific connection instance
retry = connection_manager()
retry() # Output: Attempt #1: Connecting to AWS...
retry() # Output: Attempt #2: Connecting to AWS...
retry() # Output: Attempt #3: Connecting to AWS...
- Architect Note: If we used
global, it would mess up other connection managers running in parallel.nonlocalkeeps the state isolated to this specific instance. This is how “Closures” work in Python.
Lab 4: The “Mutable Default Argument” Trap (Critical!)
Scenario: You have a function to add a security tag to a list of servers. You set an empty list [] as the default value.
The Bug: Python creates default arguments once when the function is defined, not every time it runs. This leads to data leaking between function calls.
# BAD PRACTICE (Don't do this)
def register_server(name, server_list=[]):
server_list.append(name)
return server_list
# First call - looks fine
print(register_server("Web-01"))
# Output: ['Web-01']
# Second call - checking a totally different server...
# WAIT! Why is Web-01 still there?
print(register_server("DB-01"))
# Output: ['Web-01', 'DB-01'] -> Data Leak!
# CORRECT PRACTICE (Do this)
def register_server_safe(name, server_list=None):
if server_list is None:
server_list = [] # Create a fresh list every time
server_list.append(name)
return server_list
print(register_server_safe("Web-01")) # ['Web-01']
print(register_server_safe("DB-01")) # ['DB-01'] -> Clean!
- Architect Note: This specific bug has caused major security incidents where one user sees another user’s data. Always use
Noneas a default for lists or dictionaries.
Lab 5: Scope in Loops (The “Leaky Variable”)
Scenario: You are iterating through a list of old log files to delete them. You print the last file name after the loop finishes.
Concept: In Python, variables defined inside a for loop leak out to the surrounding scope (unlike C++ or Java).
file_name = "System-Critical-Config.xml" # Important file
files_to_delete = ["temp1.log", "temp2.log", "error.log"]
for file_name in files_to_delete:
print(f"Deleting {file_name}...")
# The loop is finished. What is 'file_name' now?
print(f"Warning! The variable 'file_name' is now: {file_name}")
# Output: Warning! The variable 'file_name' is now: error.log
- Architect Note: Be careful! The loop variable
file_nameoverwrote your importantfile_namevariable from the top. Always choose unique names for loop variables (e.g.,for target_file in files_to_delete:).
Lab 6: The globals() and locals() Debugger
Scenario: You are debugging a complex script and want to see everything that is currently alive in memory.
Concept: Python has built-in functions that return a dictionary of all current variables.
x = 100
y = "DevOps"
def debugger_tool():
z = 3.14
print("--- Local Variables (Inside Function) ---")
print(locals())
debugger_tool()
print("\n--- Global Variables (Selected) ---")
# globals() is huge, so let's just check for our specific variable
current_globals = globals()
if 'x' in current_globals:
print(f"Found Global x: {current_globals['x']}")
#Output
--- Local Variables (Inside Function) ---
{'z': 3.14}
--- Global Variables (Selected) ---
Found Global x: 100