Chapter 11: Debugging

Scene: The “Broken Promise”

Chaitanya runs into the computer lab, practically buzzing with energy.

Chaitanya: “Ma’am! I am ready for Web Scraping. I’m ready to pull live data off the internet!”

Aditi Ma’am is sitting at his terminal, arms crossed, staring at a massive wall of red text on the screen.

Aditi Ma’am: “I know I promised you the internet today, Chaitanya. But look at your screen. The automated backup script we wrote yesterday? You left it running overnight to process the entire school server.”

Chaitanya: “Did it finish?”

Aditi Ma’am: “No. At 2:14 AM, it encountered a folder named 0. It tried to divide a file size by that folder name, threw a ZeroDivisionError, and completely crashed. It stopped backing up. The rest of the server is unprotected.”

Chaitanya: “Oh no. I didn’t think anyone would name a folder ‘0’!”

Aditi Ma’am: “Users will always do things you don’t expect. When your code touches the real world, it will break. Before I let you loose on the unpredictable internet, you must learn how to perform surgery on broken code. Welcome to Debugging.”

Raising Exceptions (Crashing on Purpose)

Aditi Ma’am: “Right now, your code only crashes when Python itself gets confused—like trying to divide by zero or opening a file that doesn’t exist. But you can actually tell Python to crash on purpose if the data looks wrong to you. This is called Raising an Exception.”

Chaitanya: “Why would I want my program to crash?”

Aditi Ma’am: “Because a loud crash is better than a silent failure. Imagine a script that calculates student GPAs. If a glitch causes a student to get a GPA of 500 out of 10, you want the program to halt immediately before it saves that garbage data to the Principal’s database.”

Aditi Ma’am: “You raise an exception using the raise keyword, followed by the Exception() function.”

Python

def print_id_card(student_name, age):
    if age < 4 or age > 20:
        # Halt the program immediately!
        raise Exception('Invalid age for a high school student.')
    
    print(f'Printing ID Card for {student_name}...')

# Let's test it with bad data
print_id_card('Rahul', 45)

Chaitanya: (Runs the code)

Plaintext

Traceback (most recent call last):
  File "C:\SchoolSystem\id_maker.py", line 9, in <module>
    print_id_card('Rahul', 45)
  File "C:\SchoolSystem\id_maker.py", line 4, in print_id_card
    raise Exception('Invalid age for a high school student.')
Exception: Invalid age for a high school student.

Chaitanya: “It stopped the program and printed my exact error message!”

Aditi Ma’am: “Exactly. You set a trap for bad data. If the data triggers the trap, the program halts before any real damage is done.”

Reading Tracebacks (The Breadcrumb Trail)

Chaitanya: “Whenever my code crashes, I get that massive block of text called a Traceback. Honestly, I usually ignore it and just look at the very last line to see the error name.”

Aditi Ma’am: “That is a terrible habit. The Traceback is a map of exactly where your code was when it died. You read it from the bottom up.”

  1. The Last Line: Tells you what went wrong (Exception: Invalid age...).
  2. The Line Above It: Tells you the exact line of code that triggered the crash (raise Exception(...)).
  3. The Lines Above That: Tell you the “Call Stack.” It shows which function called the function that crashed.

Aditi Ma’am: “In a script with 50 different functions calling each other, the Traceback tells you exactly which path the computer took to reach the fatal error.”

Saving Tracebacks to a File

Chaitanya: “But Ma’am, if the backup script crashes at 2 AM while I’m asleep, I won’t see the Traceback on the screen. The terminal window might close, and the error will be lost forever.”

Aditi Ma’am: “Brilliant observation. That is why we use the traceback module. Instead of letting Python crash and print the error to the screen, we can catch the error and write the Traceback into a text file.”

Python

import traceback

try:
    # Simulating a crash at 2 AM
    raise Exception('Simulated server failure.')
except:
    errorFile = open('errorInfo.txt', 'w')
    # traceback.format_exc() gets the error text as a string
    errorFile.write(traceback.format_exc())
    errorFile.close()
    print('The traceback info was written to errorInfo.txt.')

Chaitanya: “So the program doesn’t actually crash? It catches the error, silently logs the massive red text into a notepad file, and keeps running?”

Aditi Ma’am: “Yes. When you arrive at school the next morning, you just open errorInfo.txt and read exactly what went wrong while you were sleeping.”

Assertions (The Programmer’s Sanity Check)

Aditi Ma’am: “Raising Exceptions is for user errors—like someone typing the wrong age. But there is another tool called an Assertion. This is a tool for you, the programmer. It’s a sanity check.”

Chaitanya: “How is it different from raise Exception?”

