Prompt Engineering for Code Generation
Learn prompt patterns for writing, reviewing, debugging, and refactoring code with LLMs, including practical templates and real examples.
What you'll learn
- ✓Prompt structures that produce production-quality code
- ✓How to prompt for code review and bug detection
- ✓Debugging prompts that find root causes
- ✓Refactoring prompts that improve existing code
- ✓Common mistakes that produce bad code from LLMs
Prerequisites
- •Programming experience in at least one language
- •Basic understanding of LLM prompting
Why Code Prompts Are Different
Prompting for code is harder than prompting for text. Code must be syntactically correct, logically sound, handle edge cases, and follow conventions. A paragraph with a minor error is still readable. A function with a minor error throws an exception.
This means your prompts need to be more precise. Vague prompts produce vague code. Specific prompts produce specific, working code.
The Code Generation Template
Every good code generation prompt has four parts: context, requirements, constraints, and output format.
CODE_GEN_PROMPT = """
CONTEXT:
I'm building a FastAPI REST API for a task management app.
We use PostgreSQL with SQLAlchemy ORM and Pydantic for validation.
Python 3.12, async throughout.
REQUIREMENTS:
Write an endpoint that:
1. Accepts a POST request to /tasks
2. Validates the request body (title: str required, description: str optional, priority: int 1-5)
3. Creates the task in the database
4. Returns the created task with its generated ID and created_at timestamp
CONSTRAINTS:
- Use async/await for all database operations
- Include proper error handling (400 for validation, 500 for DB errors)
- Follow existing project patterns (dependency injection for DB sessions)
- No comments explaining obvious code
OUTPUT:
Return the complete endpoint function with its Pydantic models.
Do not include imports or app setup, just the models and route.
"""
Compare this to a vague prompt like “Write a FastAPI endpoint for creating tasks.” The vague version will produce something that works in isolation but will not match your project’s patterns, error handling style, or database setup.
Prompting for Code Review
LLMs are excellent code reviewers when given the right prompt structure. The key is telling the model what to look for and how to report findings.
REVIEW_PROMPT = """Review this Python function for bugs, security issues,
and performance problems.
For each issue found:
1. Quote the problematic line(s)
2. Explain the issue in one sentence
3. Show the fix
If the code is correct, say "No issues found" and stop.
Focus on:
- Logic errors and off-by-one mistakes
- SQL injection or other security vulnerabilities
- Unnecessary allocations or O(n^2) patterns
- Missing error handling for common failure modes
- Race conditions in concurrent code
def get_user_orders(db, user_id, status=None):
query = f"SELECT * FROM orders WHERE user_id = ..."
if status:
query += f" AND status = '...'"
results = db.execute(query)
orders = []
for row in results:
order = dict(row)
order['items'] = db.execute(
f"SELECT * FROM order_items WHERE order_id = ..."
).fetchall()
orders.append(order)
return orders
"""
The model will correctly identify three issues: SQL injection via string formatting, N+1 query pattern (one query per order for items), and no error handling. The structured format (“quote, explain, fix”) gives you actionable output rather than a vague “this could be improved.”
Expected output:
The model will produce output identifying three issues: SQL injection via f-string interpolation in the query, N+1 query problem from executing a query per order for items, and missing error handling. Each issue comes with the problematic line quoted and a concrete fix.
Debugging with LLMs
Debugging prompts work best when you provide the error, the code, and what you expected.
DEBUG_PROMPT = """I have a bug in this Python code.
THE CODE:
def merge_sorted_lists(list1, list2):
result = []
i, j = 0, 0
while i < len(list1) and j < len(list2):
if list1[i] <= list2[j]:
result.append(list1[i])
i += 1
else:
result.append(list2[j])
j += 1
return result
THE BUG:
merge_sorted_lists([1, 3, 5], [2, 4, 6]) returns [1, 2, 3, 4, 5] instead of [1, 2, 3, 4, 5, 6].
The last element is always missing.
EXPECTED:
[1, 2, 3, 4, 5, 6]
Find the bug and show the fix."""
The model will identify that the function returns after the while loop without appending the remaining elements from whichever list was not fully consumed. The fix is to add:
result.extend(list1[i:])
result.extend(list2[j:])
return result
Providing the expected vs actual output is critical. Without it, the model has to guess what “wrong” means.
Refactoring Prompts
Refactoring prompts should specify what aspect to improve and what to preserve.
REFACTOR_PROMPT = """Refactor this function to be more readable and maintainable.
Preserve:
- The function signature (same inputs and outputs)
- All existing behavior (do not change what it does)
Improve:
- Extract magic numbers into named constants
- Replace nested if/else with early returns
- Add type hints
def calc_price(qty, type, member):
if type == 'A':
price = qty * 29.99
if member:
price = price * 0.9
if qty > 100:
price = price * 0.85
elif qty > 50:
price = price * 0.9
elif qty > 10:
price = price * 0.95
elif type == 'B':
price = qty * 49.99
if member:
price = price * 0.85
if qty > 100:
price = price * 0.8
elif qty > 50:
price = price * 0.85
elif qty > 10:
price = price * 0.9
else:
price = qty * 9.99
return round(price, 2)
"""
This prompt constrains the model appropriately. Without “preserve the function signature,” the model might rename parameters or change the return type. Without “do not change what it does,” it might “fix” behavior it considers wrong.
Test Generation
LLMs are particularly good at generating test cases because they can imagine edge cases.
TEST_GEN_PROMPT = """Write pytest tests for this function. Include:
- Happy path tests (normal inputs)
- Edge cases (empty input, single element, very large input)
- Error cases (invalid input types, None values)
- Boundary conditions
def paginate(items: list, page: int, page_size: int = 10) -> dict:
if page < 1:
raise ValueError("Page must be >= 1")
if page_size < 1:
raise ValueError("Page size must be >= 1")
total = len(items)
total_pages = (total + page_size - 1) // page_size
start = (page - 1) * page_size
end = start + page_size
return {
"items": items[start:end],
"page": page,
"page_size": page_size,
"total": total,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1,
}
Use descriptive test names that explain the scenario. Group related tests."""
The model will generate tests covering normal pagination, first and last pages, empty lists, page beyond range, page_size of 1, page_size larger than the list, and invalid arguments.
Prompting for Different Languages
Different languages benefit from different prompt styles.
For typed languages (TypeScript, Rust, Go), specify types upfront:
Write a TypeScript function that takes a Record<string, number> and returns
the top N keys sorted by value descending. Return type: string[].
Handle the case where N is greater than the number of keys.
For Python, emphasize error handling and edge cases:
Write a Python function that... Include type hints, a docstring,
and handle these edge cases: empty input, None values, duplicate keys.
For SQL, specify the database and schema:
PostgreSQL 15. Tables:
- users (id serial, name text, email text, created_at timestamptz)
- orders (id serial, user_id int references users, total numeric, status text)
Write a query that finds users who placed more than 5 orders in the last 30 days
with a total spend over $500. Include indexes that would optimize this query.
Common Mistakes
Mistake 1: No context about the existing codebase.
# Bad
Write a function to authenticate users.
# Good
We use FastAPI with JWT tokens stored in httpOnly cookies.
Auth middleware is in app/middleware/auth.py.
User model has fields: id, email, hashed_password, is_active.
Write a login endpoint that validates credentials and sets the JWT cookie.
Mistake 2: Not specifying what to exclude.
# Bad
Write a REST API for todo items.
# Good
Write the route handlers only for a todo CRUD API.
Do not include: app setup, database config, middleware, or Docker files.
Assume the FastAPI app and SQLAlchemy session are already configured.
Mistake 3: Asking for too much at once.
Do not ask the model to write an entire application. Break it into pieces:
- First prompt: database models
- Second prompt: API schemas (giving it the models as context)
- Third prompt: route handlers (giving it models and schemas)
- Fourth prompt: tests (giving it the handlers)
Each prompt builds on the output of the previous one, and each piece is small enough to be correct.
Key Takeaways
Code generation prompts need more precision than text prompts. Always provide context about your stack, explicit requirements with numbered steps, constraints about what to include and exclude, and the desired output format. Use separate prompt patterns for review, debugging, refactoring, and test generation. Break large code generation tasks into smaller, sequential prompts. And always specify your language version, frameworks, and patterns so the model matches your existing codebase.
Related articles
- Prompt Engineering Prompt Evaluation: Measuring and Improving Quality
Learn how to measure prompt quality with evaluation datasets, scoring rubrics, A/B testing, and automated grading to iterate on prompts with evidence.
- Prompt Engineering Few-Shot Prompting: Learning from Examples
Master few-shot prompting to teach LLMs new tasks through carefully selected examples, formatting patterns, and example ordering strategies.
- Prompt Engineering Multi-Turn Conversations: Context and Memory Patterns
Learn how to design multi-turn LLM conversations with effective context management, memory patterns, conversation state tracking, and production architectures.
- Prompt Engineering Writing Effective System Prompts
Learn how to craft system prompts that reliably control LLM behavior through persona setting, constraints, output rules, and guardrails.