Skip to main content

Prompt Engineering for Code Generation: Techniques and Best Practices

Practical guide to prompt engineering techniques for reliable code generation with LLMs. Few-shot learning, chain-of-thought, role prompting, structured output, iteration and evaluation. Real examples with Ollama on-prem.

What do you think about this article?

Prompt Engineering for Code Generation: Techniques and Best Practices
AI EngineeringPhotos:RDNE Stock projectPexels

At a Glance

Practical guide to prompt engineering techniques for reliable code generation with LLMs. Few-shot learning, chain-of-thought, role prompting, structured output, iteration and evaluation. Real examples with Ollama on-prem.

Prompt engineering for code generation is the art of formulating precise requests to an LLM (Large Language Model) like llama3.1:8b on Ollama, to obtain reliable, maintainable, and directly usable code. Unlike free-form text generation, prompt engineering for code requires rigor: specificity of intent, structured format, concrete examples (few-shot), and step-by-step reasoning (chain-of-thought). This guide covers production-tested techniques from Federico Calò's backend—from prompt structure basics to automatic output evaluation and intelligent iteration. We'll use Ollama on-prem locally and Python to wire the end-to-end workflow, avoiding cloud provider dependencies.

Introduction: Why Prompt Engineering for Code Matters

In 2026, code generation via LLM is a given: feature flag implementation, boilerplate reduction, refactoring scaffolding. But the quality of generated code is directly proportional to prompt quality. A vague prompt generates vague code; a precise prompt with clear examples and constraints generates production-ready code.

The challenge: on-prem LLMs like llama3.1:8b have context window constraints (8k tokens for standard Ollama) and haven't seen your entire codebase. So the prompt must be self-contained and specific enough to disambiguate the task.

This guide teaches the 6 pillars of prompt engineering for code:

  • Spec-first prompting: declare intent and output format before the request
  • Few-shot learning: provide concrete examples in the prompt itself
  • Chain-of-thought reasoning: guide the model to reason step by step
  • Role prompting: assign a role to the model for context awareness
  • Structured output: force the model to output JSON, code, or specific formats
  • Iteration and feedback: refine prompts based on real results

Pillar 1: Spec-First Prompting — The Foundation

What Spec-First Means

Spec-first prompting means writing the prompt like a brief technical specification: crystal-clear intent, defined input/output format, named edge cases. It's not prose; it's a specification that the model executes.


import requests
import json

# Ollama local endpoint
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "llama3.1:8b"

def ollama_generate(prompt: str, temperature: float = 0.3) -> str:
    """Call Ollama on-prem and return generated text."""
    response = requests.post(
        OLLAMA_URL,
        json={
            "model": MODEL,
            "prompt": prompt,
            "stream": False,
            "temperature": temperature,
            "num_predict": 500
        }
    )
    return response.json()["response"]

# Vague prompt (ANTI-PATTERN)
vague_prompt = """
Write a Python function that validates an email.
"""

# Spec-first prompt (CORRECT PATTERN)
spec_first_prompt = """You are a Python function expert. Generate a Python function with:
INPUT:
- function name: validate_email
- parameter: email (string)
OUTPUT:
- Return: boolean (True if valid, False otherwise)
- Validation rules:
  1. Must contain exactly one '@'
  2. Local part (before @) must be 1-64 alphanumeric/dots/hyphens
  3. Domain (after @) must have at least one dot
  4. TLD must be 2-6 characters
- No external libraries (use re module only)
- Include 3 docstring examples

FUNCTION:
"""

result = ollama_generate(spec_first_prompt)
print(result)
  

The difference is dramatic. The specific prompt generates a function with type hints, docstring, and correct validation. The vague prompt generates pseudo-code or incomplete logic.

Pillar 2: Few-Shot Learning — Teaching via Examples

What Few-Shot Is

Few-shot learning means providing 1-3 complete input→output examples in the prompt before asking for the real task. The model learns from the pattern and replicates it for the new request.


