# How to debug advanced Python issues in Replit

- Tool: Replit
- Difficulty: Beginner
- Time required: 15 minutes
- Compatibility: All Replit plans (Starter, Core, Pro, Enterprise)
- Last updated: March 2026

## TL;DR

Replit supports Python debugging through pdb (Python Debugger), the logging module, traceback analysis, and memory profiling with tracemalloc. Set breakpoints with pdb.set_trace() or breakpoint(), read stack traces to identify the exact line and function causing errors, replace print statements with structured logging, and use tracemalloc to find memory leaks. All of these tools work directly in Replit's Shell and Console without any extensions.

## How to Debug Advanced Python Issues in Replit

Debugging Python goes beyond adding print statements. This tutorial covers four professional debugging techniques that work inside Replit: the built-in pdb debugger for stepping through code line by line, structured logging for tracking application behavior, traceback analysis for reading error messages effectively, and tracemalloc for identifying memory issues. These skills are essential as your Python projects grow more complex.

## Before you start

- A Replit account with a Python project (any plan)
- Basic Python knowledge including functions, variables, and imports
- Access to the Shell tab in the Replit workspace
- Familiarity with running Python scripts from the command line

## Step-by-step guide

### 1. Use pdb to set breakpoints and step through code

Python's built-in debugger pdb lets you pause execution at any line, inspect variables, and step through code one line at a time. Add breakpoint() (Python 3.7+) or import pdb; pdb.set_trace() at the line where you want to pause. Run your script from the Shell (not the Run button) so you can interact with the debugger. When execution pauses, use commands like n (next line), s (step into function), p variable (print value), c (continue), and q (quit). This is far more effective than scattering print statements.

```
# example.py
def calculate_total(items):
    total = 0
    for item in items:
        breakpoint()  # Execution pauses here
        total += item['price'] * item['quantity']
    return total

items = [
    {'name': 'Widget', 'price': 9.99, 'quantity': 3},
    {'name': 'Gadget', 'price': 24.99, 'quantity': 1},
    {'name': 'Doohickey', 'price': None, 'quantity': 2},  # Bug: None price
]

result = calculate_total(items)
print(f'Total: ${result:.2f}')
```

**Expected result:** Execution pauses at the breakpoint. You can type p item to see the current item, p total to see the running total, and n to proceed to the next line.

### 2. Read Python traceback messages to locate errors

When Python crashes, it prints a traceback showing the chain of function calls that led to the error. Read tracebacks from bottom to top: the last line is the actual error type and message, and the lines above show each function call with file name and line number. The most recent call is at the bottom. Common error types are TypeError (wrong data type), KeyError (missing dictionary key), AttributeError (calling a method on the wrong type), and IndexError (list index out of range). Understanding tracebacks lets you go straight to the bug instead of guessing.

```
# This code produces a traceback:
def get_user_email(users, user_id):
    user = users[user_id]        # Line that crashes
    return user['email']

users = {'alice': {'email': 'alice@example.com'}}
email = get_user_email(users, 'bob')  # 'bob' not in dict

# Traceback output:
# Traceback (most recent call last):
#   File "main.py", line 6, in <module>
#     email = get_user_email(users, 'bob')
#   File "main.py", line 2, in get_user_email
#     user = users[user_id]
# KeyError: 'bob'

# Fix: use .get() with a default
def get_user_email(users, user_id):
    user = users.get(user_id)
    if user is None:
        return None
    return user['email']
```

**Expected result:** You can read the traceback, identify that the error is a KeyError on line 2 caused by 'bob' not existing in the dictionary, and apply the fix.

### 3. Replace print statements with the logging module

The logging module provides structured, leveled output that you can filter by severity. Instead of print('something happened'), use logging.info('something happened'). Logging supports levels: DEBUG (detailed), INFO (general), WARNING (potential issues), ERROR (failures), and CRITICAL (fatal). You can configure it to include timestamps, file names, and line numbers automatically. Unlike print statements, logging can be turned off for production by changing the log level without removing any code.

```
import logging

# Configure logging with timestamps and levels
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%H:%M:%S'
)

logger = logging.getLogger('myapp')

def process_order(order):
    logger.info(f"Processing order {order['id']}")
    logger.debug(f"Order details: {order}")

    if order['total'] <= 0:
        logger.warning(f"Order {order['id']} has zero or negative total")
        return False

    try:
        # Simulate payment processing
        charge_payment(order)
        logger.info(f"Order {order['id']} completed successfully")
        return True
    except Exception as e:
        logger.error(f"Payment failed for order {order['id']}: {e}")
        return False
```