Aditi Ma’am: “An assert statement says: ‘I am 100% sure this condition is True. If it is False, there is a fundamental bug in my logic, and the program must crash immediately.’

Syntax: assert condition, 'Error message'

Python

podium_finishers = ['Alice', 'Bob', 'Rahul']

# We reverse the list so Alice is first, Bob is second, Rahul is third
podium_finishers.reverse()

# Sanity Check: Alice MUST be at index 0 if she won first place
assert podium_finishers[0] == 'Alice', 'Logic Error: Alice is not in first place!'

Chaitanya: (Runs the code)

Plaintext

Traceback (most recent call last):
  File "race.py", line 7, in <module>
    assert podium_finishers[0] == 'Alice', 'Logic Error: Alice is not in first place!'
AssertionError: Logic Error: Alice is not in first place!

Chaitanya: “It crashed with an AssertionError! Wait, why did it crash? I used reverse()!”

Aditi Ma’am: “Because reverse() flipped the list to ['Rahul', 'Bob', 'Alice']. Rahul is at index 0 now. Your logic was flawed. The assert statement caught your bad logic before you printed the wrong trophies.”

Chaitanya: “So I should use assert instead of try/except?”

Aditi Ma’am: “Never use assert for things outside your control.

  • If a file is missing, use an Exception. You can’t control the hard drive.
  • If user input is bad, use an Exception. You can’t control the user.
  • But if your own variable has the wrong value because your math is bad? Use an assert. It means you made a mistake.”

Aditi Ma’am: “Exceptions and Assertions are great for catching fatal errors. But what if your program isn’t crashing? What if it’s just doing the wrong math, silently?”

Chaitanya: “I usually just scatter print() statements everywhere. Like print('Value of x is:', x) to see what the variables are doing.”

Aditi Ma’am: “That is the debugging equivalent of duct tape. It’s messy, you have to delete them all later, and if you forget one, your final program will print garbage to the screen.”

Chaitanya: “Is there a better way?”

Aditi Ma’am: “Yes. In Part 2, I will teach you how to use the logging module. It is a professional dashboard for your code, and you can turn it on and off with a single switch.”


PART 2

Scene: The “Print Statement” Mess

Chaitanya is staring at his terminal. The output of his script is a chaotic wall of text.

Plaintext

Made it to line 10
Value of i is 1
Value of i is 2
Here!
Why is total 0??
Value of i is 3
End of loop

Aditi Ma’am points at the screen. “Chaitanya, what is this garbage?”

Chaitanya: “My script to calculate student percentiles isn’t working. The final number is wrong. So I put print() statements everywhere to track the variables and see what the code is doing.”

Aditi Ma’am: “And when you finally fix the bug, what are you going to do?”

Chaitanya: “I guess I’ll have to go back through all 200 lines of code and manually delete every single print() statement so the users don’t see them.”

Aditi Ma’am: “And what if you accidentally delete a print() statement that was actually supposed to show the final grade to the user?”

Chaitanya: “Then I break the program again.”

Aditi Ma’am: “Exactly. Using print() to debug is like using duct tape to fix an airplane engine. It’s messy, it mixes your debugging notes with the actual program output, and it’s dangerous to remove. Professionals use the logging module. It creates a completely separate stream of information that you can turn on and off with a single line of code.”

Setting Up the logging Module

Aditi Ma’am: “To use the logging module, you import it and set up its basic configuration at the very top of your script.”

Python

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')

Chaitanya: “That format string looks complicated.”

Aditi Ma’am: “You don’t need to memorize it. You just copy and paste it. It tells Python: ‘Every time I log a message, automatically print the current Date, Time, the Level of the error, and my Message.'”

Finding a Bug with logging

Aditi Ma’am: “Let’s look at a classic buggy program. I want to calculate the factorial of a number. (5 factorial is 5 × 4 × 3 × 2 × 1 = 120). Look at this code.”

Python

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug(f'Start of factorial({n})')
    total = 1
    for i in range(n + 1):
        total *= i
        logging.debug(f'i is {i}, total is {total}')
    logging.debug(f'End of factorial({n})')
    return total

print(factorial(5))
logging.debug('End of program')

Chaitanya: “I see. You replaced print('i is', i) with logging.debug(f'i is {i}...').”

Aditi Ma’am: “Run it. Let’s see why it’s broken.”

Output:

Plaintext

 2026-02-24 15:25:10,123 - DEBUG - Start of program
 2026-02-24 15:25:10,124 - DEBUG - Start of factorial(5)
 2026-02-24 15:25:10,124 - DEBUG - i is 0, total is 0
 2026-02-24 15:25:10,125 - DEBUG - i is 1, total is 0
 2026-02-24 15:25:10,125 - DEBUG - i is 2, total is 0
 ...
 0