# Few-shot prompt for API endpoint generation
few_shot_prompt = """Generate a FastAPI endpoint from the description. Follow the pattern:

EXAMPLE 1:
Description: Get user by ID
Response Format: JSON with id, name, email
Expected endpoint: GET /users/{user_id}

Code:
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.fetch_one("SELECT * FROM users WHERE id = $1", user_id)
    if not user:
        raise HTTPException(status_code=404)
    return {"id": user.id, "name": user.name, "email": user.email}

---

EXAMPLE 2:
Description: Create a new article with title and content
Response Format: JSON with id, title, created_at
Expected endpoint: POST /articles

Code:
@app.post("/articles")
async def create_article(article: ArticleRequest):
    article_id = await db.fetch_val(
        "INSERT INTO articles (title, content) VALUES ($1, $2) RETURNING id",
        article.title, article.content
    )
    return {"id": article_id, "title": article.title, "created_at": datetime.now()}

---

NOW:
Description: Delete an article by ID, return success status
Response Format: JSON with status and message
Expected endpoint: DELETE /articles/{article_id}

Code:
"""

result = ollama_generate(few_shot_prompt, temperature=0.2)
print(result)
  

Few-shot is powerful because the model doesn't infer the pattern from scratch; it sees it in examples and replicates it. Low temperature (0.2-0.3) makes the model deterministic and faithful to the pattern.

Pillar 3: Chain-of-Thought Reasoning

Guide the Model to Reason Step-by-Step

Chain-of-thought prompting asks the model to write its reasoning before generating code. This reduces logical bugs.


# Chain-of-thought prompt for a complex algorithm
cot_prompt = """Task: Generate a Python function to find the longest substring without repeating characters.

Before writing code, think through the approach:
1. Problem statement: Return the length of longest substring without repeating chars
2. Algorithm: Use a sliding window with a set to track chars in current window
3. Data structure: dict to store last seen index of each char
4. Edge cases:
   - Empty string → return 0
   - All unique chars → return length
   - All same chars → return 1
5. Complexity: O(n) time, O(min(n, charset_size)) space

Now generate the code:

def longest_substring_without_repeating(s: str) -> int:
"""

result = ollama_generate(cot_prompt, temperature=0.2)
print(result)
  

Asking for reasoning "outside the code" makes the model more precise. It's like rubber duck debugging with AI.

Pillar 4: Role Prompting — Assign a Role

Context and Persona

Role prompting assigns a role to the model ("You're a senior Python backend engineer", "You're a SQL optimization expert"). It improves code quality and tone of voice.


# Role prompting for SQL optimization
role_prompt = """You are a senior SQL database architect at Google Cloud.
Your job: optimize slow queries for PostgreSQL 16 with 1M+ rows.

Task: Given a slow query, rewrite it for performance.

Slow query:
SELECT u.id, u.name, COUNT(a.id) as article_count
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id
ORDER BY article_count DESC
LIMIT 10;

Optimize for:
1. Add missing indexes
2. Avoid full table scans
3. Use EXPLAIN ANALYZE to verify
4. Include index creation DDL

OPTIMIZED VERSION:
"""

result = ollama_generate(role_prompt, temperature=0.2)
print(result)
  

With role declared, the model generates index DDL, EXPLAIN suggestions, and senior-engineer comments. Without role, it might not think of these.

Pillar 5: Structured Output — Force the Format

JSON, Markdown, Specific Code

Structured output means explicitly saying: "Output valid JSON" or "Output only Python code, no comments". It reduces ambiguous parsing.


# Structured output prompting (JSON format forcing)
structured_prompt = """Generate a JSON schema validation function for a User object.

OUTPUT FORMAT (MUST be valid Python code, no markdown, no comments):
def validate_user(data: dict) -> tuple[bool, list[str]]:
    errors = []

    # Validation rules:
    # - name: required, string, 1-100 chars
    # - email: required, valid email format
    # - age: optional, integer, 18-120
    # - role: optional, must be in ['admin', 'user', 'guest']

    # Return: (is_valid: bool, errors: list of error messages)

def validate_user(data: dict) -> tuple[bool, list[str]]:
"""