**Expected result:** Your output shows timestamped, leveled messages like '14:32:01 [INFO] myapp: Processing order 123' that are easy to filter and search.

### 4. Profile memory usage with tracemalloc

If your Replit app crashes with 'Your Repl ran out of memory', you need to find what is consuming RAM. Python's built-in tracemalloc module tracks memory allocations and shows you exactly which lines of code allocate the most memory. Start tracing at the beginning of your script, run the memory-intensive operation, then take a snapshot and display the top allocations. This is especially important on Replit's Starter plan with only 2 GiB RAM.

```
import tracemalloc

# Start tracing memory allocations
tracemalloc.start()

def load_large_dataset():
    # Simulating a memory-heavy operation
    data = []
    for i in range(100000):
        data.append({'id': i, 'value': f'item_{i}' * 100})
    return data

# Run the operation
result = load_large_dataset()

# Take a snapshot and show top memory consumers
snapshot = tracemalloc.take_snapshot()
stats = snapshot.statistics('lineno')

print('\nTop 10 memory allocations:')
for stat in stats[:10]:
    print(f'  {stat}')

# Show current and peak memory usage
current, peak = tracemalloc.get_traced_memory()
print(f'\nCurrent memory: {current / 1024 / 1024:.1f} MB')
print(f'Peak memory: {peak / 1024 / 1024:.1f} MB')

tracemalloc.stop()
```

**Expected result:** You see a ranked list of memory allocations by file and line number, plus total current and peak memory usage in megabytes.

### 5. Debug API and network errors with exception chaining

When your Python app calls external APIs, errors can be vague. Use try-except blocks with detailed logging to capture the actual error message, status code, and response body. Python 3 supports exception chaining with raise ... from to preserve the original error context. For HTTP requests using the requests library, always check the status code and log the response text on failure before raising an error.

```
import requests
import logging

logger = logging.getLogger('api')

def fetch_weather(city):
    api_key = os.getenv('WEATHER_API_KEY')
    if not api_key:
        raise ValueError('WEATHER_API_KEY not set. Add it in Tools → Secrets.')

    url = f'https://api.example.com/weather?q={city}&key={api_key}'
    logger.info(f'Fetching weather for {city}')

    try:
        response = requests.get(url, timeout=10)
        logger.debug(f'Response status: {response.status_code}')

        if response.status_code != 200:
            logger.error(f'API error {response.status_code}: {response.text}')
            response.raise_for_status()

        return response.json()
    except requests.Timeout:
        logger.error(f'Request to weather API timed out after 10 seconds')
        raise
    except requests.ConnectionError as e:
        logger.error(f'Cannot connect to weather API: {e}')
        raise
```

**Expected result:** When an API call fails, your logs show the exact status code, response body, and error type instead of a generic exception.

## Complete code example

File: `debug_utils.py`

```python
"""debug_utils.py — Python debugging utilities for Replit projects.

Import these helpers during development.
Set DEBUG_MODE=true in Tools → Secrets to enable verbose output.
"""
import os
import logging
import tracemalloc
from functools import wraps
import time

# Configure logging based on Secrets
log_level = logging.DEBUG if os.getenv('DEBUG_MODE') == 'true' else logging.INFO
logging.basicConfig(
    level=log_level,
    format='%(asctime)s [%(levelname)s] %(name)s (%(filename)s:%(lineno)d): %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger('app')


def timed(func):
    """Decorator that logs how long a function takes to execute."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        try:
            result = func(*args, **kwargs)
            elapsed = time.time() - start
            logger.debug(f'{func.__name__} completed in {elapsed:.3f}s')
            return result
        except Exception as e:
            elapsed = time.time() - start
            logger.error(f'{func.__name__} failed after {elapsed:.3f}s: {e}')
            raise
    return wrapper


def log_memory(label=''):
    """Print current and peak memory usage."""
    if not tracemalloc.is_tracing():
        tracemalloc.start()
    current, peak = tracemalloc.get_traced_memory()
    logger.info(
        f'Memory [{label}]: current={current/1024/1024:.1f}MB, '
        f'peak={peak/1024/1024:.1f}MB'
    )


def top_memory_allocations(limit=10):
    """Show the top memory-consuming lines of code."""
    if not tracemalloc.is_tracing():
        logger.warning('tracemalloc not started. Call tracemalloc.start() first.')
        return
    snapshot = tracemalloc.take_snapshot()
    stats = snapshot.statistics('lineno')
    logger.info(f'Top {limit} memory allocations:')
    for stat in stats[:limit]:
        logger.info(f'  {stat}')


def safe_request(url, method='GET', timeout=10, **kwargs):
    """Make an HTTP request with full error logging."""
    import requests
    logger.info(f'{method} {url}')
    try:
        response = requests.request(method, url, timeout=timeout, **kwargs)
        logger.debug(f'Response: {response.status_code}')
        if not response.ok:
            logger.error(f'{response.status_code}: {response.text[:500]}')
        return response
    except requests.Timeout:
        logger.error(f'Timeout after {timeout}s: {url}')
        raise
    except requests.ConnectionError as e:
        logger.error(f'Connection failed: {e}')
        raise
```

