Skip to Content
Course content

149: Remote Debugging Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

There comes a point in every project where print() statements just aren't enough. You've got a bug that only happens on your staging server or inside a Docker container, and trying to replicate the exact environment locally is a nightmare. This is where remote debugging comes in. Instead of guessing what's happening, we're going to let our local IDE "reach into" the running process on the remote machine.

For this, we'll use debugpy. It's the engine that powers the Python debugger in VS Code and is basically the industry standard for this workflow.

Setting up a buggy weather aggregator

Let's imagine we have a small script that aggregates weather data from a remote API. I've written this to run on a remote server (or a container), but it's throwing a KeyError that I can't quite pin down because the input data varies.

# weather_service.py
import requests

def process_weather(city):
    # Imagine this is a real API call
    data = requests.get(f"https://api.example.com/weather/{city}").json()
    
    # This is where the crash happens
    temp = data['main']['temp'] 
    return f"The temperature in {city} is {temp} degrees."

if __name__ == "__main__":
    cities = ["London", "New York", "UnknownCity"]
    for city in cities:
        print(process_weather(city))

In a real scenario, UnknownCity might return a different JSON structure (like an error message) that doesn't have a 'main' key. Since this is running on a remote box, I don't want to keep restarting the server and adding prints. I want to pause the code execution right there and inspect the data variable.

Injecting the debug listener

To make the script "debuggable" from the outside, we need to tell Python to open a port and wait for a debugger to attach to it. I'll add debugpy to the start of the script.

import debugpy
import requests

# Listen for a connection on port 5678
debugpy.listen(("0.0.0.0", 5678))
print("Waiting for debugger to attach...")
debugpy.wait_for_client() # The script will freeze here until we connect
print("Debugger attached! Continuing execution...")

def process_weather(city):
    # ... (rest of the code)

I used 0.0.0.0 because that tells the listener to accept connections from any network interface, which is necessary when the debugger is on your laptop and the code is on a server.

The "Where is my breakpoint?" moment

Now, here is where I usually trip up the first time I do this. I ran the script on my remote server, saw the "Waiting for debugger..." message, and then I tried to connect via my IDE. But... nothing happened. The breakpoints weren't hitting.

I spent ten minutes checking my code before I realized the obvious: I forgot that the remote server has a firewall. Port 5678 was closed. If you're using AWS or a similar cloud provider, you have to explicitly open that TCP port in your security groups. Once I opened the port, the IDE connected instantly, and the script moved past the wait_for_client() call.

Hunting the KeyError in real-time

With the debugger attached, I set a breakpoint on the line temp = data['main']['temp']. When the loop hit UnknownCity, the execution paused. I looked at the data variable in my IDE's watch window and saw this:

{
    "cod": 404,
    "message": "city not found"
}

There's the culprit. There is no 'main' key because the API returned an error response. I can now fix this by adding a check for the cod key or using data.get('main'). I didn't have to restart the server or guess the payload; I saw the actual live state of the remote memory.




📋 Practical Task

Fixing the Remote API Timeout Bug

You have a script running on a remote server that processes a list of user IDs. It's occasionally crashing with a TypeError when a user profile is incomplete. Your goal is to set up remote debugging to find the offending data.

Your Task:

  1. Modify the provided script below to include a debugpy listener on port 5678.
  2. Ensure the script pauses execution and waits for the debugger to attach before it enters the for loop.
  3. Identify which user_id causes the crash by inspecting the profile variable during the remote session.
# user_processor.py
import time

# TODO: Add debugpy setup here

def fetch_profile(user_id):
    # Simulated database of users
    db = {
        1: {"name": "Alice", "email": "alice@example.com"},
        2: {"name": "Bob"}, # Missing email!
        3: {"name": "Charlie", "email": "charlie@example.com"},
    }
    return db.get(user_id)

def send_welcome_email(user_id):
    profile = fetch_profile(user_id)
    # This line is causing the TypeError
    print(f"Sending email to {profile['email'].lower()}...")

if __name__ == "__main__":
    users = [1, 2, 3]
    for u in users:
        send_welcome_email(u)
        time.sleep(1)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.