Of course! This is a fantastic and classic way to learn about Python's core mechanics. A "buggy module" is an excellent tool for understanding how Python resolves names, imports, and the difference between code running at import time versus runtime.

Let's create a step-by-step guide to building, using, and understanding a buggy module.
The Goal
We will create a Python module named buggymodule.py that is intentionally full of common Python bugs. Then, we'll create a main script, main.py, that imports and uses this module to see the errors in action.
Step 1: Create the Buggy Module (buggymodule.py)
This module will contain a variety of errors, from simple typos to more subtle logical issues.
Create a file named buggymodule.py and paste the following code into it. I've added comments explaining the bug in each section.

# buggymodule.py
# =====================================================================
# BUG 1: NameError at Import Time
# =====================================================================
# This code runs as soon as the module is imported.
# The variable 'some_other_variable' is not defined, causing a NameError.
print("--- BUG 1: NameError at Import Time ---")
# print(some_other_variable) # <-- UNCOMMENT THIS LINE TO SEE THE ERROR
# To fix it, either comment it out or define the variable first.
some_other_variable = "This is defined now."
print(f"BUG 1 is fixed. The variable is: {some_other_variable}\n")
# =====================================================================
# BUG 2: AttributeError
# =====================================================================
# This function tries to use a method that doesn't exist on a string object.
def process_string(text):
"""This function has a bug in it."""
print(f"--- BUG 2: AttributeError in process_string ---")
# The method '.upppercase()' does not exist. It should be '.upper()'.
# This error will only happen when the function is *called*, not on import.
return text.upppercase() # <-- CHANGE TO .upper() TO FIX
# Let's call the buggy function to see the error.
# process_string("hello") # <-- UNCOMMENT THIS LINE TO SEE THE ERROR
print("BUG 2 is fixed by changing 'upppercase' to 'upper'.\n")
# =====================================================================
# BUG 3: Logical Error (No Exception!)
# =====================================================================
# This function has a logical mistake. It runs without crashing but gives
# the wrong answer. This is often the hardest type of bug to find.
def add_numbers(a, b):
"""This function incorrectly adds numbers."""
print(f"--- BUG 3: Logical Error in add_numbers ---")
# The logic is wrong. It should be `return a + b`.
return a * b # <-- CHANGE TO + TO FIX
# Let's test the buggy function.
# print(f"The buggy result of 5 + 3 is: {add_numbers(5, 3)}") # <-- UNCOMMENT TO SEE
print("BUG 3 is fixed by changing '*' to '+'.\n")
# =====================================================================
# BUG 4: IndexError
# =====================================================================
# This function tries to access a list index that is out of bounds.
def get_third_item(items):
"""This function will crash if the list has less than 3 items."""
print(f"--- BUG 4: IndexError in get_third_item ---")
# If 'items' has 0, 1, or 2 elements, index 2 will not exist.
return items[2] # <-- This will raise an IndexError if len(items) < 3
# Let's call it with a list that is too small.
# short_list = ['a', 'b']
# print(f"The third item is: {get_third_item(short_list)}") # <-- UNCOMMENT TO SEE
print("BUG 4 can be fixed by checking the list length first.\n")
# =====================================================================
# BUG 5: Module-Level Name Shadowing
# =====================================================================
# This is a subtle bug where a variable name in a function "shadows"
# a variable defined at the module level.
MODULE_CONSTANT = 100
def get_constant():
"""This function accidentally uses a local variable instead of the module one."""
print(f"--- BUG 5: Name Shadowing in get_constant ---")
# By assigning to 'MODULE_CONSTANT' inside the function, Python
# creates a *new local variable* with that name, hiding the global one.
# This is a common source of bugs.
MODULE_CONSTANT = 50 # <-- This creates a LOCAL variable, not the global one.
return MODULE_CONSTANT
# Let's see the result.
# print(f"The returned constant is: {get_constant()}")
# print(f"The module-level constant is still: {MODULE_CONSTANT}") # <-- It's unchanged!
print("BUG 5 is fixed by using the 'global' keyword or a different variable name.\n")
# =====================================================================
# GOOD PRACTICE: Define a "public" API for the module
# =====================================================================
# To make our module useful, we should define what functions it's meant to export.
# A user of our module should only need to import what's necessary.
# We can fix our buggy functions and make them available.
def process_string_fixed(text):
"""The corrected version of process_string."""
return text.upper()
def add_numbers_fixed(a, b):
"""The corrected version of add_numbers."""
return a + b
def get_third_item_safe(items):
"""A safe version of get_third_item."""
if len(items) >= 3:
return items[2]
else:
return None # Return None if the item doesn't exist
# Now, a user of our module can choose to import the safe, fixed functions.
# from buggymodule import process_string_fixed, add_numbers_fixed
Step 2: Create the Main Script (main.py)
This script will import buggymodule and trigger the bugs one by one.
Create a file named main.py in the same directory as buggymodule.py and paste this code in:
# main.py
print("Starting the main script...")
# =====================================================================
# DEMONSTRATING THE BUGS
# =====================================================================
# BUG 1: NameError at Import Time
# If you uncommented the line in buggymodule.py, the import itself will fail here.
try:
import buggymodule
print("Successfully imported buggymodule.\n")
except NameError as e:
print(f"CRITICAL ERROR: Failed to import buggymodule due to a NameError: {e}\n")
# BUG 2: AttributeError (happens at runtime)
# This error only occurs when the buggy function is called.
print("--- Testing BUG 2: AttributeError ---")
try:
# We are calling the buggy version of the function from the module.
result = buggymodule.process_string("hello world")
print(f"Function result: {result}\n")
except AttributeError as e:
print(f"Caught an AttributeError as expected: {e}\n")
# BUG 3: Logical Error (No Exception!)
# This function runs but produces the wrong output. It won't raise an error.
print("--- Testing BUG 3: Logical Error ---")
buggy_sum = buggymodule.add_numbers(10, 5)
print(f"The buggy sum of 10 + 5 is: {buggy_sum} (This should be 15!)\n")
# BUG 4: IndexError (happens at runtime)
print("--- Testing BUG 4: IndexError ---")
try:
short_list = ['first', 'second']
third_item = buggymodule.get_third_item(short_list)
print(f"The third item is: {third_item}\n")
except IndexError as e:
print(f"Caught an IndexError as expected: {e}\n")
# BUG 5: Name Shadowing (subtle bug)
print("--- Testing BUG 5: Name Shadowing ---")
# The module's constant value is unchanged by the buggy function.
print(f"The module's constant is: {buggymodule.MODULE_CONSTANT}")
# The buggy function returns the local, incorrect value.
shadowed_value = buggymodule.get_constant()
print(f"The function returned: {shadowed_value} (This is the local shadow variable)\n")
# =====================================================================
# USING THE FIXED FUNCTIONS
# =====================================================================
print("--- Using the FIXED functions from the module ---")
# You can import the fixed functions directly.
from buggymodule import process_string_fixed, add_numbers_fixed, get_third_item_safe
print(f"Fixed string processing: {process_string_fixed('this works now!')}")
print(f"Fixed addition: {add_numbers_fixed(10, 5)}")
safe_list = ['a', 'b']
item = get_third_item_safe(safe_list)
print(f"Getting third item from a short list safely: {item}")
long_list = ['a', 'b', 'c', 'd']
item = get_third_item_safe(long_list)
print(f"Getting third item from a long list safely: {item}")
print("\nScript finished.")
Step 3: Run the Code and Analyze the Output
- Make sure both
main.pyandbuggymodule.pyare in the same folder. - Open your terminal or command prompt, navigate to that folder.
- Run the main script:
python main.py
Expected Output (with bugs active):
You will see the output from main.py interspersed with the print statements from buggymodule.py. The try...except blocks will catch the errors and print informative messages.
Starting the main script...
--- BUG 1: NameError at Import Time ---
BUG 1 is fixed. The variable is: This is defined now.
Successfully imported buggymodule.
--- Testing BUG 2: AttributeError ---
--- BUG 2: AttributeError in process_string ---
Caught an AttributeError as expected: 'str' object has no attribute 'upppercase'
--- Testing BUG 3: Logical Error ---
--- BUG 3: Logical Error in add_numbers ---
The buggy sum of 10 + 5 is: 50 (This should be 15!)
--- Testing BUG 4: IndexError ---
--- BUG 4: IndexError in get_third_item ---
Caught an IndexError as expected: list index out of range
--- Testing BUG 5: Name Shadowing ---
--- BUG 5: Name Shadowing in get_constant ---
The module's constant is: 100
The function returned: 50 (This is the local shadow variable)
--- Using the FIXED functions from the module ---
Fixed string processing: THIS WORKS NOW!
Fixed addition: 15
Getting third item from a short list safely: None
Getting third item from a long list safely: c
Script finished.
Key Takeaways
By creating and using buggymodule.py, you've learned:

- Import Time vs. Runtime: Code at the top level of a module runs when it's imported. Errors here will stop the import process. Errors inside functions only happen when the function is called.
- Different Error Types:
NameError: A variable or name is not defined in the current scope.AttributeError: You try to access an attribute or method that an object doesn't have (e.g.,"".foo()).IndexError: You try to access a list or tuple index that doesn't exist.Logical Error: The code runs but produces the wrong result. These require careful testing and review.
- Variable Scope (Shadowing): A variable defined inside a function is local to that function. If you have a global variable and a local variable with the same name, the local one "shadows" the global one. This is a very common source of bugs.
- Module APIs: Good modules provide a clean, predictable set of functions (a public API) for users. By defining fixed, "safe" functions and making them easy to import, you create a more robust and user-friendly library.
