Prompt Engineering for Code

Prompt engineering for code is the practice of crafting structured natural‑language or templated inputs that guide a large language model to produce, transform, or explain programming artifacts. It matters whenever you need to bridge a conceptual gap between intent and implementation—bootstrapping a function, porting logic between languages, drafting test suites, or exploring unfamiliar codebases without reading every file. The following sections are organized by what you’re trying to accomplish, with concrete prompt patterns, the standard tools you can use to ship the results, and the silent failure modes that catch people off guard.

Generating a snippet from a description

When you need a small, self‑contained function or module without opening an IDE or searching documentation, a well‑scoped prompt can return runnable code in a single step. The key is to specify the role, the exact signature, the desired output format, and any constraints that keep the model from over‑reaching.

Prompt pattern [role] + [language] + [signature] + [behavior spec] + [output format constraint]

Concrete command

curl -s -X POST http://localhost:3000/v1/complete \
  -H "Content-Type: application/json" \
  -d '{"prompt":"Write a Python function parse_log_line that takes a raw Apache log string and returns a dict with keys timestamp, method, path, and status. Handle quoted fields and return null for missing values. Output only the function body, no imports, no markdown fences.","temperature":0.2,"max_tokens":256}'

Exact output (typical)

def parse_log_line(line: str) -> dict | None:
    import re
    m = re.match(r'^(?P<timestamp>\S+) \S+ \S+ "(?P<method>\w+) (?P<path>[^"]+)" (?P<status>\d+)', line)
    if not m:
        return None
    return {
        "timestamp": m.group("timestamp"),
        "method": m.group("method"),
        "path": m.group("path"),
        "status": int(m.group("status")) if m.group("status") else None,
    }

Sharp edge The model frequently hallucinates import statements or uses functions from standard libraries that aren’t available in the target runtime version. Always greps the returned body against your project’s import whitelist before committing. If the regex depends on a specific Apache log format, test it against at least three real lines from your access logs; a single deviating field (e.g., a query string without a space) will cause the parser to silently return null for every entry.

Refactoring or translating code

Moving logic between languages or restructuring a function for readability is where prompt engineering shifts from “generate from scratch” to “preserve and adapt.” The prompt must encode the source semantics and the target idioms without relying on the model’s implicit knowledge of your codebase.

Prompt pattern [source snippet] → [target language] + [exact transformation rules] + [constraints on imports/patterns]

Concrete git workflow


python -c "
import json, sys
prompt = open('snippet.py').read() + '''
Translate this into idiomatic Go. Preserve the error‑return pattern (return err, not panic). Output only the Go function, no package declaration, no comments.
'''
print(prompt)
" > /tmp/translate_prompt.txt

curl -s -X POST http://localhost:3000/v1/complete \
  -H "Content-Type: application/json" \
  -d "$(cat /tmp/translate_prompt.txt)" > translated.go

git add translated.go
git commit -m "refactor: translate auth filter to Go"

Exact output (truncated)

func parseLogLine(line string) (map[string]string, error) {
    re := regexp.MustCompile(`^(?P<timestamp>\S+) \S+ \S+ "(?P<method>\w+) (?P<path>[^"]+)" (?P<status>\d+)`)
    matches := re.FindStringSubmatch(line)
    if matches == nil {
        return nil, fmt.Errorf("invalid log format")
    }
    m := make(map[string]string)
    m["timestamp"] = matches[1]
    m["method"] = matches[2]
    m["path"] = matches[3]
    m["status"] = matches[4]
    return m, nil
}

Sharp edge Context windows in most hosted models cap at 4 096–8 192 tokens. A Python file exceeding ~30 lines often gets truncated, causing the model to lose the tail of the function or the import block. Always split transformations into chunks under 200 lines, and verify that the generated imports (fmt, regexp, etc.) match the target language’s module system. If you’re translating a file with deep call graphs, the model may omit dependency setup; run go mod tidy or the equivalent after the first pass.

Generating test scaffolding

Tests are the most common output of code‑focused prompts, but generated suites often assume ideal state or skip edge cases. A precise prompt that enumerates signatures, coverage goals, and explicit error expectations yields far more usable results.