Chaitanya: “Ah! Look at the logs! i is 0, total is 0. Because range(n + 1) starts at 0, the very first thing it does is multiply total by 0. After that, the total stays 0 forever!”

Aditi Ma’am: “Precisely. The log timestamps show you exactly how the variables mutated at every step. To fix it, you just change range(n + 1) to range(1, n + 1) so it skips zero.”

The 5 Levels of Logging

Chaitanya: “But wait, how is this better than print? It’s still printing all that debugging text to my screen.”

Aditi Ma’am: “Because logging has Levels. Think of them like alarms in the school building.”

  1. DEBUG: The lowest level. Minor details. (Like a whisper: ‘The loop started.’)
  2. INFO: General events. (Like an announcement: ‘The file has been saved.’)
  3. WARNING: A potential problem that didn’t stop the program. (Like a yellow light: ‘User typed an invalid age, defaulting to 0.’)
  4. ERROR: A specific function failed. (Like a siren: ‘Could not save to database.’)
  5. CRITICAL: The highest level. A fatal failure. (Like a fire alarm: ‘Hard drive is full, program shutting down!’)

Chaitanya: “So I can use different functions for different problems?”

Aditi Ma’am: “Yes. Instead of just logging.debug(), you can call logging.warning('Disk space low') or logging.critical('Server disconnected'). And here is the magic trick.”

The Magic Switch: logging.disable()

Aditi Ma’am: “When your script is finally fixed and ready for the Principal to use, you do NOT delete the logging lines. You just turn them off.”

Python

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')

# The Magic Switch - Turns off ALL logs at this level and below!
logging.disable(logging.CRITICAL) 

Chaitanya: “Wait. logging.disable(logging.CRITICAL) disables Critical logs AND everything below it? So it silences Errors, Warnings, Info, and Debugs all at once?”

Aditi Ma’am: “Yes. You add that one single line of code, and instantly, your program goes completely silent. The only things that print to the screen are your actual, intentional print() statements for the user. The duct tape disappears.”

Chaitanya: “And if the bug comes back next week?”

Aditi Ma’am: “You just comment out the disable() line, and all your diagnostic logs instantly come back to life. No re-typing required.”

Logging to a File

Chaitanya: “What if I want the logs to turn on, but I don’t want the user to see them on the screen? Can I send the logs to a text file, like we did with the Tracebacks earlier?”

Aditi Ma’am: “Yes. You just add the filename argument to your basicConfig setup.”

Python

import logging
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')

Aditi Ma’am: “Now, your screen stays perfectly clean for the user. But in the background, Python is quietly writing a highly detailed, timestamped diary of everything the program is doing into myProgramLog.txt.”

Chaitanya: “That is incredibly professional. I can just ask users to email me their log file if the program breaks.”

Aditi Ma’am: “Exactly. You have stopped treating errors as annoyances and started treating them as data.”


Aditi Ma’am: “You now know how to trap bad data with Exceptions and track variables with Logging. But what if you want to freeze time? What if you want to pause your Python script mid-execution, look around inside its memory, and execute lines of code one by one in slow motion?”

Chaitanya: “Is that even possible?”

Aditi Ma’am: “Yes. In Part 3, I will teach you the most powerful tool in your arsenal: The Debugger. We are going to step inside the Matrix.”


PART 3

Scene: Freezing Time

Chaitanya is extremely frustrated. He has written a simple “Coin Toss Guessing Game,” but no matter what he types, he always loses.

Python

import random
guess = ''

while guess not in ('heads', 'tails'):
    print('Guess the coin toss! Enter heads or tails:')
    guess = input()

toss = random.randint(0, 1) # 0 is tails, 1 is heads

if toss == guess:
    print('You got it!')
else:
    print('Nope! You lose.')

Chaitanya: “Ma’am, I even added logging.debug() to print the values. Sometimes I guess ‘heads’, and the toss is clearly ‘heads’, but the if statement still triggers the else block and tells me I lost! The logic is broken.”

Aditi Ma’am: “Your logic isn’t broken, Chaitanya. Your perception of the data is. When print() and logging fail you, it is time to stop looking at the aftermath of a crash and step inside the Matrix itself. We are going to use the Debugger.”

The Debugger: A Security Camera for Code

Aditi Ma’am: “Every good code editor—whether it’s VS Code, PyCharm, or even Python’s basic IDLE—has a built-in Debugger. It allows you to freeze your program mid-execution, look around at every variable sitting in RAM, and then execute your code one single line at a time, like stepping through security footage frame by frame.”

Chaitanya: “How do I turn it on?”

Aditi Ma’am: “In your editor, instead of clicking ‘Run’, you click ‘Debug’ (or ‘Run with Debugging’). The program will start, but it will instantly pause on the very first line of code. It waits for your command.”