result = ollama_generate(structured_prompt, temperature=0.2)
print(result)
  

By declaring "OUTPUT FORMAT: Python code, no markdown", the model adheres to it. If you omit it, it might wrap the code in triple backticks.

Pillar 6: Iteration and Feedback Loop

Refine Based on Results

A prompt is never perfect on first try. Iterate: generate, evaluate, refine.


import re

def validate_prompt_output(code: str) -> tuple[bool, list[str]]:
    """Validate generated code before use."""
    issues = []

    # Check 1: Contains at least one def
    if not re.search(r'^\s*def\s+\w+', code, re.MULTILINE):
        issues.append("No function definition found")

    # Check 2: Valid syntax
    try:
        compile(code, '', 'exec')
    except SyntaxError as e:
        issues.append(f"Syntax error: {e}")

    # Check 3: Type hints present
    if not re.search(r'->\s*\w+', code):
        issues.append("Missing type hints on return")

    return len(issues) == 0, issues

# Iteration loop
prompt = """Generate a function to parse CSV rows into dataclass instances."""
max_iterations = 3
for i in range(max_iterations):
    result = ollama_generate(prompt, temperature=0.2)
    is_valid, issues = validate_prompt_output(result)

    if is_valid:
        print(f"✓ Valid on iteration {i+1}")
        break
    else:
        print(f"✗ Iteration {i+1} issues: {issues}")
        # Refine prompt for next iteration
        prompt += f"\nFix: {issues[0]}"
  

Automatic validation + feedback loop transforms a mediocre prompt into a good one in a few iterations.

Context Window Management: Token Budget

Maximize Token Usage

Ollama with llama3.1:8b has ~8k token context. A long prompt + examples + long output generation = overflow. Strategy: be concise, prioritize examples, truncate marginally useful context.


def estimate_tokens(text: str) -> int:
    """Rough estimate of token count (1 token ≈ 4 chars)."""
    return len(text) // 4

def build_optimized_prompt(
    spec: str,
    examples: list[tuple[str, str]],
    task: str,
    max_context_tokens: int = 6000
) -> str:
    """Build a prompt within token budget."""
    lines = [
        "You are a code generation expert.",
        "",
        "SPECIFICATION:",
        spec,
        ""
    ]

    # Add examples up to budget
    current_tokens = estimate_tokens("\n".join(lines))
    for i, (input_ex, output_ex) in enumerate(examples):
        example_text = f"\nEXAMPLE {i+1}:\nInput: {input_ex}\nOutput: {output_ex}"
        if current_tokens + estimate_tokens(example_text) < max_context_tokens * 0.7:
            lines.append(example_text)
            current_tokens += estimate_tokens(example_text)

    lines.append(f"\nTASK:\n{task}")
    return "\n".join(lines)

# Usage
spec = "Generate a Python function that validates a credit card number using Luhn algorithm."
examples = [
    ("4532015112830366", "True"),
    ("1234567890123456", "False"),
]
task = "def validate_cc(card_number: str) -> bool:"

optimized = build_optimized_prompt(spec, examples, task)
print(f"Prompt tokens: {estimate_tokens(optimized)}")
result = ollama_generate(optimized)
  

Real-World Case Study: Generate a SQL Parser

End-to-End Application

Imagine generating a simple SQL parser for SELECT queries. We'll apply all pillars.


sql_parser_prompt = """You are a Python expert in parsing SQL queries.

TASK: Generate a simple SQL SELECT query parser class.

SPECIFICATION:
- Class name: SimpleSelectParser
- Input: SELECT query string (e.g., "SELECT id, name FROM users WHERE age > 18")
- Output: dict with keys: columns (list), table (str), where_condition (str or None)
- No external SQL libraries (no sqlparse)
- Must handle: SELECT col1, col2 FROM table WHERE condition

EXAMPLE:
Input: "SELECT id, name FROM users WHERE age > 18"
Output: {"columns": ["id", "name"], "table": "users", "where_condition": "age > 18"}

REASONING:
1. Use regex to extract SELECT clause, FROM clause, WHERE clause
2. Split columns on comma, strip whitespace
3. Extract table name after FROM
4. Extract everything after WHERE

CODE:
"""