## Common mistakes

- **Using the Run button to debug with pdb, which does not support interactive input in Console** — undefined Fix: Run your script from the Shell tab with python filename.py so you can type pdb commands
- **Leaving breakpoint() calls in code that gets deployed to production** — undefined Fix: Search your codebase for breakpoint() and pdb.set_trace() before deploying, and remove or conditionally disable them
- **Catching all exceptions with a bare except: clause that hides the actual error** — undefined Fix: Catch specific exception types like except ValueError as e and always log the error message
- **Using print(object) for debugging dictionaries and getting unhelpful output** — undefined Fix: Use import json; print(json.dumps(obj, indent=2, default=str)) for readable, formatted output
- **Not setting timeouts on requests.get() calls, causing the Repl to hang indefinitely** — undefined Fix: Always pass timeout=10 (or an appropriate value) to requests.get() and requests.post()

## Best practices

- Run pdb from the Shell tab, not the Run button, because Console does not support interactive debugger input
- Read Python tracebacks from bottom to top: the last line is the error, the lines above show the call chain
- Use the logging module with severity levels instead of print statements for structured, filterable output
- Set a DEBUG_MODE secret in Tools → Secrets to toggle verbose logging without changing code
- Always set timeouts on HTTP requests to prevent frozen Repls from consuming resources
- Use tracemalloc proactively before your app hits the memory limit, especially on the 2 GiB Starter plan
- Add type hints to function signatures so tracebacks are easier to understand when type errors occur
- Delete breakpoint() calls before deploying — they will freeze your production app

## Frequently asked questions

### Does Replit have a visual Python debugger like VS Code?

Replit does not have a built-in visual debugger with a GUI. However, Python's built-in pdb works in the Shell tab, providing interactive breakpoints, variable inspection, and step-through execution. For complex debugging needs, teams can consult RapidDev for guidance on advanced debugging strategies.

### Why does pdb not work when I click the Run button?

The Run button sends output to the Console tab, which is read-only and does not support interactive input. Run your script from the Shell tab instead with python filename.py so you can type pdb commands.

### How do I find memory leaks in Python on Replit?

Use the tracemalloc module. Call tracemalloc.start() at the beginning of your script, take snapshots before and after operations, and compare them with snapshot2.compare_to(snapshot1, 'lineno') to see which lines increased memory usage.

### Can I use the logging module to write logs to a file on Replit?

Yes, you can configure a FileHandler in logging. However, remember that the file system resets on deployment. For persistent logs in production, use an external logging service or write to the database.

### What is the memory limit on Replit for Python apps?

The Starter plan provides 2 GiB RAM. Core provides 8 GiB. Pro provides 8 GiB or more. If your Python app exceeds the limit, Replit kills the process with 'Your Repl ran out of memory'.

### How do I debug a Python import error on Replit?

A ModuleNotFoundError means the package is not installed. Run pip install package_name in Shell. If it was installed but still fails, check for virtual environment conflicts by running which python and pip list in Shell.

### Can I use ipdb or pdbpp instead of the standard pdb?

Yes. Install them with pip install ipdb or pip install pdbpp in Shell. Use import ipdb; ipdb.set_trace() for a more feature-rich debugging experience with syntax highlighting and tab completion.

---

Source: https://www.rapidevelopers.com/replit-tutorial/how-to-enable-advanced-debugging-features-for-python-code-in-replit
© RapidDev — https://www.rapidevelopers.com/replit-tutorial/how-to-enable-advanced-debugging-features-for-python-code-in-replit
