> "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
In This Chapter
- Learning Objectives
- Introduction
- 22.1 The AI Debugging Workflow
- 22.2 Error Message Analysis
- 22.3 Stack Trace Interpretation
- 22.4 Log Analysis and Pattern Recognition
- 22.5 Interactive Debugging with AI
- 22.6 Performance Debugging
- 22.7 Environment and Configuration Issues
- 22.8 Dependency Conflicts and Resolution
- 22.9 The Debugging Conversation Template
- 22.10 Building Debugging Intuition
- Chapter Summary
- What's Next
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:
- The traceback — showing the chain of function calls that led to the error
- The location — file names and line numbers where each call occurred
- The error type and value —
KeyError: 42tells us a dictionary lookup failed for key42
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:
- Pattern recognition — It matches the error type against known patterns
- Context analysis — It examines surrounding code for likely causes
- Common cause ranking — It orders possible causes by likelihood
- Fix suggestion — It proposes solutions based on the most likely cause
Callout: Providing Context Matters
Pasting just
KeyError: 42gives 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:
- Include the complete trace — Do not truncate it
- Include relevant source code — Show the code at the key lines referenced
- Indicate which code is yours — Help AI distinguish your code from library code
- 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:
- Filter — Narrow logs to the relevant time window and component
- Sample — Include both successful and failed operations
- Annotate — Mark any entries you find suspicious
- Ask specific questions — "Why do all failures have amounts above $100?" is better than "What is wrong?"
- 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:
- Set breakpoint at the suspected problem area
- Inspect variables and capture state
- Share with AI — ask what seems wrong given the observed state
- AI suggests what to inspect next
- Step through code following AI's guidance
- 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(orpip freeze) -echo $VIRTUAL_ENV(orecho %VIRTUAL_ENV%on Windows) -uname -a(orsysteminfoon Windows) -env | grep -i relevant_variableThis 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:
- The full error message
- Your
requirements.txtorpyproject.toml - What your application actually uses from each package
- 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
Security-Related Dependency Updates
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:
- Understand the root cause — Do not just apply the fix. Ask the AI to explain why the bug occurred.
- Identify the pattern — Is this a type of bug you might encounter again? What are the warning signs?
- 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)?
- 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:
-
Structure your bug reports — The quality of AI's debugging assistance is directly proportional to the quality of the information you provide.
-
Include complete context — Full stack traces, relevant code, environment details, and what you have already tried.
-
Evaluate before applying — AI suggestions must be understood and verified, not blindly applied (see Chapter 14).
-
Use the right tool — Error messages, stack traces, logs, profiling output, and debugger sessions each have their role in the diagnostic process.
-
Learn from every bug — Each debugging session is an opportunity to build intuition that makes you faster and more effective over time.
-
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.