Debugging: Finding It Yourself
A practical guide to debugging without asking someone else first — how to isolate, verify, and actually understand your bugs.
Most beginners treat a bug like a wall. They hit it, stop, and ask someone else to climb over for them. The habit that actually builds skill is different: you learn to treat a bug like a question with a findable answer, and you go find it before you go asking.
Read the error message like it's evidence, not noise
Stack traces get skipped over constantly because they look scary. But they're usually telling you exactly where and why something broke. If you get TypeError: 'NoneType' object is not subscriptable in Python, that's not random gibberish — it means a variable you expected to hold a list or dict is actually None. The line number tells you where. Your job is to trace backward from that line and find where the value should have been set but wasn't.
Do this before searching anything online: read the last line of the traceback first, then work up. The last line usually names the actual exception. The lines above it show the call chain that got you there.
Reproduce it on purpose
If a bug only shows up sometimes, you don't understand it yet. Before touching code, try to make it happen reliably. Change one input at a time. Does it fail with an empty list but not a full one? Does it fail only on the second call to a function, not the first? A bug you can reproduce on command is a bug that's 80% solved, because now you can test whether a fix actually worked instead of guessing.
Cut the problem in half, then half again
Binary search isn't just for sorted arrays — it's the fastest way to find broken code in a long function or pipeline. Comment out or bypass the second half of your logic and check if the first half still produces the bug. If yes, the problem is in the first half. If no, it's in the second. Repeat. This works for a 200-line script just as well as it works for a multi-stage data pipeline where you're checking outputs at each stage with print() or df.head().
This beats randomly changing lines and rerunning, which is what most people do under pressure and it wastes far more time than it saves.
Use the debugger instead of print() when print() stops working
print() statements are fine for simple cases, but once you're chasing state across multiple function calls, an actual debugger saves real time. In Python, drop import pdb; pdb.set_trace() right before the suspicious line, run the script, and you get a live prompt where you can inspect variables, step line by line with n, and step into function calls with s. VS Code's built-in debugger does the same thing with breakpoints you click instead of type. Either way, the goal is the same: watch the actual state of your program instead of guessing what it probably is.
Isolate it from the rest of your project
If a bug happens inside a large codebase, don't debug it inside the large codebase. Copy the relevant function into a fresh file with fake, minimal input that triggers the same failure. If the bug disappears, something about the surrounding context — a global variable, an import, stale state — is the real cause, and you've just learned something important. If the bug persists in isolation, you now have a small, shareable, testable case, and you're much closer to the actual fix.
Check your assumptions against reality, not memory
A huge share of debugging time goes to unquestioned assumptions:
Written with AI assistance, reviewed and published by Michal Pilch (CISSP), Korra Studio.
This is one note from the Korra Studio knowledge base — the platform pairs every topic with 1-to-1 mentoring.
Get started freearrow_forward