Python Debugging in VS Code: Breakpoints Instead of print
Ever hunted a bug by inserting print(variable), running, deleting it, putting it somewhere else, and running again? I did that for a long time. The problem is that this approach shows you one thing you were curious about per run. A breakpoint shows you everything at that moment, all at once.
The Basics — F5 and F9 Get You Started
A breakpoint is a marker saying "pause the program when you reach this line." Click the gutter to the left of the line number, or press F9, and a red dot appears.
Run with F5 and it halts on that line, unfolding every local variable at that instant in the left panel. Dictionaries, nested objects—you can expand all of it. print shows you only the one variable you anticipated; a breakpoint shows you the variables you didn't anticipate too. And bugs usually live on the unanticipated side.
Once paused, four controls cover it.
- F10 Step Over — next line. Function calls execute whole and you move past them
- F11 Step Into — descend into the function
- Shift+F11 Step Out — finish the current function and return to the caller
- F5 Continue — run on to the next breakpoint
launch.json — Pin Down Your Run Configuration
Bots normally take arguments and environment variables. Rather than typing them each time, store them in .vscode/launch.json.
{
"version": "0.2.0",
"configurations": [
{
"name": "Bot - paper mode",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/bot.py",
"args": ["--mode", "paper", "--config", "config.ini"],
"env": {
"EXCHANGE_API_KEY": "${env:EXCHANGE_API_KEY}"
},
"console": "integratedTerminal",
"justMyCode": true
}
]
}
With justMyCode at its default of true, stepping won't descend into library internals. That's usually what you want, but set it to false when you're chasing an error raised inside a library.
A caution: don't write API keys directly into env. Reference them with ${env:...} as above, or use envFile—and depending on your setup, .vscode/ belongs in .gitignore too.
Conditional Breakpoints — This Changes the Game
This is the real weapon. In a loop that spins dozens of times per second, you often want to stop only at the exact moment things go wrong. A plain breakpoint halts hundreds of times and is unusable.
Right-click the breakpoint → Edit Breakpoint and attach a condition.
# Example conditions — plain Python expressions
gap_pct > 3.0 # only when the gap is abnormally wide
symbol == "TARGET_SYMBOL" # only for a specific symbol
order is None and retry_count >= 3 # only after three failed retries
There's a hit count condition as well. Set it to >= 500 and it starts pausing from the 500th pass. Perfect for bugs of the "runs fine for a while, then goes wrong" variety.
In practice this one feature replaces every bit of scratch code like if condition: import pdb; pdb.set_trace(). You never touch the source, so you can't forget to remove it.
Logpoints — print Without Stopping
Real-time bots often can't afford to pause. Quotes keep arriving; stop for five seconds and the situation has already changed. This is where logpoints come in.
Right-click the breakpoint → choose Log Message and write something like this.
entry check: {symbol} gap={gap_pct:.3f} balance={balance}
What's inside the braces is substituted with real values and printed to the debug console. The program never stops. You get the effect of a print statement while changing not one character of code. When you're done, delete the logpoint—no risk of debug code slipping into a commit.
Watch Expressions and Exception Breakpoints
Register expressions in the Watch panel and they recompute automatically on every pause. Not just plain variables—full expressions work.
len(open_positions)
sum(p.qty * p.price for p in open_positions)
(ask - bid) / bid * 100
The debug console lets you run arbitrary code in the paused context. You can even call a function directly to inspect its return value. But never call code with side effects—like an order-submission function—from the console. On a live account, that order really goes out.
And one underused but powerful feature: the "Raised Exceptions" checkbox in the breakpoints panel. Turn it on and execution halts on the exact line where an exception is raised. It catches exceptions swallowed by try/except as well, making it an instant fix for the "why is this failing silently?" class of bug.
Attaching to an Already-Running Bot
When a bot that's been running for days on a server starts looking off, restarting it throws away the state. Use remote debugging to attach instead.
Plant this in the bot code ahead of time.
import os
if os.environ.get("DEBUG_ATTACH") == "1":
import debugpy
debugpy.listen(("127.0.0.1", 5678)) # never expose externally
print("debugger waiting... port 5678")
# debugpy.wait_for_client() # only if you want to catch it from startup
Then add an attach configuration to launch.json.
{
"name": "Attach to running bot",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"pathMappings": [
{ "localRoot": "${workspaceFolder}", "remoteRoot": "/app" }
]
}
One security warning. Bind the listen address to 127.0.0.1, always. Open it on 0.0.0.0 and anyone on the internet can attach and execute arbitrary code inside your bot process. For a remote server, the correct approach is tunneling in over SSH port forwarding.
Summary
printgives you one thing at a time; a breakpoint gives you everything at that instant- Pin run arguments and environment variables in
launch.json(but never write keys inline) - Conditional breakpoints plus hit counts are the core weapon for debugging loops
- For real-time bots that must not pause, use logpoints — print without editing code
- Catch silent failures with Raised Exceptions breaking
- Attach to a running bot with
debugpy, but bind to127.0.0.1only - Never call order functions from the debug console — they really execute
Thirty minutes is enough to learn breakpoints properly. From that point on, your debugging speed changes noticeably—and so does the habit of accidentally committing leftover print statements.
댓글
댓글 쓰기