The Five Commands of Time Travel

Aditi Ma’am: “When time is frozen, you have a control panel with five main buttons. Memorize these.”

  1. Continue (or Go): Unfreezes time. The program runs normally until it finishes, or until it hits a Breakpoint (we’ll cover that in a minute).
  2. Step Into: Executes the currently highlighted line of code and pauses on the very next line. If the current line is a function call (like factorial(5)), it physically steps inside that function so you can walk through it.
  3. Step Over: Executes the current line and pauses on the next line. BUT, if the line is a function, it executes the whole function at normal speed and pauses after it finishes. (Use this to skip over built-in functions like print() so you don’t waste time exploring Python’s internal code).
  4. Step Out: If you accidentally ‘Stepped Into’ a function and want to leave, this runs the rest of the function at full speed and pauses the moment you exit it.
  5. Stop (or Quit): Instantly kills the program.

Finding the Coin Toss Bug

Aditi Ma’am: “Start the Debugger on your Coin Toss game.”

Chaitanya: (Clicks Debug) “Okay, the screen is split. My code is on the left, and there is a new panel on the right called ‘Locals’. The very first line, import random, is highlighted in yellow.”

Aditi Ma’am: “The ‘Locals’ window shows you the exact value of every variable in your computer’s memory at this exact frozen millisecond. Now, click Step Over until you reach the if toss == guess: line. Answer the input() prompt when it asks.”

Chaitanya: (Clicks Step Over a few times, types ‘heads’ into the console) “Alright, time is frozen on the if statement. I guessed ‘heads’.”

Aditi Ma’am: “Now, look at your ‘Locals’ window. What does it say?”

Chaitanya: “It says… guess = 'heads'. And toss = 1.”

Aditi Ma’am: “And what happens when Python evaluates 'heads' == 1?”

Chaitanya: (Eyes go wide) “It’s False. Always. I was comparing a string to an integer! toss is a number from random.randint(), but guess is a string from input(). They will never match. That’s why I always lost!”

Aditi Ma’am: “Exactly. When you used print(toss), you saw a 1 on the screen and assumed it meant ‘heads’. The Debugger showed you the raw, unvarnished truth of the data types. You fixed it in five seconds.”

Chaitanya: “I just need to change the code to if toss == 1 and guess == 'heads':.”

Breakpoints (The Stop Signs)

Chaitanya: “Ma’am, this is powerful. But what if my script is 10,000 lines long, and the bug is on line 8,500? Do I have to click ‘Step Over’ eight thousand times?”

Aditi Ma’am: “No. For that, we use Breakpoints. A Breakpoint is a red stop sign you place on a specific line of code. You tell the Debugger: ‘Run at full speed, but slam on the brakes the absolute millisecond you reach this exact line.'”

Chaitanya: “How do I set one?”

Aditi Ma’am: “In almost every code editor, you just click the empty margin to the left of the line number. A red dot will appear.”

Python

import random
# ... (1000 lines of setup code) ...

# -> (RED DOT HERE) <-
final_score = calculate_percentile(student_grades) 

Aditi Ma’am: “Once the red dot is there, you click the Continue (Go) button. The program processes all 1,000 lines of setup instantly, and then suddenly freezes right on the red dot. Now you can check your Variables window and start ‘Stepping’ slowly through the danger zone.”

Chaitanya: “This changes everything. I don’t have to guess what my code is doing anymore. I can just watch it happen.”

Aditi Ma’am: “You have graduated from writing code to understanding code. You are no longer flying blind.”

Summary Box (Chapter 11)

  • Exceptions: Use raise Exception('message') to intentionally crash the program when user data is invalid.
  • Tracebacks: Read them from the bottom up to see the exact sequence of function calls that caused a crash. Use traceback.format_exc() to save them to a .txt file.
  • Assertions: Use assert condition, 'message' as a sanity check for your own logic. If it fails, your math or logic is fundamentally wrong.
  • Logging: Stop using print() for debugging. Use the logging module to track variables. Turn them all off instantly with logging.disable().
  • The Debugger: Use your IDE’s debugger to freeze time. Use Breakpoints to jump to the problem, and Step Into/Over to watch your variables change line by line.

Aditi Ma’am: “The training wheels are officially off, Chaitanya. You know how to build programs, store data, and fix fatal errors. Now, it is time to unleash your scripts on the outside world.”

Chaitanya: “Web Scraping?”

Aditi Ma’am: “Yes. Tomorrow, in Chapter 12: Web Scraping, you will learn how to write programs that open browsers, click buttons, download files, and strip data off websites automatically. The entire internet is about to become your database.”


Leave a Comment

💬 Join Telegram