How to Use Prompts for Programming: Code Generation, Debugging, Refactoring, Review and Tests

Prompts for Programming

To use prompts for programming well, treat each prompt as a small specification: say what you want done (write, fix, refactor, review, explain), give the context the model cannot see (language and versions, the surrounding code, the inputs and constraints), and define what a correct answer looks like (tests that must pass, behavior that must not change, the format of the output). Most bad AI-generated code comes from prompts that leave one of those three out, so the model fills the gap with the most common answer instead of the right one for your codebase.

Prompts for Programming
debugcode
developers

This guide covers the anatomy of a programming prompt, then gives copy-ready prompts for the five tasks developers use AI for most often: generating code, debugging, refactoring, reviewing and testing. It ends with a before-and-after rewrite and the mistakes that cost the most time.

The anatomy of a programming prompt

A programming prompt is any instruction you give a language model to produce, change or analyze code. The effective ones share a structure, whichever model or tool you use.

PartWhat it containsWhat goes wrong without it
ActionThe verb: implement, fix, refactor, review, explain, and whether to edit or only suggestThe model suggests when you wanted changes, or rewrites when you wanted advice
ContextLanguage and versions, frameworks, the relevant code, how the code is called, data shapesGeneric code that does not fit your stack or uses outdated APIs
ConstraintsWhat must not change, libraries allowed or banned, performance or security limitsUnrequested refactors, new dependencies, broken callers
Success criteriaTests that must pass, example inputs and outputs, edge cases to handleCode that looks right and handles only the happy path
Output formatDiff vs full file, code only vs explanation, where assumptions goAnswers that are hard to apply or verify

Both major vendors’ guides point in this direction. Anthropic’s prompting best practices for Claude note that asking “can you suggest changes” tends to get suggestions while asking it to change a function gets edits, and that explaining the reason behind a constraint helps the model apply it sensibly. OpenAI’s prompt engineering guide recommends separating instructions from reference material with Markdown sections and XML tags, and for coding work, requiring tests and validating patches rather than trusting that they applied.

A role line (“Act as a senior backend engineer”) is optional. It can set tone and level of detail, but it does not substitute for context. Five lines of real context beat any persona.

A reusable base template

This template covers most programming requests. Delete the lines that do not apply rather than leaving them vague.

<task>
<Implement | Fix | Refactor | Review | Explain> <what>.
Edit the code directly / Only suggest changes, do not rewrite.
</task>

<context>
Language / runtime: <e.g. Node.js 22, TypeScript 5.6>
Frameworks and key libraries: <name and version>
Where this code runs and who calls it: <...>
Relevant code:
<code file="path/to/file.ts">
PASTE
</code>
</context>

<constraints>
- Do not change: <public function signatures, output format, DB schema>
- Allowed dependencies: <only what is already in package.json>
- Must handle: <edge cases, error conditions>
</constraints>

<done_when>
- <tests that must pass, or example input -> expected output>
</done_when>

Output: <a unified diff | the full file | code then a short explanation>.
List any assumption you made under "Assumptions" at the end.

The tags keep your instructions apart from pasted code, which also makes it less likely that a comment inside the code is treated as an instruction. The “Assumptions” line is cheap insurance: it turns silent guesses into a short list you can check.

Generating new code

For new code, the most common failure is asking for a function when you need a feature. A request for “a registration endpoint” invites the model to invent validation rules, error formats and a storage layer. Specify the contract instead.

Implement a POST /users endpoint in Express 4 with TypeScript.

Contract:
- Body: { email: string, password: string, displayName?: string }
- 201 with { id, email, displayName } on success
- 400 with { error: { code, message, field } } on validation failure
- 409 if the email already exists

Rules:
- Validate with the zod schemas pattern used in the example below.
- Hash passwords with the existing hashPassword() in src/auth/hash.ts.
- Use the UserRepository interface below; do not write SQL directly.
- No new dependencies.

Follow the structure of this existing endpoint:
<example>
PASTE A SIMILAR ROUTE HANDLER FROM YOUR CODEBASE
</example>

Output the route handler and its unit tests as separate files.

Why it works: the contract defines success, the rules stop the model from inventing its own hashing or data layer, and the example file transfers your conventions better than any description of them. Parameterized queries, input validation and secret handling should be stated as rules when they matter, not left to a generic “make it secure” line; a security request is only as good as the specific threats it names.

Debugging

Debugging prompts need evidence. Give the model the full error, the code involved, and what you expected to happen, then ask for a diagnosis before a fix.

I get this error:
<error>
PASTE FULL TRACEBACK OR STACK TRACE
</error>

The function is supposed to <expected behavior>.
For input <X> it should return <Y>; instead <what happens>.

<code>
PASTE THE FAILING FUNCTION AND WHAT IT CALLS
</code>