result = ollama_generate(sql_parser_prompt, temperature=0.2)

# Validation and testing
test_input = 'SELECT id, email FROM customers WHERE status = "active"'
try:
    exec(result)
    parser = eval("SimpleSelectParser()")
    output = parser.parse(test_input)
    print(f"Parsed: {output}")
except Exception as e:
    print(f"Error: {e}")
    print("Refining prompt...")
  

Common Mistakes and How to Avoid Them

Frequent Anti-Patterns

1. Prompt too vague: "Write a sorting algorithm". The model generates bubble sort, quicksort, or merge sort at random. Solution: specify which algorithm and what complexity.

2. Incomplete context: Don't give enough background on the project. The model doesn't know if the function must integrate with PostgreSQL, Redis, or an external API. Solution: include brief comments with dependencies and constraints.

3. No output validation: Assume generated code is always correct. Solution: always compile, test with 1-2 simple cases, check type hints.

4. Temperature too high: With temperature > 0.5, the model diverges from examples and spec. Solution: use temperature 0.1-0.3 for code, 0.5-0.7 for creative text.

Production Best Practices

  • Version your prompts: Keep prompt_v1.txt, prompt_v2.txt, etc. Document what changed and why.
  • Cache prompts: If you use the same prompt 100 times, cache the response. Redis + hash the request.
  • Quality monitoring: Track how many generated outputs pass validation. Target: >90%.
  • Deterministic fallback: If Ollama is down or output is invalid, return a generic template or ask the user.
  • Rate limiting awareness: Ollama on-prem has finite resources. Limit 10 req/min per user, queue the rest.

FAQ

Q: How reliable is code generated by Ollama?

A: It depends on the prompt. With spec-first + few-shot + chain-of-thought + validation, success rate is 85-95% for simple-intermediate tasks (functions <100 lines, classic algorithms). For complex tasks (distributed architectures, numerical optimizations), drop to 50-70%. Always: generate, validate, manually review, test.

Q: Can I use a cloud AI service instead of Ollama?

A: Technically yes, but this article and Federico Calò's backend use ONLY Ollama on-prem for privacy, zero recurring costs, and full control. Metered cloud AI services add costs ($0.01-0.02 per request) and external dependencies. If you already have Ollama, use it.

Q: How do I maintain code style consistency in generated code?

A: In the prompt, cite a snippet of your style (variable names, structure, imports). Few-shot works: if you give 2 examples in your style, the generated code will follow it. Alternatively: post-process with Black (Python), Prettier (JS), or an automatic formatter.

Q: How many tokens do I consume per request?

A: Rough: spec (100-300 tok) + examples (200-500 tok) + task (50-200 tok) + output (100-500 tok) = 500-1500 token per request. Ollama has no token costs (it's on-prem), so iterating is free. With a metered service, you'd calculate prompt + completion token costs and optimize.

Q: The model fails often. What do I do?

A: Iteration + feedback. Track errors, update the prompt with constraints that prevent that error type, and retry. Example: if the model forgets type hints, add "MUST INCLUDE: Python 3.9+ type hints on all function parameters and returns" to the prompt. Version and keep a story of improvements.

Conclusion

Prompt engineering for code isn't abstract science; it's a practical skill with immediate ROI: less boilerplate to write, fewer bugs, faster deploys. The 6 pillars (spec-first, few-shot, chain-of-thought, role prompting, structured output, iteration) are the production-verified playbook. Use Ollama on-prem to avoid cloud costs and dependencies, always validate generated output, and iterate until coverage >90%. Next step: integrate this workflow into your CI/CD—generate code, validate, test automatically, and deploy if all passes.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects