Level: Intermediate Estimated time: ~2 hours Prerequisites: Level 1 complete (Modules 01–03) — Module 03 in particular. This module extends compact prompting with full structured scenario coverage. Verified: 2026-07
⚠️ AI Credit note: Most prompts in this module run on the default model with short Ask-mode turns. Security review prompts recommend a frontier model — marked at the point of use.
Module 03 taught you to keep prompts small and choose the right mode. This module teaches you what to put inside those prompts.
A structural prompt is not a better-worded request — it is a different type of input. When you give Copilot a specific task with explicit constraints and a stated output format, you get deterministic, reviewable output. When you give it an open-ended request, you get generic output that requires iterative correction. Iteration is the enemy of efficient context usage.
By the end of this module, you will be able to:
- Decompose any coding task into the 4-component prompt structure: task, role, constraints, output format
- Apply the correct prompt pattern for each of the 7 core coding scenarios — plus the migration scenario — without looking them up
- Identify 5 prompt anti-patterns on sight, diagnose them by name, and rewrite them
- Chain prompts for multi-step tasks: diagnosis before fix, migration in three sequential steps
- Write a parameterized, reusable prompt template and promote it to a runnable
.prompt.mdfile - Scope prompts for security review and migration without generating vague or hallucinated output
- Estimate whether a given prompt scenario warrants a frontier model
Every effective Copilot prompt contains between two and four components:
| Component | Required? | Purpose |
|---|---|---|
| Task | Always | A single verb + a single noun: what to produce |
| Role | Optional | The expert perspective Copilot should take |
| Constraints | Always when any apply | What must be true of the output |
| Output format | Always when non-default | How the result should be structured |
Completeness test: Read your task line aloud. Does it contain exactly one verb and one noun? If two actions are in the sentence, split the prompt.
Before (incomplete):
Help me with this function — it seems broken and also the tests aren't great.
After (structured):
Role: Act as a Python debugging specialist.
Task: Diagnose why `merge_sorted(a, b)` returns an incorrect result when one list is empty.
Constraints: Do not propose a fix yet — provide the diagnosis first.
Output: A numbered list of candidate causes, each with one supporting observation from the code.
Each scenario below gives the pattern and its failure mode. Each also links its ready-to-use entry in the course prompt library — every library entry follows the same guided format (The Prompt → Example Usage → Expected output → Common Failures), so open the library entry when you want a worked, filled-in example of the pattern.
Key discipline: Name the function, name its parameter types and return type, state the constraints, and ask for only the function body.
Pattern:
Task: Write a Python function `[NAME]([PARAMS])` that [ONE-SENTENCE DESCRIPTION].
Constraints: [TYPE CONSTRAINTS] · [EDGE CASE HANDLING] · [WHAT TO OMIT]
Output: Function body only — no surrounding module, no usage example.
What goes wrong without it: Copilot generates a plausible stub with wrong assumptions about parameter types or missing edge case handling.
Reusable version of this pattern: prompts/generation/generate-function.md
Key discipline: State what changes. State what must not change. Specify the file and selection.
Pattern:
Task: Refactor `[FUNCTION NAME]` in [FILE] to extract [SPECIFIC CONCERN] into a separate function.
Constraints: Do not change the public interface. Do not change behavior. Keep existing tests passing.
Output: The refactored function and the extracted helper — no other changes.
What goes wrong without it: Copilot renames variables, adds imports, or modifies the calling code — all outside the stated scope.
Reusable versions of this pattern: prompts/refactoring/extract-function.md · prompts/refactoring/refactor-for-clarity.md
Key discipline: Describe symptoms, expected behavior, and actual behavior. Ask for diagnosis before asking for a fix.
Pattern:
Task: Diagnose why [FUNCTION NAME] produces [ACTUAL OUTPUT] when the expected output is [EXPECTED OUTPUT].
Context: [PASTE FUNCTION] [PASTE FAILING CALL]
Constraints: Diagnose only — do not suggest a fix in this message.
Output: A numbered list of candidate causes ordered by likelihood, each with one supporting observation.
What goes wrong without it: Copilot proposes a fix before identifying the root cause, fixing a symptom rather than the problem.
Reusable version of this pattern: prompts/debugging/debug-from-symptoms.md
Key discipline: Name the function under test. List the edge cases explicitly. Specify the framework and assertion style.
Pattern:
Task: Write pytest tests for `[FUNCTION NAME]`.
Constraints: Cover these cases: [LIST EACH CASE]. Use `assert` statements — no `unittest.TestCase`. Each test in its own function.
Output: Test file content only — no CLI instructions.
What goes wrong without it: Copilot writes the happy path only. Boundary cases, null input, and error conditions are skipped.
Reusable version of this pattern: prompts/testing/write-tests.md
Key discipline: State the audience, format, and depth.
Pattern:
Task: Write a docstring for `[FUNCTION NAME]`.
Constraints: Audience is a Python developer who will call this function, not maintain it. Use Google-style format. Include Args, Returns, Raises — no usage example.
Output: The docstring only, inside triple quotes.
What goes wrong without it: Copilot writes a one-line description that restates the function name. Or adds a verbose usage example the caller doesn't need.
Reusable version of this pattern: prompts/documentation/write-docstring.md
Key discipline: Scope the review to a specific concern. State what is out of scope.
Pattern:
Task: Review `[FUNCTION NAME]` for [SCOPE: logic errors | OWASP A01 | style | test coverage].
Constraints: Focus only on [SCOPE]. Do not comment on naming, formatting, or anything outside [SCOPE].
Output: A numbered list — each item: finding · severity · suggested fix.
What goes wrong without it: Copilot produces a wall of vague suggestions across every concern at once. Nothing is actionable.
Reusable version of this pattern: prompts/review/code-review.md
Key discipline: Anchor to an OWASP category. Give the language, framework, and version. Ask for the exploit path before the fix.
Pattern:
Role: Act as a security engineer specializing in [LANGUAGE/FRAMEWORK].
Task: Review `[FUNCTION NAME]` for vulnerabilities matching [OWASP CATEGORY, e.g., A02:2021 Cryptographic Failures].
Context: [LANGUAGE] [VERSION] · [FRAMEWORK] [VERSION]
Constraints: Describe the exploit path before proposing any fix. Limit scope to [OWASP CATEGORY].
Output: Finding · exploit path · recommended fix · one-line code example of the fix.
What goes wrong without it: Copilot generates a broad list of generic security advice not tied to the actual code or framework version.
Reusable version of this pattern: prompts/security/security-audit.md
Migration prompts fail more often than any other type. The reason is scope: a migration spans multiple files, multiple call sites, and multiple failure modes. A single prompt cannot handle all of that reliably.
Break migration into three sequential prompts:
| Prompt | Task | Output |
|---|---|---|
| 1 | Identify all call sites that use [OLD API] in [FILE] |
A numbered list of line references |
| 2 | For a single call site at [FILE:LINE], rewrite it to use [NEW API] |
The updated line(s) only |
| 3 | Confirm that the rewritten call preserves [SPECIFIC BEHAVIOR] |
A yes/no with justification |
Pattern for Prompt 1:
Task: List every call site that uses [OLD API METHOD] in [FILE].
Constraints: Include only direct calls — exclude comments and import statements.
Output: A numbered list, one per line: line number · call expression.
Key constraints for migration prompts:
- State the source API version and the target API version explicitly.
- Specify what must be preserved: return type, error handling contract, side effects.
- Ask for breaking changes before asking for the rewrite.
Reusable version of this pattern: prompts/migration/migrate-api.md
Prompt chaining is the practice of using the output of one prompt as the context for the next. It is mandatory for multi-step tasks — the migration scenario above is a three-prompt chain — and optional for complex single-step tasks.
When to chain:
- The task has more than one distinct action.
- The second action depends on the result of the first.
- You need to verify an intermediate result before committing to the next step.
Chaining rules:
- Each prompt in a chain must be completable on its own. If Prompt 2 is meaningless without Prompt 1's output, that output must be pasted into Prompt 2's context.
- Never ask for diagnosis and fix in the same prompt. Diagnosis first, fix second.
- End each prompt with a confirmation gate: "Reply only with the [DELIVERABLE]. Do not proceed to the next step."
Example chain — debug and fix:
# Prompt 1 — diagnose
Task: Diagnose why merge_sorted returns an incorrect result when one list is empty.
Constraints: Diagnose only. Do not propose a fix.
Output: Numbered list of candidate causes, ordered by likelihood.
# Prompt 2 — fix (after confirming diagnosis)
Context: The root cause is [PASTE CONFIRMED CAUSE FROM PROMPT 1].
Task: Fix `merge_sorted` to handle the empty-list case correctly.
Constraints: Change only the lines that address the root cause. Do not rename variables.
Output: The corrected function body only.
| Anti-pattern | Why it fails | Rewrite rule |
|---|---|---|
| Vague target — "Improve this function" | Copilot must guess the intent | Name the specific change: "Extract the rate lookup into a separate function" |
| Double task — "Fix the bug and add tests" | Two tasks compete for the same output | Split: diagnose → fix → then prompt for tests separately |
| Context dump — Paste 300 lines with no question | Copilot summarises instead of acts | Scope to the specific function; state the exact question |
| Missing output format — "Explain how to add error handling" | Copilot writes prose when you want code | Add "Output: code only" or "Output: numbered list" |
| Premature fix — "Fix the off-by-one error in merge_sorted" | Assumes the diagnosis is correct | Ask for diagnosis first; confirm it; then ask for the fix |
A prompt is reusable when it contains [UPPERCASE] placeholders for every value that changes between uses, and the surrounding text is stable.
Signs a prompt is ready to commit to the library:
- You have used it successfully at least twice.
- All variable parts are marked with
[PLACEHOLDER]names. - The output format is explicit.
- You have noted one common failure and how to fix it.
Use templates/prompt-template.md as the standard format. See prompts/ for working examples.
A prompt template is ready for a shared library when every value that varies between uses is a named [PLACEHOLDER]. Generic placeholders like [CODE] or [FUNCTION] are not sufficient — they do not communicate what value is expected.
Naming rules for placeholders:
- Use a noun that names the category of value:
[FUNCTION_NAME],[TARGET_FRAMEWORK],[OWASP_CATEGORY]. - Use
_between words. All uppercase. - Include the unit where it matters:
[MAX_LINES],[PYTHON_VERSION].
Template validation checklist:
- Every input that changes between uses has a
[PLACEHOLDER]. - The output format line names the exact structure (numbered list, code block, table).
- The constraints line states at least one thing the output must NOT contain.
- The template produces correct output when tested with at least two different sets of values.
Example of under-parameterized vs well-parameterized:
Under-parameterized:
Write a test for the validation function using pytest.
Well-parameterized:
Task: Write pytest tests for `[FUNCTION_NAME]` in `[FILE_PATH]`.
Constraints: Cover these cases: [EDGE_CASE_LIST]. One test function per case. Use bare `assert` — no TestCase.
Output: Test file content only — starting from the import line.
A prompt file is the productized form of a parameterized template. Instead of pasting a Markdown prompt and filling placeholders by hand, you save the prompt as a .prompt.md file and invoke it directly in chat with /.
| Scope | Location | When to use |
|---|---|---|
| Workspace | .github/prompts/*.prompt.md (or .prompts/) |
Repo-wide team prompts, version-controlled |
| User | VS Code user prompts directory | Personal prompts that follow you across projects |
---
mode: agent
model: claude-sonnet
tools: ['codebase', 'editFiles']
description: Extract a long function into smaller helpers
---
Task: Refactor `${input:targetFunction}` in `${file}` by extracting helpers.
Constraints:
- Each helper has one responsibility and ≤ 20 lines.
- Public signature of `${input:targetFunction}` does not change.
- Existing tests must still pass.
Output: The full updated file content only.Frontmatter fields:
mode—ask,agent, or omit to inherit current mode.model— pin a specific model; omit to use the chat's current model.tools— list of tool names (Agent mode only) the prompt may use.description— shown in the/picker.
Body substitutions:
${input:variableName}— prompts the user when invoked.${input:variableName:default}— with a default value.${selection}— current editor selection.${file}— current file path.${workspaceFolder}— repo root.
| Mechanism | Activation | Best for |
|---|---|---|
| Instruction file (Module 05) | Always-on for matching scope | Conventions, style, "always do X" |
Prompt file (.prompt.md) |
On-demand via / |
Reusable actions: extract function, write tests, security audit |
| Custom agent (Module 06 + 08) | On-demand via @ |
Scoped role with bounded tool permissions |
Quick rule: If the rule should fire every turn → instruction file. If it is a named action I run sometimes → prompt file. If it is a bounded role with restricted tools → agent.
- Naming:
verb-object.prompt.md—extract-function.prompt.md,audit-security.prompt.md. - One objective per file. If you find yourself adding
ORto the description, split it. - Version with the repo. Treat prompts like code — review them in PRs, change
Verifieddates when they change. - Link upstream. When a prompt relies on a path-specific instruction file, mention it in the description.
- Prompt files for one-off tasks. If you will run it once, just type the prompt — no file needed.
- Secrets in frontmatter. Frontmatter is committed. No tokens, no internal hostnames.
- Body bloat. A prompt body over ~150 lines is doing too much — split into two prompts and chain them.
- Inheriting a model with no rationale. If a prompt pins a specific model, the description must say why — otherwise the next contributor will not know whether to keep or remove the pin.
- Prompt files as documentation. A
.prompt.mdfile is something Copilot runs. If you want humans to read it for guidance, that belongs inprompts/README.mdor ininstructions/.
The course already contains a Markdown prompt library at prompts/. Each file there can be promoted to a runnable .prompt.md:
- Copy the prompt file to
.github/prompts/[name].prompt.md. - Convert each
[PLACEHOLDER]to${input:placeholderName}. - Add frontmatter with
mode,description, and (if needed)modelandtools. - Run it twice with different inputs to verify variables substitute correctly.
- Add a one-line cross-reference back to the source Markdown so contributors know they are paired.
Guided example — promote the testing prompt. Take prompts/testing/write-tests.md and save its core prompt as .github/prompts/write-tests.prompt.md:
---
mode: ask
description: Generate a pytest suite for a named function with explicit edge cases
---
Task: Write pytest tests for `${input:functionName}` in `${file}`.
Constraints: Cover these cases: ${input:edgeCaseList}. One test function per case. Use bare `assert` — no `unittest.TestCase`.
Output: Test file content only — starting from the import line.Invoke it in chat with /write-tests; VS Code asks for functionName and edgeCaseList before sending. Run it twice with different inputs — visibly different output proves the substitution works. Note what changed from the Markdown source: [FUNCTION_NAME] became ${input:functionName}, the file path became ${file}, and the frontmatter carries the description into the / picker.
Verify availability and frontmatter schema against the VS Code prompt files docs — both the format and the supported tools list evolve.
| Scenario | Recommended model | Reasoning |
|---|---|---|
| Code generation | Default | Syntax tasks — no multi-step reasoning needed |
| Refactoring | Default | Structural, not semantic |
| Documentation | Default | Language generation, well within default capability |
| Code review — style + logic | Default | Pattern matching |
| Testing | Default | Mechanical coverage expansion |
| Debugging — simple logic | Default | Sufficient for most bugs |
| Debugging — async / concurrency | Frontier | Multi-step causal reasoning needed |
| Security review (OWASP scope) | Frontier | Exploit-path reasoning requires depth |
| Migration, large codebase | Auto or frontier | Depends on complexity |
Rule of thumb: Only escalate to a frontier model when the task requires multi-step causal reasoning that auto model selection demonstrably gets wrong. Always try auto first.
Work through these in order — they build from component analysis to a full prompt chain. Every example gives you the exact prompt, what Copilot should produce, and what to pay attention to. The prompt library linked from each scenario above uses this same guided format for the eight scenario patterns themselves — the examples here cover what the library does not: spotting missing components, rewriting anti-patterns, chaining, and model decisions.
Before writing structured prompts, learn to see what is absent. Read each prompt and decide which of the four components is missing — then compare with the analysis below.
Prompt A:
Add error handling to this function.
Prompt B:
Role: Act as a Python expert.
Task: Rewrite the `calculate_tax` function.
Output: The refactored function only.
Prompt C:
Task: Write tests for `validate_email`.
Constraints: Use pytest. Cover null, empty string, and valid input.
Analysis:
| Prompt | Missing | Consequence |
|---|---|---|
| A | Constraints (which errors to handle, what to return on error) and output format | Copilot guesses the error policy and wraps everything in a generic try/except |
| B | Constraints (what to change, what must not change) | "Rewrite" with no boundaries — Copilot may change the interface and behavior |
| C | Output format (file content? function names? assertion style?) | You get tests in an arbitrary structure that you then reformat by hand |
What to observe: the absent component is always the one Copilot fills with a guess. Every guess is a potential correction turn.
Each prompt below contains at least one anti-pattern from the table above. Diagnose it by name, then compare your rewrite with the corrected version.
Prompt A:
Look at my validate_email function. It has some issues with how it handles edge cases. Can you help me make it more robust and also add some tests while you're at it?
Diagnosis: Double task — improving the function and writing tests compete for the same output, so both come back half-done. "Some issues with edge cases" is also a vague target: Copilot must guess which edge cases you mean.
Corrected (two sequential prompts):
Task: Rewrite `validate_email(address)` to reject addresses with leading or trailing whitespace.
Constraints: Keep the return type `bool`. Do not use external libraries. Do not write tests in this message.
Output: The updated function only.
Task: Write pytest tests for the updated `validate_email`.
Constraints: Cover: valid address, missing @, whitespace-padded address, None input. One test function per case. Bare `assert` only.
Output: Test file content only.
Expected difference: instead of a mixed blob of partially revised function plus two generic tests, you get a reviewable function change first, then a test suite targeting the exact cases you named.
Prompt B:
Fix the security problem in hash_password.
Diagnosis: Premature fix — it assumes the diagnosis and skips the exploit path. It also gives no language, version, or OWASP scope, so the "fix" arrives without the reasoning you need to review it.
Corrected:
Role: Act as a security engineer specializing in Python.
Task: Review `hash_password` for vulnerabilities matching OWASP A02:2021 (Cryptographic Failures).
Context: Python 3.11 · no external auth library.
Constraints: Describe the exploit path before proposing any fix. Limit scope to A02.
Output: Finding · exploit path · recommended fix · one-line code example of the fix.
Expected difference: instead of an unexplained code swap, you get the finding (MD5 is cryptographically broken), the exploit path, and a fix you can evaluate — in that order.
Prompt C:
Here's my whole utils file. Review it and tell me everything that's wrong with it.
[paste of 250 lines]
Diagnosis: Context dump — 250 lines and no specific question. Copilot summarises instead of acting, and "everything that's wrong" produces a wall of unranked, unactionable observations.
Corrected (one function, one concern):
Task: Review `check_permissions(user, resource)` for logic errors — any path that grants or denies access incorrectly.
Constraints: Focus only on the access-decision logic. Do not comment on naming, style, or other functions.
Output: A numbered list — each item: finding · severity · suggested fix.
Expected difference: a short ranked list of access-logic findings you can act on, instead of a page of generalities. Repeat per function and per concern as needed.
This function drops elements when the input lists have unequal length:
def merge_sorted(a, b):
result = []
i, j = 0, 0
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i])
i += 1
else:
result.append(b[j])
j += 1
while i < len(a) - 1:
result.append(a[i])
i += 1
while j < len(b) - 1:
result.append(b[j])
j += 1
return resultPrompt 1 (chat panel, Ask mode — paste the function with the prompt):
Task: Diagnose why `merge_sorted([1, 3], [2])` returns `[1, 2]` when the expected output is `[1, 2, 3]`.
Constraints: Diagnose only — do not propose a fix in this message.
Output: A numbered list of candidate causes ordered by likelihood, each with one supporting observation.
Expected output: identification of the off-by-one bound in the tail loops — while i < len(a) - 1 stops one element early, so the last element of whichever list was not exhausted is silently dropped.
Prompt 2 (same session, after confirming the diagnosis):
Context: The root cause is the `- 1` bound in both tail loops — they stop one element before the end of the list.
Task: Fix `merge_sorted` so the remaining tail of either list is appended in full.
Constraints: Change only the lines that address the root cause. Do not rename variables.
Output: The corrected function body only.
Expected output: the tail loops corrected to while i < len(a) and while j < len(b) (or an equivalent slice-based result.extend(a[i:])).
What to observe: the failing call with expected vs. actual output gives Copilot something concrete to reason against, and the confirmation gate between the prompts is where you verify the diagnosis before authorizing the change. Collapsing this into one prompt is the premature-fix anti-pattern.
Target function:
DISCOUNT_CODES = {
"SAVE10": 0.10,
"HALF": 0.50,
}
def apply_discount(price, code):
discount = DISCOUNT_CODES.get(code, 0.0)
return round(price * (1 - discount), 2)The prompt (chat panel, Ask mode):
Task: Write pytest tests for `apply_discount(price, code)`.
Constraints: Cover these cases: valid code "SAVE10", invalid code, price of zero, negative price. One test function per case. Use bare `assert` statements — no `unittest.TestCase`.
Output: Test file content only — no CLI instructions.
Expected output: a test file with four named test functions, one per listed case — for example apply_discount(100, "SAVE10") == 90.0 for the valid code and apply_discount(100, "BOGUS") == 100.0 for the unknown code.
What to observe: without the explicit case list, Copilot tests the happy path only. And the negative-price test forces a decision: the function currently applies the discount to negative prices without complaint. The test documents that behavior — whether it is a bug is your call, not Copilot's. A well-constrained testing prompt surfaces design questions a vague one hides.
For each task, decide Default or Frontier before reading the reference answers, and give a one-sentence reason.
| Task | Your answer |
|---|---|
Write a Google-style docstring for apply_discount |
|
| Debug a race condition in an async task queue | |
Generate a pytest suite for parse_config |
|
Perform an OWASP A02 security audit on hash_password |
|
Refactor calculate_tax to extract the rate lookup |
|
Migrate 15 call sites from requests to httpx |
Reference answers:
| Task | Answer | Reason |
|---|---|---|
Write a Google-style docstring for apply_discount |
Default | Language generation; no multi-step reasoning |
| Debug a race condition in an async task queue | Frontier | Concurrent causal reasoning requires depth |
Generate a pytest suite for parse_config |
Default | Mechanical coverage expansion |
Perform an OWASP A02 security audit on hash_password |
Frontier | Exploit-path reasoning justified |
Refactor calculate_tax to extract the rate lookup |
Default | Structural, not semantic |
Migrate 15 call sites from requests to httpx |
Auto or Frontier | Depends on call-site complexity |
What to observe: only two of the six tasks clearly justify a frontier model, and both involve multi-step causal reasoning. If your answers escalated more often than that, recalibrate: try the default first and escalate on demonstrated failure, not anticipated difficulty.
| Mistake | Root cause | Fix |
|---|---|---|
| Reusing one long chat for unrelated tasks | Assumes more history always helps | Start a new session per task and paste a 3-5 bullet summary if needed |
| Reviewing too much code at once | Equates larger scope with better quality | Review one function or one concern per prompt |
| Skipping the expected vs actual behavior in debugging prompts | Assumes the error message is enough | Always provide expected output, actual output, and one failing input |
| Escalating to frontier models before trying auto selection | Habit or fear of weak output | Try default first, then escalate only if reasoning quality is insufficient |
| Accepting first output without validation | Treats model output as final | Run tests or check acceptance criteria before adopting changes |
| Action in this module | Cost |
|---|---|
| Guided examples in Ask mode (default model, short turns) | Low |
| Two-prompt debugging chain | Low — two scoped turns typically cost less than one vague prompt plus its correction loop |
| Security review with a frontier model | Medium — escalate only for the scenarios the decision table justifies |
Invoking a .prompt.md file |
Same as typing the prompt by hand — the saving comes from fewer failed iterations, not from the mechanism |
The economics of this module are simple: every anti-pattern is also a token leak. A vague target, a context dump, or a missing output format buys you correction turns — and every turn is billed. A structured prompt that lands on the first attempt is the cheapest prompt you can send.
- GitHub Copilot documentation
- GitHub Copilot Chat in your IDE
- Prompt engineering for Copilot Chat (VS Code)
- GitHub Copilot model selection (VS Code)
- Prompt files in VS Code
Review the module summary, complete its Self-Check, then continue: