Python Debugging in VS Code: Breakpoints Instead of print

한국어로 읽기

TECH INSIGHT / EXPLAINED

Breakpoints are useful when you can reproduce a failure with the same input. Start with a tiny Python program that cannot place orders, then connect the paused line, local variables, and call stack.

An instructor explains paused code and variable values to two colleagues.
AI-generated conceptual illustration. An instructor explains paused code and variable values to two colleagues.

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.

Check the selected interpreter and Python Debugger extension, then start with F5. A pause exposes variables in the selected stack frame. Do not treat that view as a consistent snapshot of the whole system: other threads and external services have separate state.

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.

Conditional breakpoints narrow a reproduction to a particular input or iteration. Avoid calling functions with side effects in a breakpoint expression.

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: {symbol} gap={round(gap_pct, 3)} balance={balance}

Braces evaluate Python expressions. Logpoints avoid an interactive pause, but expression evaluation and output still have overhead. They do not guarantee unchanged timing, and they can expose sensitive values.

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

Attaching connects a debugger to a running process. Pausing a live order loop can cause timeouts or disconnects, so first reproduce the issue in a paper environment. The following code prepares a controlled debugging environment; it is not a permanently public service.

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" }
  ]
}

A debugger can execute code with the process permissions. This example listens on localhost; use an access-controlled SSH tunnel for remote access. Binding to 0.0.0.0 listens on all interfaces, while actual external reachability also depends on firewalls and routing. Shut down the debugging listener when finished.

Reproduce a division-by-zero failure

The example intentionally raises ZeroDivisionError on its third iteration. Put a conditional breakpoint on change with previous == 0. Just before division, previous is 0 and current is 105.

prices = [100, 102, 0, 105]
for previous, current in zip(prices, prices[1:]):
    change = (current - previous) / previous
    print(change)

The fix is not merely hiding the exception. Decide whether zero is valid or represents missing data, then explicitly validate, skip, or return an error. Run the same input again to verify the chosen behavior.

References

Content and examples reviewed: 2026-09-29

댓글