1. Explain the cause, pointing to the line where the bad value
   originates (not only where it surfaces).
2. Propose the smallest fix.
3. Say what else this fix could affect.

A classic example is a Python KeyError: 'user_id' when reading a request. A bare “fix this” usually gets request.get('user_id'), which hides the error rather than explaining why the key is missing (a different content type, a renamed field, a missing auth middleware). Step 1 is what forces that explanation. The full set of symptom-specific prompts, including silent bugs and intermittent failures, is in the guide to debugging Python code with ChatGPT and Claude.

Refactoring

Refactoring means changing code’s structure without changing its behavior. The prompt has to say which structure to change, by what measure, and how behavior will be checked.

Refactor the CSV import script below to reduce peak memory use.
It currently loads the whole file; files can reach several GB.

Constraints:
- The output file must be byte-for-byte identical for the same input.
- Keep the command-line interface unchanged.
- Standard library only (csv module), no pandas.

Process:
1. First list the changes you plan and why, and wait.
2. After I approve, output the refactored script as a diff.
3. Give me a command to compare old and new output on a sample file.

The “wait” step is worth the extra round trip on anything non-trivial: reviewing a plan is faster than reviewing a rewrite. The equivalence check in step 3 is how you know behavior did not change. Two other refactoring patterns are worth keeping:

  • Scoped standards fixes: “Only fix naming and docstring violations against the conventions below. Do not alter logic.” Narrow scope keeps the diff reviewable.
  • Characterization tests first: for legacy code with no tests, get tests that pin the current behavior before any refactor. The legacy refactoring prompts cover that sequence.

Reviewing code

Review prompts work best with one concern per prompt: security, correctness, edge cases or missing tests. Ask for a quoted line and a concrete failure scenario for each finding, so you can verify it quickly.

Review this diff for correctness only, against this purpose:
<one-sentence purpose>.
Ignore style. For each finding: the quoted line, an input that
produces the wrong result, and the fix. If you find nothing, list
the cases you checked.

The four-pass version, with a security pass mapped to the OWASP Top 10, is in AI code review prompts.

Writing tests

The quickest improvement to AI-written tests is to separate thinking from typing: ask for a list of edge cases first, edit it, then ask for test code that follows an existing test file. The spec, not the implementation, should define expected values, or the tests will enshrine current bugs. The details and a worked pytest example are in the unit test prompts guide.

Before and after: rewriting a weak prompt

This example is illustrative. A developer wants a function to deduplicate customer records.

Before:

Write a Python function to remove duplicate customers from a list.

The likely result: a function that compares whole dictionaries or uses set() on emails, keeps an arbitrary record, and lower-cases nothing. It runs, and it is wrong for real data.

After:

Implement dedupe_customers(records: list[dict]) -> list[dict] in
Python 3.12, standard library only.

Two records are duplicates if their emails match after trimming
whitespace and lower-casing. When duplicates exist, keep the record
with the most recent "updated_at" (ISO 8601 string, always UTC), and
keep the original order of first appearance otherwise.

Records with a missing or empty email are never duplicates; keep them
all.

Done when these hold:
- [{"email": "A@x.com ", "updated_at": "2026-01-01T00:00:00Z"},
   {"email": "a@x.com", "updated_at": "2026-02-01T00:00:00Z"}]
  -> only the second record
- two records with email "" -> both kept

Output the function and pytest tests for the two cases above plus
three more edge cases you choose. List assumptions at the end.

The rewrite does not use any special phrasing. It states the rules a colleague would ask about: what counts as a duplicate, which record wins, what happens to bad data. The two examples in “done when” remove most of the remaining ambiguity, and the request for assumptions catches what is left.

Mistakes that cost the most time

  • Asking for too much at once. A whole feature in one prompt produces a large answer that is hard to review. Break the work into steps you can check; prompt chaining describes the pattern.
  • Leaving out versions. Models default to whatever was common in their training data, which often means deprecated APIs.
  • Pasting the whole repository. More code is not more context. Paste the code involved and the interfaces it touches.
  • Accepting code you do not understand. If you cannot explain the change in a code review, ask the model to explain it, or do not merge it.
  • Letting tests define the solution. Anthropic’s guidance warns against code that special-cases test inputs; ask for a general solution and say that tests verify it rather than define it.
  • Pasting secrets or customer data. Redact keys, tokens and personal data before pasting into a hosted model, and follow your organization’s policy on sharing source code.
  • Skipping the run. No prompt makes generated code correct by itself. Run it, run the tests, and read the diff.

The pattern under all of these prompts is the same: the model does well with the information a good colleague would need, and badly without it. Investing a minute in context and success criteria usually saves several rounds of correction.

Sources

Scroll to Top