21 min read

> "Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan

Chapter 22: Debugging and Troubleshooting with AI

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan

Learning Objectives

By the end of this chapter, you will be able to:

  • Remember the structured debugging conversation template and its four key components (Bloom's Level 1)
  • Understand how AI interprets error messages, stack traces, and log output to identify root causes (Bloom's Level 2)
  • Apply AI-assisted debugging workflows to real Python errors, performance issues, and environment problems (Bloom's Level 3)
  • Analyze complex error patterns, log anomalies, and dependency conflicts using AI as a diagnostic partner (Bloom's Level 4)
  • Evaluate AI debugging suggestions for correctness and relevance before applying fixes (Bloom's Level 5)
  • Create custom debugging tools and workflows that integrate AI assistance into your daily development practice (Bloom's Level 6)

Introduction

Every developer, no matter how experienced, spends a significant portion of their time debugging. Studies estimate that developers spend between 35% and 50% of their working hours finding and fixing bugs. Debugging is not a failure state — it is a fundamental part of the software development process. Yet it is also one of the most frustrating parts, especially when you find yourself staring at an error message that seems completely unrelated to the actual problem.

AI coding assistants have transformed debugging from a solitary, often agonizing exercise into a collaborative investigation. When you paste an error message into an AI assistant, you are not just asking "what does this mean?" — you are engaging a partner that has processed millions of similar errors, can cross-reference documentation instantly, and can reason about your code's behavior in ways that complement your own understanding.

This chapter teaches you how to use AI effectively as a debugging partner. You will learn structured approaches for presenting problems to AI, techniques for analyzing errors and logs, strategies for performance debugging, and methods for resolving environment and dependency issues. Most importantly, you will learn how to build your own debugging intuition over time, using AI as a teacher rather than just a tool.

Prerequisites: This chapter assumes familiarity with Python fundamentals (Chapter 5), experience with AI coding sessions (Chapter 6), and an understanding of how AI can produce incorrect output (Chapter 14). Familiarity with testing concepts from Chapter 21 will also be helpful.


22.1 The AI Debugging Workflow

Debugging with AI is not simply copying and pasting an error message and hoping for the best. Effective AI-assisted debugging follows a structured workflow that maximizes the quality of the AI's analysis and minimizes wasted time.

The Four-Phase Debugging Cycle

The AI debugging workflow consists of four distinct phases:

Phase 1: Reproduce and Document

Before engaging AI, you must be able to reliably reproduce the bug and document what you observe. This means:

  • Identifying the exact steps that trigger the bug
  • Recording the complete error output (not just the final line)
  • Noting the expected behavior versus actual behavior
  • Capturing the environment details (Python version, OS, relevant package versions)

Phase 2: Present to AI

How you present the problem to AI dramatically affects the quality of the response. A well-structured prompt includes context, the error, relevant code, and what you have already tried. We cover this in detail in Section 22.9.

Phase 3: Evaluate and Apply

AI suggestions must be critically evaluated before application. As we discussed in Chapter 14, AI can confidently provide incorrect solutions. Always:

  • Understand why the suggested fix should work before applying it
  • Test the fix in isolation when possible
  • Verify that the fix does not introduce new issues

Phase 4: Learn and Document

After resolving the bug, take a moment to internalize what you learned. Ask the AI to explain the root cause in depth. Document the bug and its resolution for your team. This phase builds the debugging intuition discussed in Section 22.10.

Callout: The "Rubber Duck" Effect

Many developers report that the mere act of structuring a bug report for AI helps them solve the problem themselves. This is a digital version of "rubber duck debugging" — the practice of explaining your code to an inanimate object to clarify your thinking. Even if you solve the bug before the AI responds, the structured approach still provides value.

When to Engage AI

Not every bug warrants AI assistance. Here is a practical decision framework:

Situation Recommendation
Syntax error with a clear message Fix it yourself — builds muscle memory
Unfamiliar error message Ask AI to explain the error
Error in a library you did not write Ask AI — it likely has seen this before
Intermittent or timing-dependent bug Use AI to help design a reproduction strategy
Performance regression Use AI to analyze profiling output
Environment or dependency issue AI excels here — these are well-documented
Logic bug in your own code Start by explaining the logic to AI

The Debugging Mindset

Effective debugging with AI requires a specific mindset. You are not delegating the debugging — you are collaborating. The AI brings broad knowledge of common errors and patterns. You bring specific knowledge of your system, your requirements, and the history of changes. Neither alone is as effective as both working together.

Developer Knowledge          AI Knowledge
─────────────────────       ─────────────────────
Your system's architecture   Common error patterns
Recent code changes          Library documentation
Business requirements        Stack Overflow solutions
Runtime environment          Language specifications
User behavior patterns       Best practice patterns

22.2 Error Message Analysis

Error messages are the starting point of most debugging sessions. Understanding how to read them yourself and how to present them to AI effectively is a critical skill.

Anatomy of a Python Error

Every Python error follows a predictable structure. Consider this example:

Traceback (most recent call last):
  File "app/views.py", line 45, in get_user_profile
    profile = user.profile
  File "app/models.py", line 112, in profile
    return self._profile_cache[self.id]
KeyError: 42

This error contains three key pieces of information:

  1. The traceback — showing the chain of function calls that led to the error
  2. The location — file names and line numbers where each call occurred
  3. The error type and valueKeyError: 42 tells us a dictionary lookup failed for key 42

Common Python Error Categories

When presenting errors to AI, it helps to understand the broad categories:

Syntax Errors occur before your code runs:

# SyntaxError: invalid syntax
def calculate_total(items)    # Missing colon
    return sum(items)

# SyntaxError: unexpected EOF while parsing
data = {
    "name": "Alice",
    "age": 30
# Missing closing brace

Name and Attribute Errors indicate something is not found:

# NameError: name 'resutl' is not defined
print(resutl)  # Typo in variable name

# AttributeError: 'list' object has no attribute 'push'
my_list = [1, 2, 3]
my_list.push(4)  # Should be .append()

Type Errors arise from operations on incompatible types:

# TypeError: can only concatenate str (not "int") to str
message = "The count is: " + 42  # Need str(42)

# TypeError: 'NoneType' object is not subscriptable
result = some_function()  # Returns None unexpectedly
print(result["key"])

Value and Key Errors indicate invalid data:

# ValueError: invalid literal for int() with base 10: 'abc'
number = int("abc")

# KeyError: 'email'
user = {"name": "Alice"}
print(user["email"])  # Key does not exist

How AI Interprets Errors

When you paste an error into an AI assistant, the AI performs several layers of analysis:

  1. Pattern recognition — It matches the error type against known patterns
  2. Context analysis — It examines surrounding code for likely causes
  3. Common cause ranking — It orders possible causes by likelihood
  4. Fix suggestion — It proposes solutions based on the most likely cause

Callout: Providing Context Matters

Pasting just KeyError: 42 gives the AI very little to work with. Pasting the full traceback, the relevant function, and a description of what you expected to happen transforms the AI from guessing to diagnosing. Always include more context rather than less.

Effective Error Presentation

Here is an example of a poor error report versus an effective one:

Poor:

I'm getting a KeyError in my code. How do I fix it?

Effective:

I'm building a user profile system in Flask. When I access /profile/42,
I get this error:

Traceback (most recent call last):
  File "app/views.py", line 45, in get_user_profile
    profile = user.profile
  File "app/models.py", line 112, in profile
    return self._profile_cache[self.id]
KeyError: 42

Here's the relevant code from models.py:

class User:
    _profile_cache = {}

    @property
    def profile(self):
        return self._profile_cache[self.id]

The cache is populated during login. This error happens when a user
accesses their profile after the server restarts but before they log
in again. I expected the cache to persist across restarts. I've tried
adding a default value with .get() but I need to understand why the
cache is empty.

The second version gives the AI everything it needs: the technology stack, the full traceback, the relevant code, the conditions under which the bug occurs, and what the developer has already tried. The AI can now provide a targeted, useful response about server-side caching strategies.

Multi-Error Analysis

Sometimes you encounter multiple errors in sequence as you try to fix a problem. When presenting these to AI, show the progression:

I'm fixing errors one at a time and getting a chain of new errors.

Error 1 (original): ImportError: cannot import name 'OAuth2Session' from 'requests'
Fix attempt: pip install requests-oauthlib
Error 2 (after fix): AttributeError: module 'requests_oauthlib' has no attribute 'OAuth2Session'
Fix attempt: Changed import to from requests_oauthlib import OAuth2Session
Error 3 (after fix): TypeError: OAuth2Session.__init__() got an unexpected keyword argument 'scope'

Can you help me understand the correct way to use OAuth2Session?

This chain of errors tells the AI a story about your debugging journey and helps it identify the actual root cause rather than just the surface symptom.


22.3 Stack Trace Interpretation

Stack traces are the most information-dense artifact in debugging. Learning to read them — and to help AI read them effectively — is essential.

Reading Stack Traces Bottom-Up

Python stack traces show the call chain from the outermost call (top) to the point of failure (bottom). While the error type is at the bottom, the cause is often somewhere in the middle:

Traceback (most recent call last):
  File "main.py", line 8, in <module>         # 1. Entry point
    result = process_order(order_data)
  File "orders.py", line 23, in process_order  # 2. Business logic
    total = calculate_total(order_data["items"])
  File "orders.py", line 45, in calculate_total # 3. Calculation
    price = get_price(item["product_id"])
  File "products.py", line 12, in get_price     # 4. Data access
    return prices[product_id]
KeyError: 'PROD-999'

The error is a KeyError at the bottom, but the root cause might be: - An invalid product ID being passed from the order data (layer 2) - A prices dictionary that was not populated correctly (layer 4) - A data validation step that should have caught this (missing entirely)

Presenting Stack Traces to AI

When sharing stack traces with AI, follow these guidelines:

  1. Include the complete trace — Do not truncate it
  2. Include relevant source code — Show the code at the key lines referenced
  3. Indicate which code is yours — Help AI distinguish your code from library code
  4. Describe the data flow — Explain what values you expected at each step
Here's a stack trace from my order processing system. The files
main.py, orders.py, and products.py are my code. The error happens
when processing an order that contains a recently discontinued product.

[stack trace here]

The prices dictionary is loaded from the database at startup.
Products can be discontinued through an admin interface at any time.
I think the issue is that the prices dictionary is stale, but I'm
not sure of the best approach to fix it.

Callout: Library Stack Frames

Stack traces often include many frames from third-party libraries or the Python standard library. When presenting these to AI, do not remove them — they provide important context. However, you can note which frames are yours: "Lines from django/core/* are framework code; my code is in app/views.py and app/models.py."

Nested and Chained Exceptions

Python 3 supports exception chaining, which can produce complex traces:

Traceback (most recent call last):
  File "db.py", line 15, in connect
    conn = psycopg2.connect(dsn)
psycopg2.OperationalError: connection refused

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "app.py", line 30, in get_data
    db = connect_with_retry()
  File "db.py", line 20, in connect_with_retry
    raise DatabaseUnavailableError("Could not connect after 3 retries")
DatabaseUnavailableError: Could not connect after 3 retries

When presenting chained exceptions to AI, include both traces. The "During handling of the above exception" section tells the AI about the error propagation path, which is crucial for understanding the full picture.

Async Stack Traces

Asynchronous code produces stack traces that can be confusing because the call chain is not as linear:

Traceback (most recent call last):
  File "server.py", line 45, in handle_request
    data = await fetch_user_data(user_id)
  File "services.py", line 23, in fetch_user_data
    response = await client.get(f"/users/{user_id}")
  File "httpx/_client.py", line 1574, in get
    return await self.request("GET", url, **kwargs)
  ...
httpx.ConnectTimeout: timed out

When presenting async traces to AI, mention that the code is asynchronous and describe the concurrency model (asyncio, threading, etc.). This context helps the AI reason about timing-related issues.


22.4 Log Analysis and Pattern Recognition

Logs are the continuous narrative of your application's behavior. While error messages capture moments of failure, logs capture the story leading up to those failures. AI is remarkably good at finding patterns in log data that humans might miss.

Structuring Logs for AI Analysis

Before you can effectively use AI for log analysis, your application needs to produce useful logs. Here are key principles:

import logging

# Good: Structured, contextual logging
logger = logging.getLogger(__name__)

def process_payment(order_id: str, amount: float) -> bool:
    logger.info(
        "Processing payment",
        extra={
            "order_id": order_id,
            "amount": amount,
            "timestamp": datetime.utcnow().isoformat()
        }
    )
    try:
        result = payment_gateway.charge(amount)
        logger.info(
            "Payment successful",
            extra={"order_id": order_id, "transaction_id": result.id}
        )
        return True
    except PaymentError as e:
        logger.error(
            "Payment failed",
            extra={"order_id": order_id, "error": str(e)},
            exc_info=True
        )
        return False

Presenting Logs to AI

When sharing logs with AI for analysis, follow this template:

I'm investigating intermittent 500 errors on our /checkout endpoint.
Here are the relevant log entries from the last hour, filtered to
show only the checkout flow. Timestamps are in UTC.

[2024-01-15 14:23:01] INFO  Processing payment order_id=ORD-1234 amount=59.99
[2024-01-15 14:23:01] INFO  Payment successful order_id=ORD-1234 txn=TXN-5678
[2024-01-15 14:23:02] INFO  Processing payment order_id=ORD-1235 amount=129.99
[2024-01-15 14:23:05] ERROR Payment failed order_id=ORD-1235 error="timeout"
[2024-01-15 14:23:05] INFO  Retry 1/3 order_id=ORD-1235
[2024-01-15 14:23:08] ERROR Payment failed order_id=ORD-1235 error="timeout"
[2024-01-15 14:23:08] INFO  Retry 2/3 order_id=ORD-1235
[2024-01-15 14:23:11] ERROR Payment failed order_id=ORD-1235 error="timeout"
[2024-01-15 14:23:11] ERROR Max retries exceeded order_id=ORD-1235
[2024-01-15 14:23:11] ERROR 500 Internal Server Error /checkout

The pattern seems to happen with larger order amounts. Normal load is
about 50 orders per minute. Can you identify any patterns?

What AI Looks For in Logs

When analyzing logs, AI assistants search for several types of patterns:

Temporal patterns: Do errors cluster at specific times? This might indicate scheduled jobs, traffic spikes, or external service maintenance windows.

Correlation patterns: Do errors correlate with specific parameters (order amounts, user IDs, product categories)? This might indicate data-dependent bugs.

Sequence patterns: Is there a specific sequence of events that always precedes the error? This reveals causal chains.

Frequency patterns: Are errors increasing over time? This might indicate a resource leak or growing data set hitting a threshold.

Absence patterns: What should be in the logs but is not? A missing log line between two expected entries can indicate a silent failure.

Callout: Log Volume Management

AI assistants have context window limits. When sharing logs, do not dump thousands of lines. Instead, filter to the relevant time window and components. Include a few successful operations alongside the failures so the AI can compare them. If you have structured logs (JSON format), consider summarizing the data: "Out of 500 requests in this window, 23 failed with timeout errors, all for amounts above $100."

Building a Log Analysis Workflow

A structured approach to AI-assisted log analysis:

  1. Filter — Narrow logs to the relevant time window and component
  2. Sample — Include both successful and failed operations
  3. Annotate — Mark any entries you find suspicious
  4. Ask specific questions — "Why do all failures have amounts above $100?" is better than "What is wrong?"
  5. Iterate — Based on AI's analysis, gather more specific logs and repeat

22.5 Interactive Debugging with AI

While post-mortem analysis of errors and logs is valuable, some bugs require interactive debugging — stepping through code line by line, inspecting variables, and observing execution flow. AI can enhance this process significantly.

Using AI with pdb Output

Python's built-in debugger, pdb, produces output that you can share with AI:

# Add a breakpoint to your code
def calculate_discount(user, cart):
    breakpoint()  # Python 3.7+ way to enter pdb
    base_discount = user.loyalty_points / 100
    cart_discount = cart.total * 0.1 if cart.total > 100 else 0
    return base_discount + cart_discount

When you hit the breakpoint, capture your pdb session:

> app/pricing.py(4)calculate_discount()
-> base_discount = user.loyalty_points / 100
(Pdb) p user.loyalty_points
None
(Pdb) p type(user)
<class 'app.models.User'>
(Pdb) p user.__dict__
{'id': 42, 'name': 'Alice', 'loyalty_points': None, 'created_at': datetime(...)}
(Pdb) p cart.total
159.99
(Pdb) p cart.items
[<CartItem: Widget x2>, <CartItem: Gadget x1>]

Now share this with AI:

I'm debugging a TypeError in my discount calculation. Here's my pdb
session showing the variable state when the function is called:

[pdb output above]

The function expects loyalty_points to be an integer, but it's None
for this user. This user was created through our API, not the web
form. Why might loyalty_points be None, and what's the best way to
handle this?

Debugger-AI Workflow Pattern

The most effective interactive debugging workflow alternates between the debugger and AI:

  1. Set breakpoint at the suspected problem area
  2. Inspect variables and capture state
  3. Share with AI — ask what seems wrong given the observed state
  4. AI suggests what to inspect next
  5. Step through code following AI's guidance
  6. Share results and iterate

This creates a feedback loop where AI's broad knowledge guides your specific investigation.

Conditional Breakpoints with AI Guidance

When debugging intermittent issues, AI can help you craft targeted breakpoints:

Prompt: "I have a function that processes 10,000 records but only
fails on about 5 of them. I can't figure out which ones. How can I
set a conditional breakpoint to catch only the failing cases?"

AI Response: "You can use a conditional breakpoint in pdb:

import pdb

def process_record(record):
    # Break only when the condition that causes failure is met
    if record.get('amount') and not isinstance(record['amount'], (int, float)):
        pdb.set_trace()
    # ... rest of processing

Or if you're using an IDE, set a conditional breakpoint with:
    isinstance(record.get('amount'), str)
This will pause only when amount is a string instead of a number."

Using print-Based Debugging with AI

Despite the sophistication of modern debuggers, strategic print statements remain a valid debugging technique, especially in environments where interactive debugging is impractical (production servers, CI pipelines, containerized applications):

def process_batch(items):
    print(f"DEBUG: process_batch called with {len(items)} items")
    results = []
    for i, item in enumerate(items):
        print(f"DEBUG: Processing item {i}: type={type(item)}, value={item!r}")
        try:
            result = transform(item)
            print(f"DEBUG: Item {i} transformed: {result!r}")
            results.append(result)
        except Exception as e:
            print(f"DEBUG: Item {i} FAILED: {type(e).__name__}: {e}")
            raise
    print(f"DEBUG: Batch complete, {len(results)} results")
    return results

Share the output with AI and ask it to identify the anomalous item that caused the failure.

Callout: AI Can Generate Debug Instrumentation

One powerful technique is to ask AI to add debugging instrumentation to your code. Paste a function that is misbehaving and ask: "Add detailed logging/print statements to this function so I can trace the execution path and variable values." The AI will add instrumentation at the key decision points, which saves you from guessing where to put print statements.


22.6 Performance Debugging

Performance bugs are among the most challenging to diagnose because they often do not produce error messages. The code runs correctly — it just runs too slowly. AI is an excellent partner for performance debugging because it can quickly analyze profiling output and suggest optimizations.

Profiling with cProfile

Python's built-in cProfile module captures detailed performance data:

import cProfile
import pstats

# Profile a function call
cProfile.run('process_large_dataset(data)', 'output.prof')

# Print sorted results
stats = pstats.Stats('output.prof')
stats.sort_stats('cumulative')
stats.print_stats(20)  # Top 20 functions by cumulative time

Example output:

         2847263 function calls (2847251 primitive calls) in 12.453 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.002    0.002   12.453   12.453 main.py:1(process_large_dataset)
     1000    0.045    0.000   11.234    0.011 db.py:23(query_record)
     1000    9.876    0.010    9.876    0.010 {method 'execute' of 'cursor'}
     1000    0.234    0.000    1.113    0.001 serialize.py:5(to_dict)
   500000    0.879    0.000    0.879    0.000 {built-in method builtins.isinstance}

Presenting Profiling Data to AI

When sharing profiling output with AI, include context about your expectations:

I profiled my data processing pipeline and it's taking 12.4 seconds
for 1000 records. I need it under 2 seconds. Here's the cProfile
output:

[profiling output above]

The main bottleneck appears to be database queries (9.876 seconds in
cursor.execute). Each of the 1000 records triggers a separate database
query. The records come from a CSV upload.

My current code:

def process_large_dataset(data):
    results = []
    for record in data:
        db_record = query_record(record["id"])  # One query per record
        results.append(to_dict(db_record))
    return results

What's the best way to optimize this?

AI will typically identify the N+1 query problem and suggest batch querying, which is exactly the right fix for this pattern.

Memory Profiling

Memory issues require different tools. The memory_profiler package provides line-by-line memory usage:

from memory_profiler import profile

@profile
def build_report(data):
    raw_records = load_all_records(data)      # Memory spike here?
    processed = transform_records(raw_records) # Or here?
    report = generate_html(processed)          # Or here?
    return report

Output:

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
     3     45.2 MiB     45.2 MiB           1   @profile
     4                                         def build_report(data):
     5    245.8 MiB    200.6 MiB           1       raw_records = load_all_records(data)
     6    445.3 MiB    199.5 MiB           1       processed = transform_records(raw_records)
     7    644.9 MiB    199.6 MiB           1       report = generate_html(processed)
     8    644.9 MiB      0.0 MiB           1       return report

This output tells a clear story: memory doubles at each step because data is being copied rather than transformed in place. Present this to AI with: "My function uses 644 MiB for data that starts at 200 MiB. Each step seems to create a full copy. How can I reduce memory usage?"

AI-Assisted Performance Optimization Strategies

AI excels at suggesting performance patterns you might not have considered:

  • Batch operations — Replacing N individual operations with one batch operation
  • Caching — Identifying repeated computations that could be memoized
  • Algorithm improvements — Spotting O(n^2) patterns that could be O(n log n) or O(n)
  • Lazy evaluation — Using generators instead of building large lists in memory
  • Concurrency — Identifying I/O-bound work that could be parallelized

Callout: Measure Before and After

Always profile before and after applying AI's optimization suggestions. As discussed in Chapter 14, AI can suggest optimizations that are theoretically sound but do not address the actual bottleneck. Trust the profiler, not intuition. A simple time.time() measurement around the critical section is the minimum verification you should perform.


22.7 Environment and Configuration Issues

Environment issues — wrong Python version, missing system dependencies, incorrect configuration — are among the most frustrating bugs because the code itself is correct. AI is particularly well-suited to diagnosing these problems because they tend to follow well-documented patterns.

Python Version Issues

# This works in Python 3.10+ but fails in 3.9
def process(data: dict[str, list[int]]) -> None:
    match data:
        case {"type": "batch", "items": items}:
            handle_batch(items)
        case {"type": "single", "item": item}:
            handle_single(item)

Error on Python 3.9:

  File "app.py", line 3
    match data:
          ^
SyntaxError: invalid syntax

When presenting this to AI, include your Python version:

I'm getting a SyntaxError on code that works on my machine but fails
in CI. My local Python is 3.12, CI runs 3.9. Here's the error:
[error above]
What Python 3.10+ features am I using, and how do I make this
backward compatible?

Virtual Environment Issues

Virtual environment problems are extremely common:

ModuleNotFoundError: No module named 'requests'

This error seems simple, but the cause can be subtle:

I installed requests with pip but still get ModuleNotFoundError.
Here's my diagnostic output:

$ which python
/usr/bin/python3
$ which pip
/home/user/.local/bin/pip
$ pip show requests
Name: requests
Version: 2.31.0
Location: /home/user/.local/lib/python3.11/site-packages

My virtual environment is at /home/user/project/venv/. It looks like
pip installed requests globally instead of in my venv. How do I fix this?

AI will immediately spot that the python and pip commands point to different environments, a classic configuration error.

Configuration Debugging Pattern

For configuration issues, provide AI with the complete configuration and the resulting error:

My Flask app fails to connect to PostgreSQL in Docker. Here are the
relevant files:

docker-compose.yml:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"
  web:
    build: .
    environment:
      DATABASE_URL: postgresql://postgres:secret@localhost:5432/myapp

Error:
sqlalchemy.exc.OperationalError: could not connect to server:
Connection refused. Is the server running on host "localhost" and
accepting TCP/IP connections on port 5432?

AI will recognize the common Docker networking mistake: within Docker Compose, services communicate using service names, not localhost. The DATABASE_URL should use db instead of localhost.

Operating System Differences

Cross-platform issues are another area where AI shines:

# Works on Linux, fails on Windows
import os
config_path = os.path.join("/etc", "myapp", "config.yaml")
FileNotFoundError: [Errno 2] No such file or directory: '/etc/myapp/config.yaml'

When running on Windows, /etc does not exist. AI will suggest using platform-aware configuration paths with pathlib or appdirs.

Callout: Share Your Full Environment

For environment issues, provide AI with the output of these diagnostic commands: - python --version - pip list (or pip freeze) - echo $VIRTUAL_ENV (or echo %VIRTUAL_ENV% on Windows) - uname -a (or systeminfo on Windows) - env | grep -i relevant_variable

This context helps AI pinpoint environment-specific causes quickly.


22.8 Dependency Conflicts and Resolution

Dependency conflicts are the bane of Python development. The infamous "dependency hell" occurs when two packages require incompatible versions of a shared dependency. AI assistants are remarkably effective at navigating these conflicts.

Understanding Dependency Errors

ERROR: pip's dependency resolver does not currently take into account
all the packages that are installed. The following packages have
incompatible dependencies:

 - package-a 2.0 requires shared-lib>=3.0,<4.0
 - package-b 1.5 requires shared-lib>=2.0,<3.0

ERROR: Cannot install package-a==2.0 and package-b==1.5 because
these package versions have conflicting dependencies.

When presenting this to AI, include:

  1. The full error message
  2. Your requirements.txt or pyproject.toml
  3. What your application actually uses from each package
  4. Whether you have flexibility on package versions
I'm getting a dependency conflict between package-a and package-b.
Both need shared-lib but at incompatible versions.

My requirements.txt:
package-a==2.0
package-b==1.5
other-package==3.2

I use package-a for its data validation features and package-b for
its HTTP client. Is there a version combination that works, or should
I replace one of these packages?

Version Constraint Strategies

AI can help you navigate version constraints with several strategies:

Finding compatible versions:

Can you help me find versions of package-a and package-b that are
compatible? I need at least these features:
- package-a: the DataValidator class (introduced in 1.8)
- package-b: async HTTP support (introduced in 1.3)

Here's what pip shows for available versions:
[pip index versions output]

Using dependency resolution tools:

AI will often suggest tools like pip-compile (from pip-tools), poetry, or pipdeptree to visualize and manage dependencies:

# Visualize dependency tree
pip install pipdeptree
pipdeptree --warn fail

# Use pip-tools for deterministic resolution
pip install pip-tools
pip-compile requirements.in

Alternative package suggestions:

If no compatible versions exist, AI can suggest alternative packages that provide similar functionality without the conflict.

Lock Files and Reproducibility

AI can help you understand and manage lock files:

My poetry.lock and pyproject.toml seem to be out of sync. When I run
poetry install, I get:

Warning: poetry.lock is not consistent with pyproject.toml. You may
be getting improper dependencies. Run `poetry lock` to fix it.

But when I run poetry lock, it fails with a resolution error. Here's
my pyproject.toml:
[contents]

And the error from poetry lock:
[error message]

What should I do?

Callout: Dependency Debugging Checklist

When facing dependency issues, gather this information before asking AI: 1. Your Python version (python --version) 2. Your dependency specification file (requirements.txt, pyproject.toml, etc.) 3. Currently installed packages (pip freeze) 4. The full error message 5. Your operating system (some packages have platform-specific dependencies) 6. Whether you are using a virtual environment

AI can also help when you need to update a dependency for security reasons but the update breaks compatibility:

GitHub Dependabot says I need to update cryptography from 3.4.8 to
41.0.0 due to CVE-2023-XXXXX. But when I update, my tests fail
because the API changed. Here are the failing tests and errors:

[test output]

What changed between cryptography 3.x and 41.x that I need to adapt to?

22.9 The Debugging Conversation Template

After years of community practice, a clear template has emerged for productive debugging conversations with AI. This section formalizes that template.

The DESCRIBE Framework

Use the DESCRIBE framework for structuring debugging conversations:

D - Describe the expected behavior E - Error message and stack trace (complete) S - Source code (relevant portions) C - Context (environment, recent changes, data) R - Reproduce steps I - Investigated already (what you have tried) B - Behavior observed (what actually happens) E - Environment details (versions, OS, dependencies)

Template in Practice

Here is the complete template applied to a real debugging scenario:

## What I Expected (Describe)
My Django REST API endpoint /api/users/ should return a paginated
list of users with their profile information.

## The Error
When I access the endpoint, I get a 500 error. Here's the traceback
from the Django debug page:

Traceback (most recent call last):
  File ".../rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "api/views.py", line 34, in list
    serializer = UserSerializer(queryset, many=True)
  File "api/serializers.py", line 18, in __init__
    super().__init__(*args, **kwargs)
  File ".../rest_framework/serializers.py", line 119, in __init__
    self.fields  # Force field evaluation
  File "api/serializers.py", line 25, in get_fields
    profile_data = self.get_profile(obj)
AttributeError: 'UserSerializer' object has no attribute 'get_profile'

## Relevant Source Code
# api/serializers.py
class UserSerializer(serializers.ModelSerializer):
    profile = serializers.SerializerMethodField()

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'profile']

    def get_fields(self):
        fields = super().get_fields()
        profile_data = self.get_profile(obj)  # Line 25
        return fields

    def get_profile(self, obj):
        return ProfileSerializer(obj.profile).data

## Context
- Django 4.2, Django REST Framework 3.14
- I recently added the profile field to the serializer
- The User model has a OneToOneField to Profile
- This worked before I tried to customize get_fields()

## Reproduction Steps
1. Start the development server: python manage.py runserver
2. GET http://localhost:8000/api/users/
3. Error occurs immediately

## What I've Already Tried
- Removing the get_fields override (this works but I need it for
  conditional field inclusion)
- Adding @property decorator to get_profile (no change)
- Checking that Profile objects exist for all users (they do)

## Observed Behavior
The error occurs every time, not intermittently. It happens for all
users, not just specific ones.

## Environment
- Python 3.11.5
- Django 4.2.7
- djangorestframework 3.14.0
- macOS 14.1
- SQLite (development)

This structured report gives the AI everything it needs. The AI can immediately identify that get_fields() is called during serializer initialization, before any object is available, so self.get_profile(obj) fails because obj is not defined in that scope.

Iterative Debugging Conversations

Debugging often requires multiple rounds of conversation. Here is how to structure follow-up messages:

Round 1: Present the bug using the DESCRIBE framework

Round 2: "Your suggestion to [X] resolved the immediate error, but now I'm seeing a new issue: [new error]. Here's the updated code after your fix: [code]"

Round 3: "That works for single users, but when I request the list endpoint with many=True, I get: [different error]. Is this related to the serializer changes?"

Each round builds on the previous one, maintaining the thread of investigation. Avoid starting a brand-new conversation for each error in a chain — the context from previous rounds is valuable.

Quick Debug Templates

For simpler issues, you do not need the full DESCRIBE framework. Here are quick templates:

Error explanation:

What does this Python error mean and what commonly causes it?
[paste error]

Code not working as expected:

This function should [expected behavior] but instead it [actual behavior].
Input: [example input]
Expected output: [expected output]
Actual output: [actual output]
[paste function]

Performance issue:

This code takes [X seconds] for [N items]. I need it under [Y seconds].
Profiling shows the bottleneck is [specific function/operation].
[paste code and profiling output]

Callout: Saving Debugging Templates

Create a text file or snippet in your editor with these templates. When you hit a bug, paste the appropriate template and fill in the blanks. This habit ensures you consistently provide high-quality debugging information to AI, which leads to faster resolutions.


22.10 Building Debugging Intuition

The ultimate goal of AI-assisted debugging is not to become dependent on AI for every bug, but to build your own debugging intuition over time. Each debugging session with AI is a learning opportunity.

The Learning Loop

Every time AI helps you solve a bug, actively engage the learning loop:

  1. Understand the root cause — Do not just apply the fix. Ask the AI to explain why the bug occurred.
  2. Identify the pattern — Is this a type of bug you might encounter again? What are the warning signs?
  3. Learn the diagnostic approach — How did the AI reason about the problem? What questions did it ask (or what questions should you ask yourself next time)?
  4. Build a mental model — Update your understanding of how the relevant system works.
Follow-up prompt: "Thanks, that fixed the bug. Can you explain in
more detail why Python's dictionary .get() method returns None by
default instead of raising a KeyError? I want to understand when to
use .get() versus direct access, so I can avoid this class of bug
in the future."

Common Bug Patterns to Internalize

Through repeated AI-assisted debugging, you will start recognizing these patterns instantly:

The Off-by-One Error:

# Bug: range(len(items)) misses the last item when comparing pairs
for i in range(len(items)):
    if items[i] > items[i + 1]:  # IndexError on last iteration
        swap(items, i, i + 1)

The Mutable Default Argument:

# Bug: all calls share the same list object
def add_item(item, items=[]):
    items.append(item)
    return items

# add_item("a") returns ["a"]
# add_item("b") returns ["a", "b"]  — unexpected!

The Late Binding Closure:

# Bug: all functions return 4 (the final value of i)
functions = []
for i in range(5):
    functions.append(lambda: i)

# All return 4, not 0, 1, 2, 3, 4

The Silent None Return:

# Bug: function falls through without explicit return
def find_user(user_id):
    for user in users:
        if user.id == user_id:
            return user
    # Implicitly returns None — should this raise an exception?

The String/Bytes Confusion:

# Bug: mixing str and bytes
response = requests.get(url)
data = response.content  # bytes, not str
if "error" in data:  # TypeError in Python 3
    handle_error()

Building a Personal Bug Database

Create a simple log of bugs you encounter, their root causes, and how you solved them:

## Bug Log

### 2024-01-15: KeyError in user profile cache
- **Symptom:** KeyError when accessing /profile after server restart
- **Root cause:** In-memory cache not persisted across restarts
- **Fix:** Added Redis-backed cache with fallback to database query
- **Pattern:** In-memory state that should survive restarts
- **Prevention:** Default to persistent storage; use in-memory only
  as performance optimization with fallback

### 2024-01-18: Intermittent test failures in CI
- **Symptom:** Test passes locally, fails ~20% of the time in CI
- **Root cause:** Test depended on dictionary ordering (Python 3.7+
  preserves insertion order, but test data was created from a set)
- **Fix:** Sorted results before comparison
- **Pattern:** Non-deterministic test data
- **Prevention:** Always sort or use set comparison for unordered data

From AI-Assisted to AI-Informed

As your debugging intuition grows, your relationship with AI evolves:

Beginner stage: "Here's an error. What does it mean and how do I fix it?"

Intermediate stage: "I think this is a caching issue because X. Can you confirm and suggest the best caching strategy for this use case?"

Advanced stage: "I've diagnosed this as a race condition in the connection pool. I'm considering three approaches: [A, B, C]. What are the tradeoffs?"

At the advanced stage, you are using AI as a sounding board and knowledge reference rather than as a primary debugger. You have internalized the common patterns and can quickly narrow down possibilities. AI fills in the gaps in your knowledge — specific API details, edge cases you have not encountered, alternative approaches you might not have considered.

Cross-Referencing with Tests

As discussed in Chapter 21, tests and debugging are deeply interconnected. When you fix a bug, write a test that would have caught it:

def test_user_profile_after_cache_clear():
    """Regression test: profile access should work even when cache is empty.

    Bug fixed 2024-01-15: KeyError when accessing profile after server
    restart because in-memory cache was empty.
    """
    user = create_test_user()
    cache.clear()  # Simulate server restart

    # Should not raise KeyError
    profile = user.profile
    assert profile is not None
    assert profile.user_id == user.id

This creates a safety net that prevents regression and documents the bug for future developers.

Callout: The Debugging Journal

Consider keeping a "debugging journal" for one month. Each time you debug with AI, write three sentences: what the bug was, what caused it, and what you learned. At the end of the month, review your entries. You will likely see patterns — types of bugs you encounter repeatedly, areas of your codebase that are fragile, and gaps in your understanding that you can proactively fill. This practice accelerates the development of debugging intuition far more than simply fixing bugs and moving on.

Knowing When Not to Use AI

Paradoxically, one sign of strong debugging intuition is knowing when AI will not help. Some debugging tasks are better handled through other means:

  • Visual bugs — Screenshot the UI and describe what is wrong, but also use browser DevTools
  • Timing-sensitive bugs — AI cannot observe your system in real time; you need logging and monitoring
  • Data-dependent bugs — If the bug depends on specific production data, you need to inspect that data
  • Architecture-level issues — AI can help with symptoms, but systemic problems require human judgment about the system design

Chapter Summary

Debugging with AI is a skill that combines technical knowledge, communication ability, and critical thinking. The structured approaches in this chapter — the four-phase debugging cycle, the DESCRIBE framework, and the learning loop — provide a foundation for effective AI-assisted debugging.

Key principles to remember:

  1. Structure your bug reports — The quality of AI's debugging assistance is directly proportional to the quality of the information you provide.

  2. Include complete context — Full stack traces, relevant code, environment details, and what you have already tried.

  3. Evaluate before applying — AI suggestions must be understood and verified, not blindly applied (see Chapter 14).

  4. Use the right tool — Error messages, stack traces, logs, profiling output, and debugger sessions each have their role in the diagnostic process.

  5. Learn from every bug — Each debugging session is an opportunity to build intuition that makes you faster and more effective over time.

  6. Write tests after fixes — As Chapter 21 emphasizes, a bug fix without a regression test is an invitation for the bug to return.

The goal is not to eliminate debugging from your workflow — that is impossible. The goal is to make debugging faster, more systematic, and more educational. With AI as your debugging partner, you can achieve all three.


What's Next

In Chapter 23, we will explore documentation and technical writing with AI assistance. You will learn how AI can help you write clear documentation, generate API references, and maintain documentation alongside your code — skills that are closely related to the communication techniques you practiced in this chapter's debugging conversations.