Prompting Patterns That Get Great Code

Prompting Patterns That Get Great Code

The quality of what Copilot produces is almost entirely determined by what you give it. The same model, given a vague prompt, produces vague code. Given a precise, well-structured prompt, it produces production-quality code. This lesson is a practical guide to prompting well.

Pattern 1: The Specification Comment

Before you write a function, write a detailed comment that specifies exactly what it should do. Treat it like a mini-spec:

python
# Merge two sorted lists into a single sorted list. # Both input lists may be empty. # Do not modify the input lists. # Time complexity should be O(m + n) where m and n are the lengths of the inputs. # Return a new list. def merge_sorted(a: list[int], b: list[int]) -> list[int]:

After this, press Enter. The model has everything it needs: input types, output type, behavior for edge cases (empty lists), and a complexity constraint.

Compare to just typing def merge_sorted(a, b): — the model would guess at types, might modify the inputs, and would not know about the complexity requirement.

Pattern 2: Show, Then Ask

If you want code that follows a specific style, show an example first:

typescript
// Existing code in the file: async function getUser(id: string): Promise<Result<User, AppError>> { try { const user = await userRepo.findById(id); if (!user) return err(new NotFoundError(`User ${id} not found`)); return ok(user); } catch (e) { return err(new DatabaseError("getUser failed", e)); } } // Now type: // Same pattern as getUser, but fetch a product by its SKU async function getProductBySku(sku: string): Promise<Result<Product, AppError>> {

Copilot will follow the exact same Result<T, E> pattern, the same error types, and the same structure because you showed it what the pattern looks like in your codebase.

Pattern 3: Constrain the Solution Space

When you do not constrain, the model chooses freely — sometimes picking patterns you did not want. Add constraints explicitly:

javascript
// Parse the CSV string and return an array of objects. // - Use only the standard library (no csv-parse or papaparse) // - Handle quoted fields that contain commas // - First row is always the header // - Return empty array if input is empty string // - TypeScript, strict mode function parseCsv(input: string): Record<string, string>[] {

Without the "no external library" constraint, Copilot might import csv-parse. Without "first row is header", it might treat headers as data.

Pattern 4: Write the Test First

Writing the test before the implementation is a powerful prompting technique. The test is a precise, executable specification:

python
def test_calculate_discount(): # 10% discount for orders over $100 assert calculate_discount(150.0) == 15.0 # No discount for orders under $100 assert calculate_discount(80.0) == 0.0 # Exactly $100 gets no discount (strictly greater than) assert calculate_discount(100.0) == 0.0 # Discount caps at $50 assert calculate_discount(1000.0) == 50.0

Now type

code
def calculate_discount(amount: float) -> float:
and Copilot will read the test and implement exactly the behavior described by the assertions. This is far more precise than a prose description.

Pattern 5: The Incremental Build

Never ask for a large feature in one shot. Break it into steps, verify each one:

Step 1:

code
// Step 1: Define the TypeScript interface for a shopping cart item // Fields: productId (string), quantity (number), unitPrice (number)

Accept and verify.

Step 2:

code
// Step 2: Function to calculate the total price of a cart // Input: CartItem[] // Output: number (sum of quantity * unitPrice for each item) // Round to 2 decimal places

Accept and verify.

Step 3:

code
// Step 3: Function to apply a discount code to a cart total // Discount codes: "SAVE10" = 10%, "SAVE20" = 20%, "FLAT5" = $5 off // Invalid codes return the original total unchanged

Each step is small enough to read and verify in 30 seconds.

Pattern 6: Paste the Error

When debugging, do not just ask "fix this". Paste the exact error:

python
# This function is raising: # TypeError: '<' not supported between instances of 'str' and 'int' # on line 12 when processing mixed-type lists. # Fix it to handle both string and int elements by converting to string before comparing. def find_min(items):

The error message plus the fix direction gives Copilot exactly what it needs.

Pattern 7: Reference the Docs

For APIs or libraries Copilot might not know well, paste the relevant documentation snippet:

typescript
// According to the Stripe API docs: // stripe.paymentIntents.create({ // amount: number (in smallest currency unit, e.g. cents), // currency: string (e.g. "usd"), // automatic_payment_methods: { enabled: true } // }) // Returns a PaymentIntent object with client_secret. // Create a payment intent for the given order amount in USD async function createPaymentIntent(amountInDollars: number) {

By including the relevant API signature in the comment, you prevent Copilot from inventing parameters.

What makes prompts fail

ProblemFix
No type informationAdd types to the comment or signature
No edge case mentionList edge cases explicitly: "empty input", "null", "negative numbers"
No pattern referenceOpen a similar file in another tab or paste an example
Too broadBreak into smaller steps
No constraintsExplicitly state what not to use or do
Outdated APIPaste the relevant docs section into the comment