Prompt pattern [function signature] + [desired coverage percentage] + [list of edge cases with expected exceptions] + [assertion style]

Concrete output Prompt:

Generate a pytest test suite for def fetch_user(id: int) -> User: covering happy path, missing user (404), and malformed ID (ValueError). Assert return type and exception messages exactly. Use unittest.mock for the backend call.

Generated file (excerpt):

import pytest
from unittest import mock
from myapp import fetch_user

@mock.patch("myapp.backend.query")
def test_fetch_user_happy_path(mock_query):
    mock_query.return_value = {"id": 1, "name": "Alice"}
    user = fetch_user(1)
    assert user.id == 1
    assert user.name == "Alice"

def test_fetch_user_missing():
    import json, re
    with pytest.raises(ValueError, match="User not found"):
        fetch_user(999)

Sharp edge Generated tests frequently miss teardown logic or assume database state that doesn’t exist in CI. They may also use assert statements that raise AssertionError instead of the documented exception type, making it hard to distinguish test failures from code bugs. Always run the generated suite in a fresh, isolated database container before merging. If the model uses pytest.raises with a regex that’s too broad, you’ll get false positives; tighten the match pattern to the exact error message your backend emits.

Building multi‑file or config structures

When the goal is to scaffold a small service or a configuration set, the prompt must describe the component layout, inter‑file dependencies, and the exact export interface each file must present.

Prompt pattern [component list] + [file layout] + [export interface per file] + [build/runner command]

Concrete docker integration Prompt:

Generate a three‑file Node.js URL shortener module:
- routes.js: express app with a single POST /shorten route that calls db.save
- db.js: knex wrapper exposing a single save(slug, url) function
- validators.js: export a function isValidSlug(slug) that returns true only for [a‑z0‑9] strings of length 5‑10
Each file must export an init(app) function that accepts an express app instance. Output only the file contents, no package.json, no installation instructions.

Generated files (excerpt):

// routes.js
export function init(app) {
  app.post("/shorten", async (req, res) => {
    const { slug, url } = req.body;
    if (!isValidSlug(slug)) return res.status(400).json({ error: "bad slug" });
    await db.save(slug, url);
    res.json({ short: `${process.env.DOMAIN}/${slug}` });
  });
}
# After the files are on disk
docker build -t url-shortener .
docker run -p 3000:3000 url-shortener

Sharp edge Models frequently invent package names, import paths, or environment variable conventions that don’t resolve. A common failure is generating import db from "./db" when the actual file exports module.exports.save = …. Post‑process the output with sed or a linter before running docker build. Another silent failure: the model may omit the Dockerfile CMD or ENTRYPOINT entirely, causing docker run to exit immediately. Always inspect the generated Dockerfile (or generate it separately) and verify that EXPOSE, WORKDIR, and the start command match the runtime you intend to use.

Sharp edges

  • Context‑window truncation – Most hosted models cap token usage between 4 K and 32 K. Large source files or multi‑file prompts get cut, leading to incomplete functions, missing imports, or forgotten return statements. Split prompts into sub‑1 K chunks when working with files larger than ~150 lines.
  • Hallucinated APIs and imports – Generated code often references functions or packages that don’t exist in the target language or runtime. Always run a syntax check (python -c "compile(...)", go build, node --check) before committing.
  • Silent assumption of runtime state – Test generators may assume a database connection, environment variables, or global objects that aren’t present in a clean CI environment. Validate generated test suites against a fresh container before merge.
  • Flag confusion in API calls – When using curl (or any HTTP client) to query an LLM endpoint, -d sends JSON, while -F sends multipart form data. Using -F for a JSON payload silently URL‑encodes the body, often resulting in a 400 “invalid JSON” response. Stick to -H "Content-Type: application/json" -d '...' for JSON‑based completions.
  • Prompt drift – Adding too many constraints (“no imports, no comments, use only stdlib, avoid regex, prefer functional style”) can push the model into a corner where it produces syntactically valid but semantically wrong code. Draft prompts iteratively: start with a minimal viable request, inspect the output, then add constraints one at a time.

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.