Skip to content
SparkTime, home
AI From Zero to Productive
Lessons
1 · Understanding Modern AI
2 · Talking to AI Effectively
3 · AI for Learning
4 · AI for Work and Productivity
5 · AI for Developers
6 · AI for Career Growth
7 · Research, Verification and Responsible AI
8 · Automation and AI Agents
9 · Build Your Personal AI System
Lesson 19 of 35

Lesson 19 · AI for Developers

How to Use AI to Debug Code Safely

Pasting an error into an assistant can solve a bug in a minute, or produce a patch that hides it. This article gives a safe debugging workflow: reproduce, share safely, ask for hypotheses, test them, review the fix, add a regression test, with a worked example and a list of dangerous 'fixes'.

On this page

Key takeaways

  • Treat the assistant as a source of hypotheses, not a patch dispenser. Reproduce the bug first, then ask for ranked causes and ways to test each.
  • Share the smallest useful piece of code and error, and remove secrets, tokens, customer data and proprietary details before pasting.
  • Review every proposed fix for whether it addresses the cause or only hides the symptom. Watch for blanket exception handlers, disabled checks and deleted tests.
  • Add a regression test that fails before the fix and passes after it. Make sure you can explain why the bug happened.
  • Run commands an assistant suggests only when you understand them, ideally on a branch or in a sandbox, never destructive ones on data you cannot restore.

A bug report arrives at 4 p.m. on Friday. You paste the stack trace into an assistant and, within seconds, get a patch. It makes the error go away. You ship it. On Monday you discover that the error went away because the patch quietly swallowed it, and now the wrong data is being written without any warning.

Debugging is where AI help is most tempting and where the gap between "the error disappeared" and "the bug is fixed" is widest. This article shows how to get the speed without the trap.

What you will learn

#
  • A safe, step-by-step debugging workflow with an assistant.
  • What to share and what never to paste.
  • A worked example in which the obvious fix is wrong.
  • Common "fixes" that hide bugs.
  • How to treat commands that an assistant suggests you run.

The idea in plain words

#

Debugging is the scientific method applied to code: observe what happens, form a hypothesis about why, test it, and repeat. An assistant is very good at generating hypotheses, because it has seen countless similar bugs. It is much less reliable at knowing which one is true for your program, because it cannot run your code, see your data or know your environment (unless it is an agent that can, in which case the same caution applies to its actions).

So split the work: it proposes, you test. For a foundation in systematic debugging without AI, see How to Debug Any Bug: A Calm, Repeatable Process.

The workflow

#
  1. Reproduce. Make the bug happen reliably. If you cannot reproduce it, no assistant can help you fix it.
  2. Shrink it. Cut away everything that is not needed to trigger the bug, until you have the smallest example. This often reveals the cause by itself.
  3. Write down expected versus actual. "I expected 35. I got 25."
  4. Prepare a safe, minimal share (see below).
  5. Ask for hypotheses, ranked, with a test for each, not a patch.
  6. Test the hypotheses yourself: a log line, a debugger, a unit test, a print.
  7. Ask for the smallest fix for the confirmed cause.
  8. Review the diff (see the symptom-hiding list).
  9. Add a regression test that fails before the fix and passes after.
  10. Explain the bug in a sentence. If you cannot, you have not finished.

A prompt for step 5:

Code
I have a bug. Expected: average_of_last([10, 20, 30, 40], 0) should not returna number, since there is nothing to average. Actual: it returns 25.0.Python 3.11. Code: [paste function].Do not give me a fix yet. List the three most likely causes in order oflikelihood, and for each give one quick way I can test whether it is the cause.

Asking for causes and tests keeps you in control, and it works well because the assistant's strength (breadth of experience) is used while its weakness (not seeing your runtime) is routed around.

A worked example

#

The function. It should return the average of the last n values of a list.

Python
# File: last_n_average.pydef average_of_last(values, n):    """Return the average of the last n values (buggy first version)."""    window = values[-n:]    return sum(window) / len(window)def average_of_last_fixed(values, n):    """Return the average of the last n values, or None when there is nothing to average."""    if n <= 0 or not values:        return None    window = values[-n:]    return sum(window) / len(window)scores = [10, 20, 30, 40]print("n=2 buggy:", average_of_last(scores, 2))print("n=0 buggy:", average_of_last(scores, 0))print("n=2 fixed:", average_of_last_fixed(scores, 2))print("n=0 fixed:", average_of_last_fixed(scores, 0))print("empty fixed:", average_of_last_fixed([], 3))
Code
n=2 buggy: 35.0n=0 buggy: 25.0n=2 fixed: 35.0n=0 fixed: Noneempty fixed: None

The symptom. average_of_last(scores, 0) returns 25.0, which is the average of all the values. A user reported "my chart shows an average when I selected zero days".

What a hurried assistant might suggest (illustrative): "Wrap the function in a try/except and return 0 on error", or "add if n == 0: return 0."

Why those are wrong. Neither explains the cause. The real cause: in Python, values[-0:] is the same as values[0:], because -0 equals 0, so the slice starts at the beginning and returns the whole list. The code never failed, so a try/except does nothing. And return 0 invents a number: an average of nothing is not zero.

Hypothesis testing. Quick checks confirm it: print values[-0:] and values[-2:] and compare. Cause confirmed.

The fix and its design decision. The fixed function returns None for "nothing to average", so callers can tell it from a real average of 0. Whether to return None, raise an error or return 0 is a design decision that depends on the callers, not something the assistant can decide for you. In the output above, the fixed version gives 35.0 for n=2 as before, and None for both n=0 and an empty list.

Regression test. Add tests such as: n=2 gives 35.0, n=0 gives None, an empty list gives None, n larger than the list length uses all values. Verify that the n=0 test fails on the old code and passes on the new.

Understanding. One sentence: "A negative-zero slice index starts at the beginning, so n=0 returned the whole list." You could now spot this class of bug elsewhere.

What to share, and what never to paste

#

Debugging prompts tend to contain the most sensitive material, because errors reveal internals.

Never paste, into any tool not approved for it:

  • Passwords, API keys, tokens, private keys, connection strings, .env files.
  • Customer data, personal data, production database contents.
  • Proprietary source code, if your employer does not allow it.
  • Stack traces and logs that include internal hostnames, user emails or file paths you should not expose.

What to do instead:

  • Redact. Replace secrets and identifiers with placeholders (<API_KEY>, user@example.com).
  • Minimise. Share the smallest function and the error, not the whole repository.
  • Use a made-up reproduction. Often you can recreate the bug with fake data.
  • If you ever paste a real secret by accident, treat it as exposed: rotate it (create a new one and disable the old) and tell your security contact if you have one.

Privacy and Security covers this in more detail.

"Fixes" that hide bugs

#

Review every proposed change against this list:

Pattern

Why it is dangerous

Wrapping code in a broad try/except that ignores the error

The bug continues silently, and your data may be wrong

Returning a default value (0, empty string) when something fails

Invents data and hides failure

Adding sleep or increasing timeouts

Masks a race condition or a slow dependency

Disabling validation, type checks or certificate verification

Removes a safeguard to make a symptom go away

Special-casing the failing input

Fixes your test, not the logic

Deleting or loosening the failing test

Fixes the report, not the program

Pinning, downgrading or "force" flags without understanding

Can hide an incompatibility or reintroduce a vulnerability

A large rewrite for a small bug

Increases the area you must review

Ask yourself after reading the diff: "Does this change the cause, or only what I see?"

Commands the assistant tells you to run

#

Assistants will sometimes tell you to run shell commands, change permissions or reinstall packages. Before running anything:

  • Understand it. Ask for an explanation of each part, and check it in the official documentation of the tool.
  • Be wary of destructive or broad commands: deleting files, force-pushing, changing permissions recursively, running scripts downloaded and piped straight into a shell, disabling security features.
  • Work on a branch or copy, with version control and backups, so you can undo.
  • Prefer the smallest scope (one folder, one file, a dry run if the tool offers it).

If you use an agent mode that runs commands for you, set the permissions narrowly, require approval for risky actions and review what it ran. Module 8 returns to this.

When the assistant goes in circles

#

