Why your AI writes code that looks right and breaks in production
Ask ChatGPT to "write a FastAPI endpoint" and you'll get something that imports pydantic, uses @app.get, and looks convincing. Run it — ImportError, deprecated BaseSettings, a synchronous DB call inside an async def. The model didn't hallucinate a library; it hallucinated your context.
Code generation fails for three boring reasons: missing constraints (versions, style, edge cases), missing verification (no tests, no types, no lint), and missing scope ("build a service" instead of "add one function to this file"). The prompts below fix all three. Each one is a role contract: you give the model a spec, a definition of done, and a refusal clause — the same things a tech lead gives a junior.
These are the prompts I actually paste into ChatGPT and Claude Code daily. They assume you know the difference between a list and a tuple and that you've read PEP 8 at least once.
1. The Spec-First Prompt (kills 80% of hallucinations)
When to use: any non-trivial function. The model must restate the contract before writing code.
You are a senior {Python
|TypeScript|Go} engineer. Before writing any code,
restate the task as a numbered spec: inputs, outputs, error cases,
external dependencies, and what you are NOT going to do.
If anything is ambiguous, ask exactly 3 clarifying questions and stop.
Only after I reply "go" — write the implementation.
Real example — a retry decorator:
# Spec restated by the model:
# 1. Wrap sync function, retry on requests.exceptions.RequestException
# 2. Exponential backoff: base 0.5s, factor 2, max 3 attempts
# 3. Re-raise last exception; no logging side effects
import time, functools, requests
def retry(times=3, base=0.5):
def deco(fn):
@functools.wraps(fn)
def wrapper(*a, **kw):
for i in range(times):
try:
return fn(*a, **kw)
except requests.RequestException:
if i == times - 1:
raise
time.sleep(base * (2 ** i))
return wrapper
return deco
The spec step is where the model catches its own bad assumptions about requests vs httpx.
2. The Version-Lock Prompt
When to use: always. The model's training cutoff is not your requirements.txt.
Target environment:
- Python 3.12, Pydantic v2, FastAPI 0.115, SQLAlchemy 2.0 (async)
- Node 22, TypeScript 5.6, strict: true
Use ONLY APIs that exist in these versions. If unsure — say "unsure"
and cite the official docs URL. Do not use deprecated aliases
(e.g. `pydantic.BaseSettings`, `validator`, `orm_mode`).
Common traps this catches: Pydantic v1 @validator (v2 uses @field_validator), datetime.utcnow() (deprecated in 3.12), axios default import in ESM builds. One line saves an hour of stack traces.
3. Type-Driven TypeScript Prompt
When to use: any API boundary. Types are the cheapest tests you'll ever write.
Write a TypeScript module. Rules:
- strict: true, noUncheckedIndexedAccess: true
- No `any`, no `as` casts except at the JSON.parse boundary
- Export a discriminated union for the result type
- Include a type-level test using `satisfies`
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: string };
async function fetchUser(id: string): Promise<Result<User>> {
const r = await fetch(`/api/users/${id}`);
if (!r.ok) return { ok: false, error: `HTTP ${r.status}` };
return { ok: true, data: (await r.json()) as User };
}
Forcing a Result union means the compiler refuses to let you forget the error branch.
4. The SQL Explain-Before-You-Write Prompt
When to use: any query touching >100k rows.
You are a PostgreSQL 16 DBA. Given this schema and query intent:
1. Propose the query.
2. Show the expected EXPLAIN (ANALYZE, BUFFERS) shape.
3. Name every index it needs and the DDL to create them.
4. Flag any implicit cast that would disable an index.
-- Intent: top 10 customers by 30-day revenue
SELECT c.id, SUM(o.total) AS revenue
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.created_at >= now() - interval '30 days'
GROUP BY c.id
ORDER BY revenue DESC
LIMIT 10;
CREATE INDEX CONCURRENTLY idx_orders_customer_created
ON orders (customer_id, created_at) INCLUDE (total);
The INCLUDE (total) makes it an index-only scan. The model will not volunteer this unless you ask for the plan.
5. The Test-First Contract Prompt
When to use: bugfixes. Reproduce before you fix.
Write a failing test that reproduces the bug first.
Use pytest / vitest. The test must fail on the current code
and pass after the fix. Show both files.
This single habit eliminates the most common AI failure mode: a "fix" that changes behavior in three unrelated places because nothing pinned the original contract.
6. The Adversarial Review Prompt
When to use: before every PR.
You are a hostile code reviewer. Find:
- unhandled error paths and swallowed exceptions
- race conditions and TOCTOU bugs
- N+1 queries
- secrets or PII in logs
- anything that would fail under 10x load
Return a table: line
| problem | severity | fix.
If the code is clean, say so — do not invent issues.
The last sentence matters: without it, models pad reviews with cosmetic nitpicks.
7. The Refactor-With-Guardrails Prompt
When to use: legacy code you're afraid to touch.
Refactor this function without changing its public signature or behavior.
Constraints: keep the same exceptions, same return shape,
same side-effect order. Add type hints. Do not rename exports.
Provide a diff, not a rewrite.
"Provide a diff" is the key phrase — it forces the model to prove it understood the original before editing.
8. The Migration Prompt (Python 2→3, v1→v2, JS→TS)
When to use: dependency upgrades.
Migrate the following {Pydantic v1 / Vue 2 / CommonJS} code to
{version target}. Produce a table of every breaking change you
applied, with a link to the official migration guide section.
Do not touch anything not required by the migration.
The "table of changes + docs link" requirement makes the migration auditable — you can grep the guide and verify.
9. The Performance Budget Prompt
When to use: hot paths only.
Optimize this function to run in under {X} ms for {N} items.
Measure first with timeit / performance.now(). Show before/after numbers.
Reject any optimization that hurts readability unless it hits the budget.
Real numbers beat vibes. timeit output pasted back into the chat is the fastest way to keep the model honest.
10. The Error-Message Decoder Prompt
When to use: when you're stuck at 2 a.m.
Here is the full traceback and the 20 lines around the failure.
Explain the root cause in one sentence. Then give the minimal fix.
Do not rewrite the file. If the cause is environmental (version,
path, permissions), say so explicitly.
Pasting the traceback with surrounding context is the difference between a fix and a guess.
11. The Doc-and-Docstring Prompt
When to use: before handing code to a teammate.
Add docstrings in Google style to every public function.
Include Args, Returns, Raises, and one runnable example.
Keep it under 8 lines per function. No marketing language.
Runnable examples in docstrings double as doctests — pytest --doctest-modules will run them.
12. The "I Don't Know" Prompt
When to use: when the model sounds confident about a library you've never heard of.
For every API you reference, state whether you are certain it exists
in the target version, or whether you are guessing.
Format: `api_name — certain | unsure — source: <url>`.
If unsure, propose a version you are certain about instead.
This one prompt has saved more debugging hours than any other. Hallucinated APIs are the single most expensive failure mode in code generation — the model invents a plausible client.fetch_all() and you lose an afternoon.
The pattern behind all twelve
Every prompt above does the same three things: constrains the environment (versions, types, style), demands verification (tests, EXPLAIN plans, timeit numbers), and grants permission to say "I don't know". That last part is counterintuitive but critical — a model that can admit uncertainty is a model you can trust on the parts where it doesn't.
None of this replaces understanding your code. If you can't read the diff, you can't ship it. The prompts are a multiplier on judgment, not a substitute for it.
If you want to go deeper into prompt engineering for engineering work — few-shot patterns, ReAct loops, structured output — the ASI Biont blog has a companion piece on turning LLMs into predictable tools, plus hands-on material on Python backend and DevOps automation. Start with one prompt from this list tomorrow morning and measure the time you get back.
Comments