Few-Shot Examples

45 min intermediate Lesson 4

Learning Outcomes

  • Explain what few-shot learning means and why it works for code generation
  • Provide effective input/output examples that guide AI behaviour
  • Determine how many examples are needed for different tasks
  • Prioritise example quality over quantity
  • Choose between zero-shot and few-shot approaches based on task complexity

Lesson Plan

Segment Duration Topic
Intro 3 min The power of "show, don't tell"
Explain 8 min What few-shot learning means for AI coding
Demo 10 min Providing input/output examples in practice
Explain 7 min How many examples is enough
Demo 8 min Quality vs quantity of examples
Practice 7 min Zero-shot vs few-shot decision making
Wrap-up 2 min Key takeaways, preview next lesson

Before You Begin

Pre-work:

  • Complete Lessons 1-3 (Anatomy, Role/Context, Step-by-Step)
  • Think of a coding pattern you repeat often (API handlers, component structures, test files)
  • Find 2-3 existing files in your project that follow the same pattern

Shopping List:

  • Any AI coding tool (Claude Code, Cursor, or Codex CLI)
  • A project with established patterns you'd like AI to follow
  • Examples of your code that demonstrate your preferred style

1 What Few-Shot Learning Means for AI Coding

Few-shot learning is the technique of showing AI examples of what you want before asking it to produce new output. Instead of describing your desired format, style, or pattern in words, you demonstrate it.

The concept:

  • Zero-shot: "Write a test for this function" (no examples)
  • One-shot: "Here's an example test for a similar function. Write one for this new function."
  • Few-shot: "Here are 2-3 examples of tests in our codebase. Follow the same pattern for this new function."

Why few-shot works better for code:

Code has precise structural requirements — indentation, naming conventions, import patterns, error handling styles. Describing these in prose is tedious and error-prone. Showing them is instant and unambiguous.

Compare these two approaches:

Descriptive (zero-shot):

Write an Express route handler that:
- Uses async/await with try/catch
- Destructures parameters from req.params
- Calls a service method, not direct database access
- Returns JSON with a { data } wrapper on success
- Returns { error: message } on failure with appropriate status code
- Uses 201 for creation, 200 for retrieval
- Logs errors with the request ID from req.id

Demonstrative (one-shot):

Here's an example route handler from our codebase:

// GET /api/products/:id
const getProduct = async (req, res) => {
  try {
    const { id } = req.params;
    const product = await productService.findById(id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    return res.status(200).json({ data: product });
  } catch (err) {
    logger.error({ requestId: req.id, error: err.message });
    return res.status(500).json({ error: 'Internal server error' });
  }
};

Now write a similar handler for GET /api/orders/:id using orderService.findById.

The second approach communicates the same information — naming style, error handling, response format, logging — but without needing to spell out each rule. AI extracts the pattern from the example.

NOTE
Key Insight
Few-shot examples communicate implicit knowledge — the conventions, style choices, and patterns that are difficult to articulate but immediately obvious when you see them. One example is worth a hundred words of description.

2 Providing Input/Output Examples

The most powerful form of few-shot prompting shows explicit input/output pairs: "Given THIS, produce THAT."

Pattern: Input -> Output mapping

For code transformations:

Convert JavaScript functions to TypeScript with proper types.

Example 1:
Input:
function add(a, b) {
  return a + b;
}

Output:
function add(a: number, b: number): number {
  return a + b;
}

Example 2:
Input:
function getUser(id) {
  return db.query('SELECT * FROM users WHERE id = ?', [id]);
}

Output:
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

async function getUser(id: string): Promise<User | null> {
  return db.query<User>('SELECT * FROM users WHERE id = ?', [id]);
}

Now convert this function:
Input:
function createOrder(userId, items, discount) {
  const total = items.reduce((sum, item) => sum + item.price, 0);
  const finalTotal = total - (total * discount);
  return db.insert('orders', { userId, items, total: finalTotal });
}

Notice how the examples teach AI:

  • Add type annotations to all parameters and return values
  • Add interface definitions when the type is complex
  • Add async/await when the function involves I/O
  • The level of type specificity you expect

Pattern: Style transfer

For making AI match your project's conventions:

Write a React component following our project conventions.

Example component (Button.tsx):
import { type FC, type MouseEvent } from 'react';
import { clsx } from 'clsx';
import styles from './Button.module.css';

interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  onClick: (e: MouseEvent<HTMLButtonElement>) => void;
}

export const Button: FC<ButtonProps> = ({
  label,
  variant = 'primary',
  disabled = false,
  onClick,
}) => {
  return (
    <button
      className={clsx(styles.button, styles[variant])}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
};

Now create a TextInput component with: value, onChange, placeholder,
error message, and disabled state.

Pattern: Test generation

Write tests following the pattern used in our codebase.

Example test (product.test.ts):
describe('ProductService', () => {
  let service: ProductService;
  let mockRepo: jest.Mocked<ProductRepository>;

  beforeEach(() => {
    mockRepo = createMockRepo();
    service = new ProductService(mockRepo);
  });

  describe('findById', () => {
    it('returns the product when found', async () => {
      const expected = createTestProduct({ id: '123' });
      mockRepo.findById.mockResolvedValue(expected);

      const result = await service.findById('123');

      expect(result).toEqual(expected);
      expect(mockRepo.findById).toHaveBeenCalledWith('123');
    });

    it('returns null when not found', async () => {
      mockRepo.findById.mockResolvedValue(null);

      const result = await service.findById('nonexistent');

      expect(result).toBeNull();
    });
  });
});

Now write tests for OrderService with methods: create, findById, updateStatus.
TIP
Tip
When providing examples, use REAL code from your project rather than made-up examples. Real code carries subtle conventions (import order, spacing patterns, naming) that invented examples might miss.

3 How Many Examples Is Enough

Research and practical experience converge on a simple guideline: 2-3 examples is the sweet spot for most coding tasks.

Why 2-3 works:

  • 1 example might be treated as a single case, not a pattern. AI might follow it exactly rather than generalising.
  • 2 examples establish a pattern. AI can identify what varies and what stays constant.
  • 3 examples confirm the pattern and show edge cases. Diminishing returns begin here.
  • 4+ examples rarely help and consume context window space that could be used for the actual task.

When 1 example is enough:

  • The pattern is simple and obvious (naming convention, file structure)
  • You're just showing format/style, not logic
  • The example is comprehensive (covers happy path and error case)
Follow this file naming convention:
Example: getUserById.ts -> getUserById.test.ts

Create test files for: createOrder.ts, validatePayment.ts, sendNotification.ts

When you need 2 examples:

  • The pattern has variation you want to demonstrate
  • There are different cases that should be handled differently
Example 1 (single resource):
GET /api/products/:id -> getProductById(req, res)

Example 2 (collection with filters):
GET /api/products?category=X&sort=Y -> getProducts(req, res)

Now create handlers for the /api/orders endpoints (both single and collection).

When you need 3 examples:

  • The pattern has branching logic or multiple variants
  • You need to show edge cases or exception handling
  • The difference between examples is subtle
Example 1 (simple validation):
Input: { name: "" }
Output: { valid: false, errors: [{ field: "name", message: "Required" }] }

Example 2 (type validation):
Input: { age: "not-a-number" }
Output: { valid: false, errors: [{ field: "age", message: "Must be a number" }] }

Example 3 (cross-field validation):
Input: { startDate: "2024-03-01", endDate: "2024-02-01" }
Output: { valid: false, errors: [{ field: "endDate", message: "Must be after startDate" }] }

Now write validators for the order schema: { quantity, unitPrice, discount, shippingDate }
WARNING
Watch Out
More examples does NOT mean better results. Each example consumes context window space. If your 5 examples push the actual task out of AI's attention, you'll get worse results than 2 well-chosen examples. Be economical.

The diversity principle:

Your examples should be DIFFERENT from each other. Showing three nearly identical examples teaches nothing new. Choose examples that demonstrate different aspects of the pattern:

  • Example 1: The simple/happy path
  • Example 2: A variation or option
  • Example 3: An edge case or error handling

4 Quality vs Quantity of Examples

A single excellent example outperforms five mediocre ones. Here's what makes an example high-quality:

Characteristics of a GOOD example:

  1. Representative — it genuinely reflects the pattern you want repeated
  2. Complete — it includes imports, error handling, edge cases
  3. Clean — it follows best practices (no hacks, workarounds, or TODOs)
  4. Annotated — brief comments explain non-obvious choices (optional but helpful)

Characteristics of a BAD example:

  1. Outdated — uses deprecated APIs or old patterns you've since abandoned
  2. Exceptional — it's a special case, not representative of the general pattern
  3. Incomplete — missing error handling, imports, or edge cases
  4. Messy — contains workarounds, commented-out code, or inconsistencies

Demonstration:

Bad example to provide:

// TODO: refactor this
const handleSubmit = (e) => {
  e.preventDefault()
  // const oldValidation = validateForm(data) // removed
  if (data.email && data.password) { // basic check for now
    fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(data)
    }).then(r => r.json()).then(d => {
      if(d.token) {
        localStorage.setItem('token', d.token) // move to context later
        navigate('/dashboard')
      }
    }).catch(e => console.log(e)) // fix error handling
  }
}

AI will replicate the bad patterns: no types, TODO comments, console.log errors, no proper error handling, localStorage directly in the handler.

Good example to provide:

const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
  e.preventDefault();
  setIsLoading(true);
  setError(null);

  const validation = validateLoginForm(formData);
  if (!validation.valid) {
    setErrors(validation.errors);
    setIsLoading(false);
    return;
  }

  try {
    const response = await authService.login(formData.email, formData.password);
    authContext.setToken(response.token);
    navigate('/dashboard');
  } catch (err) {
    if (err instanceof AuthError) {
      setError(err.userMessage);
    } else {
      setError('An unexpected error occurred. Please try again.');
      logger.error('Login failed', { error: err });
    }
  } finally {
    setIsLoading(false);
  }
};

AI will replicate the good patterns: proper types, validation before submission, service abstraction, typed error handling, loading state management, finally block.

TIP
Tip
Before using code as a few-shot example, ask: 'Would I approve this in code review?' If not, clean it up first or find a better example. AI amplifies whatever patterns you show it — good or bad.

Curating your example library:

Keep a collection of "golden examples" — files in your project that represent your best patterns:

prompts/examples/
  route-handler.ts        # Ideal Express route
  react-component.tsx     # Ideal component structure
  unit-test.test.ts       # Ideal test file
  service-class.ts        # Ideal service pattern
  migration.sql           # Ideal migration format

When you need AI to follow your conventions, paste the relevant golden example.


5 When to Use Zero-Shot vs Few-Shot

Not every prompt needs examples. Here's a decision framework:

Use ZERO-SHOT (no examples) when:

  • The task is standard and well-understood ("Write a binary search function")
  • You're asking for something AI has seen millions of times in training
  • You want AI to use its own best practices rather than follow yours
  • The output format is obvious (a function, a class, a test)
  • You're exploring and don't have a fixed pattern in mind
Zero-shot works fine:
"Write a debounce utility function in TypeScript with configurable delay."

AI knows what debounce is, what it returns, and how it's typically implemented.

Use ONE-SHOT (1 example) when:

  • You have a specific format you want replicated
  • Your naming or structural convention differs from the "standard"
  • The task is clear but you want a particular style
One-shot is better:
"Generate a migration file like this example:

exports.up = (knex) => knex.schema.alterTable('users', (t) => {
  t.string('avatar_url', 500).nullable();
  t.index('avatar_url');
});

exports.down = (knex) => knex.schema.alterTable('users', (t) => {
  t.dropIndex('avatar_url');
  t.dropColumn('avatar_url');
});

Now create a migration that adds 'phone_number' (string, 20 chars, nullable, indexed)
and 'phone_verified' (boolean, default false) to the users table."

Use FEW-SHOT (2-3 examples) when:

  • The pattern has variation that needs demonstrating
  • Your conventions are unusual or project-specific
  • The task involves transformation with multiple input types
  • You've tried zero-shot and got inconsistent results
Few-shot is necessary:
"Convert API responses to our frontend models.

Example 1:
API: { user_id: "123", first_name: "Jane", created_at: "2024-01-01T..." }
Model: { userId: "123", firstName: "Jane", createdAt: new Date("2024-01-01T...") }

Example 2:
API: { order_id: "456", line_items: [...], total_cents: 2500 }
Model: { orderId: "456", lineItems: [...], totalAmount: 25.00 }

Convert this:
API: { product_id: "789", display_name: "Widget", price_cents: 999, is_active: true }
"

The examples teach: snake_case to camelCase, _cents to dollars, _at strings to Date objects, _id stays as string. Describing all these rules in prose would be longer and less clear.

Decision flowchart:

  1. Is this a standard, well-known task? -> Zero-shot
  2. Do you need a specific format? -> One-shot
  3. Does the pattern have variation? -> Few-shot (2 examples)
  4. Are there edge cases to demonstrate? -> Few-shot (3 examples)
  5. Did zero-shot give poor results? -> Add examples and retry
NOTE
Key Insight
Few-shot is your recovery strategy. When zero-shot fails — AI uses the wrong style, format, or approach — adding 1-2 examples of what you actually want is the fastest fix. Don't start with few-shot every time, but keep it ready as your primary debugging tool for prompt quality.

Questions & Answers

Q: Won't AI just copy the example exactly instead of generalising?
This can happen with one example if it's too similar to the target task. Using 2 examples with variation teaches AI what's constant (the pattern) versus what changes (the specifics). If AI is copying too literally, add diversity: show examples with different data types, different numbers of fields, or different complexity levels.
Q: Can I use examples from different projects or must they be from the same codebase?
Same codebase is ideal because it carries your project's implicit conventions. But examples from other projects work fine if you're demonstrating a universal pattern (like how to structure a test). The key is that the example represents what you want the output to look like — its origin matters less than its quality.
Q: How do I handle examples that are too long to paste?
Trim them to the essential parts. If your example route handler file is 200 lines, extract the 30 lines that demonstrate the pattern. Add a note: "Simplified for clarity — the actual file includes pagination and caching which aren't relevant here." Context window space is precious; use it for what teaches the pattern.
Q: Should I label my examples or just paste them?
Label them. "Example 1:", "Input:", "Output:", or "Here's how we do it:" all help AI understand what's an example versus what's the task. Without labels, AI might try to modify or extend your example instead of treating it as a template for new code.

Key Takeaways

  1. Show, don't tell — one example communicates more than paragraphs of description, especially for style and convention.
  2. 2-3 examples is the sweet spot — enough to establish a pattern without wasting context space.
  3. Quality beats quantity — one clean, representative example outperforms five sloppy ones.
  4. Use diverse examples — show different cases to teach the pattern, not a single case.
  5. Zero-shot first, few-shot as recovery — try without examples, add them when results aren't right.

Next Steps: In Lesson 5 — Constraints & Guardrails, we'll learn how to bound AI's output with positive and negative constraints that prevent common failure modes.