If you have tried three fixes and the problem persists:

  1. Stop and restate the problem in two sentences with expected versus actual behaviour.
  2. Start a new conversation with the cleaned-up minimal reproduction, because the old thread may be cluttered by failed ideas.
  3. Ask it to explain the code before fixing it. If the explanation is wrong, you have found the misunderstanding.
  4. Question your own assumptions. Is the input what you think? Is it the code you think is running?
  5. Take a break or ask a colleague. A person who sees the problem fresh often finds it quickly.

Bugs that need measurement

#

Concurrency problems, performance regressions and memory leaks are hard to guess at. Do not accept a confident diagnosis. Collect evidence with profilers, logs, traces and benchmarks, and give those results to the assistant.

Common mistakes

#
  • Pasting the error and applying the first patch.
  • Skipping the reproduction and then not knowing if the fix worked.
  • Pasting secrets or proprietary code.
  • Accepting a fix you cannot explain.
  • Not adding a regression test.
  • Letting the assistant widen the change to "clean up" unrelated code.
  • Running unfamiliar commands on a machine or data you cannot restore.

Exercises

#

Exercise 1

Cause or symptom?

#

An assistant suggests wrapping a database call in try: ... except Exception: pass because the call sometimes raises a timeout. Explain what this does to the bug and propose two better follow-up questions.

Exercise 2: Write the hypothesis prompt

#

You have a function parse_price("Rs 1,200") that returns 1 instead of 1200. Write a debugging prompt that asks for ranked hypotheses and tests, without asking for a fix.

Exercise 3: Regression tests

#

For average_of_last_fixed, list five test cases (inputs and expected outputs) including edge cases, and say which should fail on the buggy version.

Exercise 4: Sanitise before sharing

#

Rewrite this stack-trace line so that it is safe to paste into a public assistant: File "/home/rahul/acme-billing/payments.py", line 88, in charge: requests.post("https://api.acme-internal.example/v2/charge?key=sk_live_EXAMPLE", json=card).

Show solutionHide solution

It does not fix the timeout. It hides it: the failure still happens, but nothing is reported, so the program carries on as if the call had succeeded. Data may not be saved, a payment may not be recorded, and no one will know until the consequences appear.

Better follow-up questions: "What are the possible reasons for a timeout in this call (slow query, network, lock contention, connection pool exhaustion), and how can I test each?" and "If a timeout is expected sometimes, how should my code handle it deliberately: retry with limits, log it, return a clear error to the caller?" The aim is to find the cause and then choose a deliberate response.

Exercise 2

Show solutionHide solution
Code
Bug: parse_price("Rs 1,200") returns 1, but I expect 1200. Python 3.11. Code:[paste the function]. Other inputs: "Rs 500" returns 500 (correct), "1200" returns1200 (correct).Do not give me a fix. List the three most likely causes, ordered by likelihoodgiven these inputs, and for each give one quick test (a print statement or a smallcall) that would confirm or rule it out.

Including the other inputs that behave correctly is valuable evidence. It suggests that the comma is involved, which narrows the hypotheses sharply.

Exercise 3

Show solutionHide solution

For average_of_last_fixed:

Input

Expected

Fails on buggy version?

([10, 20, 30, 40], 2)

35.0

No

([10, 20, 30, 40], 0)

None

Yes (returns 25.0)

([], 3)

None

Yes (raises an error, because it divides by zero)

([10, 20], 5)

15.0 (uses all values)

No

([10, 20, 30], 3)

20.0

No

On the buggy version, the empty list case actually raises ZeroDivisionError, because the window is empty. That is a second bug the original report did not mention, found because you wrote the edge cases.

Exercise 4

Show solutionHide solution
Code
File "/path/to/project/payments.py", line 88, in charge:requests.post("https://<internal-api-host>/v2/charge?key=<API_KEY>", json=card)

Also replace card with a description (json=<example card dict>) if the real value contains payment data. Removed: the username in the path, the internal hostname and the live secret key. If the real key was ever pasted somewhere it should not be, rotate it. A key beginning sk_live suggests a live payment key, which is highly sensitive.

Where to go next

#
  • Guide · Technology

    Writing Better AI Prompts: A Practical Guide

  • Guide · Technology

    Using AI Tools at Work Without Getting Into Trouble

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages