GitHub Copilot Best Practices for Beginners
Most “Copilot best practices” lists say things like be specific and review the output, which are true and useless. Nobody sets out to be vague.
This lesson demonstrates the principles instead. Each one comes with a weak version, a stronger version, and an explanation of what changed — because the gap between the two is where the skill actually lives.
Key takeaways
- Context beats phrasing. What Copilot can see matters more than how you word the request.
- Specify the contract — inputs, outputs, errors, and the test that should pass.
- Say what must not change. Models default to helpfulness, which shows up as unrequested edits.
- One capability per request. A change that does three things is three requests.
- Review is not the last step of the work. It is the part of the work that does not get delegated.
1. Context beats phrasing
Start here, because it reorders everything else.
Two developers ask for the same thing in the same words. One has a single file open and no repository instructions. The other has the interface definition open, a custom instructions file naming the language version and test framework, and the neighbouring module that already implements this pattern visible in the workspace.
They get materially different answers, and neither typed a better prompt.
The practical consequences:
- Open the files that matter before asking. The interface, the neighbour, the test.
- Attach context explicitly with
#file,#selectionand#projectrather than hoping the right thing is focused. - Write repository custom instructions once. This is the highest-return configuration in the product.
2. Specify the contract
The canonical demonstration.
Create a Python API.
Create a FastAPI REST endpoint that accepts a server hostname and returns CPU, memory, and disk utilisation as JSON.
Use Pydantic for request validation. Return HTTP 400 for invalid hostnames. Include pytest tests covering successful and failed requests.
Why the second is stronger — it makes five decisions the first leaves to chance:
| Decision | Weak version | Strong version |
|---|---|---|
| Framework | Guessed | FastAPI |
| What it returns | Guessed | CPU, memory, disk as JSON |
| Validation | Probably absent | Pydantic |
| Error behaviour | Guessed | HTTP 400 for invalid hostnames |
| Verification | None | pytest, success and failure cases |
Each unstated decision is one the model will make confidently and you will discover later. The strong prompt is not longer for its own sake — every clause removes a guess.
3. Constrain what must not change
Models default to helpfulness. In practice that means unrequested improvements: a renamed function, a “better” exception type, an extended feature you did not ask for.
Refactor this function.
Refactor this function to reduce duplication without changing externally observable behaviour.
Keep the public signature, the exception types, and the log messages exactly as they are. Explain what you extracted and why.
Why the second is stronger — it names the fixed points. That is what makes a refactor a refactor rather than a rewrite, and it gives you a short list to check in the diff.
The highest-value constraint of all, for anything involving tests:
Do not modify existing tests to make this change pass. If a test appears to be wrong, stop and tell me instead of changing it.
Without it, the most common agent failure mode is a green test suite that proves nothing, because the assertions were quietly adjusted until they matched.
4. Provide examples
When you want output in a particular shape, showing beats describing.
Write the error responses in our standard format.
Write the error responses following this existing example exactly, including field names and the nesting:
{ "error": { "code": "invalid_hostname", "message": "...", "field": "hostname" } }Use the same code naming convention: lowercase, underscore-separated, describing the problem rather than the HTTP status.
Why the second is stronger — “our standard format” means nothing outside your team. One concrete example conveys the schema, the naming convention and the nesting in less space than describing them would take.
5. Work in increments
A request that changes three things produces a diff that does three things, and those diffs do not get read carefully.
Add authentication, rate limiting, and request logging to this API.
Add rate limiting to the public endpoints in this router.
Use the existing Redis client in app/cache.py. Limit to 100 requests per
minute per API key. Return HTTP 429 with a Retry-After header. Do not change
authentication or logging.
Why the second is stronger — it produces a diff you can hold in your head. Then you do the next one. Three reviewable changes beat one unreviewable change, and the sequencing costs you nothing.
6. Ask for reasoning when you need to evaluate it
An answer you cannot check is an answer you have to trust, and trust is exactly what should not be extended here.
Optimise this function.
Optimise this function and explain each change: what it costs now, what it costs after, and which change accounts for most of the improvement.
If a change trades readability for speed, say so explicitly so I can decide whether it is worth it.
Why the second is stronger — it converts an opaque diff into a set of claims you can check. “This avoids re-sorting on every call” is testable. A rewritten function with no explanation is something you either accept or discard whole.
The same move works for debugging. Asking for a root cause plus the reasoning that reached it lets you spot when the reasoning is wrong even if the conclusion sounds plausible — which is the common case with subtle bugs.
7. Ask it to find problems, not just fix them
Generation gets the attention; critique is where the ratio of value to risk is best, because there is no diff to review.
Review this implementation and identify: inputs that would produce a wrong result rather than an error, behaviour that contradicts the docstring, and anything a reviewer would ask me to change.
Do not rewrite it. Just list the problems.
“Do not rewrite it” matters more than it looks. Without it, you get a new implementation and lose the diagnosis — and the diagnosis was the valuable part, because it tells you something about your own blind spots.
This is also the cheapest way to use Copilot on code you do not fully trust yourself to review, since a wrong critique costs you nothing but a moment’s thought, while a wrong rewrite costs you a debugging session.
8. Review as if a stranger wrote it
Because one did.
Generated code arrives with none of the context that normally accompanies a change — no author to ask, no reasoning you remember, no history. It arrives looking finished, which is precisely the problem.
A workable checklist:
- Read the diff, not the file. Reading changed files in place makes it far too easy to see what you expected rather than what changed.
- Check what you did not ask about. A change outside the stated scope is the one least likely to be reviewed, because it is not what you were looking for.
- Look at deletions. Diffs draw the eye to additions. Removed guard clauses and removed comments explaining why something odd was necessary are where regressions hide.
- Watch for silently widened behaviour. Broadened exception handling, a loosened type, removed validation, a default where a value was required. These make things pass without making them correct.
- Check every new import. Dependencies arrive quietly and are rarely questioned.
9. Test what you accept
Especially when it looks obviously correct — that is when scrutiny drops.
Three specific habits:
Run it. Not “it looks right”. Run it.
Verify generated tests actually test something. Revert the fix and confirm the test fails. A suite that passes both ways is not testing the change.
Check the assertions, not just the structure. A generated test with a wrong expected value passes against buggy code and gives you false confidence, which is worse than having no test at all.
10. Keep secrets out of context
A related habit: ask specifically where secrets could leak, because error paths are both where it happens and the least-reviewed code in most projects.
Identify anywhere in this code where a credential, token or personal data could end up in a log, an error message, an exception trace, or a response body.
11. Review suggested dependencies
Generated code adds imports, and imports become dependencies.
List every dependency this change adds or updates. For each one, tell me what it is for and whether the same result could be achieved with the standard library.
Copilot cannot check maintenance status, licence, or known advisories — use a real scanner for that. What it can do is surface the additions so you decide consciously rather than by omission.
12. Write repository custom instructions
The single configuration with the best return, and the one most teams never do.
# Project conventions
Python 3.12. Run tests with `pytest -q` from the repository root.
## Style
- Type hints on every public function.
- Docstrings state the contract: parameters, return value, exceptions raised.
- Prefer the standard library. Do not add a dependency without a stated reason.
## Errors
- Raise specific exceptions, never bare `except:`.
- Error messages name the offending value.
- Never return sentinel values such as -1 or None for failure.
## Structure
- HTTP handlers in `app/api/`, business logic in `app/services/`.
- Handlers must not contain business logic.
## Tests
- pytest, mirroring the module layout under `tests/`.
- Use parametrize for input tables rather than repeated near-identical tests.
- Do not modify existing tests to make a change pass. Raise the conflict instead.Check it into the repository and the whole team inherits it, including every agent session.
13. Constrain agent permissions deliberately
Agentic surfaces can run commands. The approval prompts are the control point, not friction.
Sensible defaults:
- Approve freely: tests, linters, type checkers, builds,
git status,git diff. - Read carefully: anything installing packages, anything writing outside the workspace.
- Decline or run yourself: anything destructive, anything touching production, anything involving credentials.
Copilot CLI exposes explicit permission modes (/permissions) and OS-level
sandboxing (/sandbox) precisely so that “allow everything” is a deliberate
choice rather than an accidental one. Use allow-all in a disposable container
if you like. Do not use it in a working tree you care about.
14. Keep humans in the loop where it counts
Not everywhere — that would defeat the purpose. Specifically:
- Anything irreversible. Migrations against real data, infrastructure applied to live environments, force pushes.
- Anything crossing a trust boundary. Authentication, authorisation, input validation, deserialisation.
- Anything where being subtly wrong is expensive. Concurrency, numerical precision, money.
These are the areas where generated code is most convincing and least reliable at the same time, which is the worst possible combination.
15. Avoid overreliance
The one that is easy to nod at and hard to act on.
Reviewing generated code requires the same skill as writing it. If that skill atrophies, you lose the ability to catch the errors — and you lose it gradually, without noticing, because everything appears to be working.
Two habits worth keeping:
Stay able to do it yourself. Occasionally write the thing by hand, particularly in areas you rely on Copilot for most. If that feels difficult, it is information.
Do not use it while learning something new. The struggle is where understanding forms. Skipping it produces someone who can ship code they cannot debug — which works right up until the moment it matters.
What good looks like after a month
Principles are easier to hold when you know what they add up to. A developer using Copilot well a month in looks like this, and it is a less dramatic picture than the marketing suggests.
They have a repository instructions file and it has been edited more than once, because it grew out of corrections they got tired of repeating.
They reach for chat to understand things, not only to produce things. The ratio of “explain this” to “write this” is higher than they expected when they started.
They can say which layer they are in. Completion for transcription, chat for constraints and comprehension, agent mode for supervised multi-file work, the cloud agent for delegated work. They do not use agent mode for a three-line change or completion for an architectural decision.
Their prompts are boring and specific. No incantations, no role-play preambles, no “you are an expert engineer”. Inputs, outputs, constraints, definition of done.
They still read every diff, and they are faster at it than they used to be, because reviewing generated code is itself a skill that improves with practice.
They know what it costs. Roughly, in credits, and specifically which activities are expensive.
What is absent from that picture is as informative as what is in it. There is no prompt library of magic phrases, no model preference held with conviction, and no claim that any of it doubled their output. The gains are real and unevenly distributed: large on transcription, negligible on the parts of the job that were always about judgement.
Common mistakes
The failures worth naming, because each one is common and each one is fixable.
Blaming the model for a context problem. Poor results are far more often about what Copilot could see than which model produced them. Check the context before switching models or upgrading a plan.
Accepting long suggestions with a short glance. Scrutiny should scale with suggestion length, and for most people it does the opposite.
Treating a green test suite as verification without checking the suite would have failed. This is the single most consequential mistake on the list, because it looks exactly like success.
Approving every terminal command to move faster. The prompts are the boundary. Removing the boundary is a decision, not a shortcut.
Using it to skip learning. The productive struggle is where understanding forms, and understanding is what review requires.
Writing elaborate prompts instead of opening a file. Thirty seconds of context beats three minutes of phrasing, reliably.
Putting it together
The full loop, which is the same at every level of the product:
- Establish context. Open the files. Write the instructions once.
- Specify the contract. Inputs, outputs, errors, definition of done.
- State the constraints. What must not change.
- Keep it small. One capability per request.
- Read the diff. Not the file — the diff.
- Run it. Verify the tests fail without the change.
- Check the boundaries. Secrets, dependencies, permissions, trust boundaries.
Steps 5 through 7 are the ones that do not get delegated. They are also the ones under time pressure, which is exactly why they need to be habits rather than intentions.
Where to go next
You have finished Cluster 1 — GitHub Copilot Fundamentals. From here:
- Revisit the complete guide now that the pieces have context.
- Apply these principles with 50 use cases.
- Check the Academy roadmap for what comes next: IDE workflows, language-specific technique, DevOps, the CLI, prompting and models, agents and MCP, and enterprise security.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.
Was this lesson helpful?
Your answer is stored in this browser and is not sent anywhere.