GitHub Copilot for Programming: Language Guide
The first two clusters of this Academy were about the product. What Copilot is, what each surface does, and how the same features behave differently in VS Code, Visual Studio, JetBrains, Eclipse and Xcode. This cluster is about something else entirely: the language you are actually writing in, and what it does to the value of a suggestion.
That is not a small difference. Copilot’s output is text either way. What changes between languages is how much of that text you can verify without reading it carefully, and how expensive it is when you fail to.
Key takeaways
- Copilot’s suggestion quality varies by language, and GitHub says so explicitly — it depends on “the volume and diversity of training data for that language.” Nobody publishes a per-language accuracy score, and any site that shows you one made it up.
- The far more useful question is not how good is Copilot in language X but what does language X let me check automatically. Rust’s borrow checker and TypeScript’s compiler reject whole classes of wrong suggestion before a human reads them. Python, JavaScript, Ruby and shell do not.
- Types are context in both directions. A typed signature makes the suggestion better going in, and makes a wrong suggestion fail loudly coming out.
- Every language in this cluster has a fastest honest check — one command that tells you something true about generated code in seconds. Learn yours and run it before you read the diff, not after you merge it.
- The two languages with no type system at all, shell and SQL, are the two where a single bad suggestion can do irreversible damage. They are the last two lessons in this cluster for that reason.
What GitHub actually documents about language support
Start with the primary sources, because the folklore here is worse than usual.
GitHub’s language-support reference lists a set of core languages — C, C++, C#, Go, Java, JavaScript, Kotlin, PHP, Python, Ruby, Rust, Scala, Swift and TypeScript — and marks Copilot as supported for every one of them. Alongside that table it states the caveat plainly:
The language support for GitHub Copilot varies depending on the volume and diversity of training data for that language.
The Copilot product FAQ goes further and is worth quoting exactly, because these four sentences are the entire published basis for any claim about relative language quality:
GitHub Copilot is trained on all languages that appear in public repositories.
JavaScript is well-represented in public repositories and is one of GitHub Copilot’s best supported languages.
Languages with less representation in public repositories may produce fewer or less robust suggestions.
Given public sources are predominantly in English, GitHub Copilot will likely work less well in scenarios where natural language prompts provided by the developer are not in English.
The responsible-use documentation adds the limitation that matters most for this cluster. Copilot’s suggestions are drawn from a large body of code but have, in GitHub’s words, “a limited scope,” and the model “may generate code that appears to be valid but may not actually be semantically or syntactically correct or may not accurately reflect the intent of the developer.”
Read that sentence again with a specific language in mind. In Rust, code that is not semantically correct usually does not compile. In Bash, code that is not semantically correct runs perfectly and deletes the wrong directory.
The real variable: what your language lets you verify
Here is the reframing this whole cluster is built on.
Every suggestion Copilot makes falls into one of three buckets:
- Wrong in a way a tool catches instantly. A type error, an undefined symbol, a borrow-checker violation, a syntax error. Cost: seconds.
- Wrong in a way a test catches. An off-by-one, a mishandled empty input, an inverted condition. Cost: minutes, if the test exists.
- Wrong in a way only a human catches. A plausible but incorrect business rule, an insecure default, an API that does not exist in your installed version, a query that returns confident wrong numbers. Cost: unbounded.
Languages differ enormously in how much they move from bucket three into bucket one. That movement — not “quality” — is what you should optimise for.
| Language | Type system | Primary package tool | Common tests | Typical Copilot uses | Key review risk |
|---|---|---|---|---|---|
| PythonLesson 27 | Dynamic, with optional static typing | pip (PyPI); uv and Poetry are common front-ends | pytest, unittest | HTTP endpoints, data transforms, automation scripts, pytest suites, docstrings | Silent type mismatches — nothing fails until the wrong value reaches production |
| JavaScriptLesson 28 | Dynamic | npm (also pnpm, yarn) | node:test, Vitest, Jest | Express routes, DOM handlers, async data fetching, test scaffolding, build config | Browser and Node APIs mixed in one file — the suggestion runs in the wrong runtime |
| TypeScriptLesson 29 | Gradual | npm (also pnpm, yarn) | Vitest, node:test, Jest | Typed API clients, interface and generic definitions, discriminated unions, typed tests | Assertions and `any` used to silence the compiler instead of fixing the type |
| JavaLesson 30 | Static | Maven or Gradle | JUnit 5, AssertJ, Mockito | DTOs and records, Spring controllers, stream pipelines, JUnit tests, builder boilerplate | A dependency coordinate that does not exist, or a version that predates the API being called |
| C#Lesson 31 | Static, memory-safe | NuGet via the dotnet CLI | xUnit, NUnit, MSTest | Minimal API endpoints, LINQ queries, DI registration, records and DTOs, xUnit tests | Async code that blocks, or LINQ that silently runs in memory instead of in the database |
| C++Lesson 32 | Static, manual memory management | vcpkg or Conan; often none | GoogleTest, Catch2, doctest | Container manipulation, algorithm calls, CMake targets, test fixtures, RAII wrappers | Ownership and lifetime — code that compiles cleanly and is still undefined behaviour |
| GoLesson 33 | Static, memory-safe | Go modules | the standard testing package | HTTP handlers, table-driven tests, struct definitions, context plumbing, CLI flags | Discarded errors, and goroutines with no cancellation path |
| RustLesson 34 | Static, ownership-based | Cargo (crates.io) | the built-in test harness, proptest | Enum and trait definitions, match arms, iterator chains, Serde derives, unit tests | Fighting the borrow checker with clone() and unwrap() rather than fixing the design |
| PHPLesson 35 | Dynamic, with optional static typing | Composer (Packagist) | PHPUnit, Pest | Controllers, Eloquent and Doctrine queries, form validation, PHPUnit tests, migrations | Two decades of insecure PHP in the training data: string-built SQL and unescaped output |
| RubyLesson 36 | Dynamic | Bundler (RubyGems) | RSpec, Minitest | Enumerable chains, service objects, RSpec specs, Rake tasks, Rails scaffolding | Generated metaprogramming, and Rails parameter handling that trusts user input |
| BashLesson 37 | Untyped shell | None — the system package manager is the dependency | bats-core | Health checks, CI steps, log processing, backup and deployment scripts | A destructive command run against the wrong path, and unquoted expansions |
| SQLLesson 38 | Declarative, schema-typed | None — the database engine is the runtime | pgTAP, dbt tests, application-level assertions | Analytical SELECTs, joins, CTEs, window functions, migrations, query explanation | A query that returns plausible numbers that are wrong — a fan-out join or a mishandled NULL |
The twelve lessons in this cluster
Python
APIs, data work, ML, automation
- Typing
- Dynamic, with optional static typing
- Package ecosystem
- pip (PyPI); uv and Poetry are common front-ends
- Testing
- pytest, unittest
- Typical Copilot work
- HTTP endpoints, data transforms, automation scripts, pytest suites, docstrings
JavaScript
Web front ends and Node services
- Typing
- Dynamic
- Package ecosystem
- npm (also pnpm, yarn)
- Testing
- node:test, Vitest, Jest
- Typical Copilot work
- Express routes, DOM handlers, async data fetching, test scaffolding, build config
TypeScript
Type-safe web and Node code
- Typing
- Gradual
- Package ecosystem
- npm (also pnpm, yarn)
- Testing
- Vitest, node:test, Jest
- Typical Copilot work
- Typed API clients, interface and generic definitions, discriminated unions, typed tests
Java
JVM services and enterprise systems
- Typing
- Static
- Package ecosystem
- Maven or Gradle
- Testing
- JUnit 5, AssertJ, Mockito
- Typical Copilot work
- DTOs and records, Spring controllers, stream pipelines, JUnit tests, builder boilerplate
C#
.NET services and desktop apps
- Typing
- Static, memory-safe
- Package ecosystem
- NuGet via the dotnet CLI
- Testing
- xUnit, NUnit, MSTest
- Typical Copilot work
- Minimal API endpoints, LINQ queries, DI registration, records and DTOs, xUnit tests
C++
Systems, engines, performance-critical code
- Typing
- Static, manual memory management
- Package ecosystem
- vcpkg or Conan; often none
- Testing
- GoogleTest, Catch2, doctest
- Typical Copilot work
- Container manipulation, algorithm calls, CMake targets, test fixtures, RAII wrappers
Go
Cloud services, CLIs, infrastructure
- Typing
- Static, memory-safe
- Package ecosystem
- Go modules
- Testing
- the standard testing package
- Typical Copilot work
- HTTP handlers, table-driven tests, struct definitions, context plumbing, CLI flags
Rust
Systems, CLIs, WebAssembly, services
- Typing
- Static, ownership-based
- Package ecosystem
- Cargo (crates.io)
- Testing
- the built-in test harness, proptest
- Typical Copilot work
- Enum and trait definitions, match arms, iterator chains, Serde derives, unit tests
PHP
Web applications and APIs
- Typing
- Dynamic, with optional static typing
- Package ecosystem
- Composer (Packagist)
- Testing
- PHPUnit, Pest
- Typical Copilot work
- Controllers, Eloquent and Doctrine queries, form validation, PHPUnit tests, migrations
Ruby
Rails web apps, scripting, tooling
- Typing
- Dynamic
- Package ecosystem
- Bundler (RubyGems)
- Testing
- RSpec, Minitest
- Typical Copilot work
- Enumerable chains, service objects, RSpec specs, Rake tasks, Rails scaffolding
Bash
Automation, CI/CD, system administration
- Typing
- Untyped shell
- Package ecosystem
- None — the system package manager is the dependency
- Testing
- bats-core
- Typical Copilot work
- Health checks, CI steps, log processing, backup and deployment scripts
SQL
Analytics, reporting, application data access
- Typing
- Declarative, schema-typed
- Package ecosystem
- None — the database engine is the runtime
- Testing
- pgTAP, dbt tests, application-level assertions
- Typical Copilot work
- Analytical SELECTs, joins, CTEs, window functions, migrations, query explanation
Copilot with dynamically typed languages
Python, JavaScript, PHP and Ruby share a property that changes how you should work: the signature tells the model almost nothing.
A Python function that takes data and returns something could be doing
anything at all. Copilot will guess — plausibly, fluently, and sometimes wrongly
— and nothing in the toolchain will contradict it until the wrong value reaches
production. The same applies to a JavaScript function taking opts, a PHP method
taking $input, or a Ruby method taking params.
The mitigation is the same in all four, and it is not “prompt better”:
- Give the model the types the language does not require. Python type hints, JSDoc annotations, PHP parameter and return types, Sorbet or RBS signatures. These improve the suggestion going in and give a static analyser something to check coming out.
- Turn on the analyser that reads them. Type hints with no
mypyrun are documentation.mypy, Pyright,tsc --checkJs, PHPStan and Sorbet are what convert them into a gate. - Write the test first when the shape is subtle. In a dynamic language a failing test is often the only mechanical signal you will get.
Create a Python 3 function that accepts a list of latency measurements in milliseconds and returns p50, p95 and p99 as a dataclass.
Use type hints on every parameter and the return value.
Reject an empty list with ValueError; the message must name the parameter.
Use nearest-rank percentiles, not interpolation, and say so in the docstring.
Include pytest tests for a normal case, a single-element list, an unsorted input, and the empty-list error.
Notice what that prompt does. It does not say “write good code.” It states the return shape, the error behaviour, the algorithm (because “percentile” is ambiguous and the model will pick one silently), and the test cases. Every one of those is a constraint the language would not have enforced by itself.
Copilot with statically typed languages
TypeScript, Java, C#, C++, Go and Rust give you something the dynamic languages cannot: a compiler that has an opinion about the suggestion.
This cuts both ways, and the second way is the one people miss.
The obvious benefit is that a wrong type is rejected. If Copilot invents a method
on your interface, tsc, javac, go build or cargo check says so before you
read the code. That is bucket three collapsing into bucket one, which is exactly
what you want.
The less obvious cost is that a model under pressure from a type checker will route around it. These are the specific patterns to watch for, and each has its own lesson in this cluster:
- TypeScript:
asassertions andanyinserted to silence an error rather than to fix a type. - Java: raw types or an unchecked cast where a generic bound is the real answer.
- C#:
!null-forgiving operators sprinkled to quiet nullable warnings. - C++: a
reinterpret_castor a raw pointer where ownership was the question. - Go:
interface{}/anywhere a concrete type would do. - Rust:
clone(),unwrap(), and in the worst case anunsafeblock.
Every one of those compiles. Every one of those is the model telling you it did not understand the constraint, in a form that looks like it did.
The other thing static typing changes is how you supply context. In a typed codebase, the type definitions are the best prompt you have. Opening the interface file, the record, the trait or the struct in an adjacent tab does more for suggestion quality than any amount of natural-language description, because it gives the model the exact names and shapes rather than your paraphrase of them.
Copilot for scripting and automation
Bash is its own category, and not because it is simple.
Shell has no type system, no compiler, no package manifest and no meaningful runtime error handling by default. It also has the shortest distance between “a plausible-looking suggestion” and “an irreversible action against production.” A generated Python function that is wrong throws an exception. A generated shell line that is wrong removes a directory tree.
Everything the other languages get from tooling, shell has to get from process:
- Read every command in the suggestionHuman judgementEspecially rm, mv, dd, chmod, chown, sudo, and anything piping a remote URL into a shell.
- Run ShellCheckIt finds unquoted expansions, useless subshells and unassigned variables mechanically.
- Check quoting on every expansionHuman judgementAn unquoted $VAR with a space in it becomes two arguments. That is how a script deletes the wrong thing.
- Dry-run where the tool supports itrsync --dry-run, terraform plan, kubectl --dry-run=client, apt --simulate.
- Execute in a non-production environment firstA container, a VM, or a scratch directory you can afford to lose.
- Only then run it for realHuman judgement
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
That is not paranoia; it is the compensating control for a language that has no compiler. The full treatment is in GitHub Copilot for Bash and Shell Scripts.
Copilot for data and queries
SQL is different again, and the difference is subtle enough that it catches experienced engineers.
A generated SQL query almost always runs. It usually returns rows. The rows
usually look reasonable. And it is entirely possible for all of that to be true
while the numbers are wrong — because a join fanned out, because a NULL silently
excluded rows from a comparison, or because refunded orders were never filtered
out.
Here is a real measurement from the worked example in GitHub Copilot for SQL, run against a small seeded SQLite database. Two queries answering “the top customers by order value in the last 30 days” — one written the obvious way, one written correctly:
First-draft query Corrected query
Grace Example 1280 Grace Example 420
Linus Example 510 (absent — all of Linus's orders were refunded)
Alan Example 390 Alan Example 390Neither query errored. Neither returned an implausible number. The first one was simply wrong, by a factor of three for the top customer, and it would have gone into a report.
The other thing SQL has that no other language in this cluster does: there is
no single SQL. LIMIT is PostgreSQL, MySQL and SQLite; SQL Server wants
TOP or OFFSET … FETCH. Date arithmetic differs in all four. Copilot will
happily produce a dialect you are not running, and the error message will be a
syntax error a hundred characters into a query you did not read.
How a suggestion gets built, and why that is language-specific
It helps to have a concrete model of where a suggestion comes from, because the levers you have are all in that pipeline.
When you pause in an editor, Copilot assembles a prompt from the code around your cursor, the file you are in, other files open in your editor, and — depending on the surface and the IDE — your repository’s custom instructions and whatever you have explicitly attached in chat. That bundle goes to a model that has read a very large amount of public code. What comes back is the most probable continuation given that bundle.
Two consequences follow, and both are language-shaped.
Consequence one: the model’s prior is whatever is common in public code for
your language. For a language with an overwhelming dominant style — Go, where
gofmt and idiomatic error handling are near-universal — the prior is close to
what you want. For a language with decades of divergent style in public
repositories — PHP, where the same task has been written five different ways
across five major-version eras — the prior is a blend of good and long-obsolete
practice. That is not a quality judgement about the language. It is a fact about
its corpus, and it changes what you check.
Consequence two: your context has to out-argue that prior. This is why opening the right files matters so much more than phrasing the prompt well. If your repository uses a service-layer pattern and the model’s prior is fat controllers, one open example of your service layer moves the suggestion further than three sentences describing it.
The practical version, which every lesson in this cluster repeats in its own terms:
- Open the file that defines the vocabulary — the type, the interface, the schema, the existing handler.
- Open the manifest, so version-specific APIs are anchored.
- Open one test, so the model knows what your assertions look like.
- Then write the prompt.
What Copilot is reliably good at, in every language
It is easy to write a cluster like this and leave the impression that generated code is mostly a liability. It is not, and being vague about where it earns its keep would be its own kind of dishonesty.
These tasks are consistently high-value across all twelve languages:
Mechanical translation of a shape you have already decided. Turning a JSON example into a typed structure, a struct into a builder, a schema into a DTO, an interface into a stub implementation. You have made the decisions; the model is doing the typing.
Test scaffolding from a described behaviour. Not “write tests for this function” — that reproduces the function’s bugs — but “write tests for a function that must do X, reject Y, and handle the empty case.” Copilot is good at enumerating cases you would have written eventually.
Explaining unfamiliar code. Reading is a much easier task than writing, and the failure mode is far more visible. Pasting an unfamiliar regex, a dense iterator chain or a legacy stored procedure into chat and asking what it does is one of the highest-value things Copilot offers, in every language.
Interpreting an error. A compiler error, a stack trace, a linker failure, a failing test’s diff. The error text is precise, unambiguous context — exactly what the model normally lacks.
Repetitive edits with an established pattern. The fifth handler that looks like the first four. The next table-driven test case. The migration that mirrors the last one.
First drafts of documentation and comments — with the caveat that a generated docstring describes what the code appears to do, which is only useful if the code is right.
What Copilot is reliably bad at, in every language
Symmetrically, these are the tasks where the failure rate is high enough that you should plan for it rather than be surprised by it.
Anything that depends on a fact it cannot see. Your database schema, your
internal service’s actual response shape, the version of the library you
installed, the semantics of a column named status. It will guess, and the guess
will be well-formed.
Numerical and boundary correctness. Percentile definitions, rounding modes, inclusive versus exclusive ranges, timezone handling, monetary arithmetic in floating point. The code will look right.
Security-relevant defaults. Left to itself, generated code tends toward the permissive option: CORS open, TLS verification off, a debug flag on, a secret in a constant. Not maliciously — permissive code is what makes examples work, and examples are heavily represented in public repositories.
Concurrency. Every language in this cluster with a concurrency story has a section on this, because plausible-looking concurrent code is the single hardest category to review by reading.
Large architectural decisions. Copilot’s documented limitation is a limited scope; it works from the code near your cursor. It is not in a position to know that the thing you asked for should not exist.
Anything where “it compiles and the tests pass” is not sufficient. Which is most of the interesting work.
Frameworks are effectively a second language
Worth naming explicitly, because it explains a lot of confusing behaviour.
When you ask for “a Python API endpoint,” the model has to pick a framework, and
frameworks change the answer far more than the language does. FastAPI, Django
and Flask have three different notions of what an endpoint is. Express 4 and
Express 5 differ on whether a rejected promise reaches your error handler.
Spring Boot 2 and 3 differ on the entire javax to jakarta namespace. ASP.NET
Core minimal APIs and MVC controllers are not interchangeable.
Copilot will produce whichever is most probable given your context. If your context is thin, “most probable” means “most common in public code,” which skews toward whatever was dominant when the corpus was written — not necessarily what you are running.
Three habits fix nearly all of this:
- Name the framework and major version in the prompt, every time, until it is in your repository instructions.
- Keep the manifest visible.
requirements.txt,package.json,pom.xmland their siblings are the cheapest possible disambiguation. - Show one existing example from your codebase. A single real handler settles a dozen ambiguities at once.
Framework-specific technique is deliberately out of scope for these twelve lessons — they are language pillars, and each is written so that framework guides can hang off it later without the language page having to be rewritten.
Public code, licensing and duplicate suggestions
GitHub documents that, while the probability is low, Copilot “may generate code suggestions that match code in the training set.” This is a policy and process question rather than a language question, but it interacts with language in one practical way: the more idiomatic and constrained a language is, the more likely two correct implementations look identical, and the less meaningful a match is.
Two things worth knowing:
- Copilot offers a duplicate-detection setting that can block suggestions matching public code. Whether it is on is an organisational decision, and on Business and Enterprise plans an administrator may have set it for you.
- A match is not automatically a licence problem, and the absence of a match is not a guarantee of one. If your organisation has a policy here, it belongs in your review checklist alongside the technical checks — not instead of them.
The security and governance side of this is Cluster 8 material rather than Cluster 3 material. What belongs here is the habit: when a suggestion arrives that is substantially larger and more complete than what you asked for, that is worth a second look for several reasons, of which provenance is only one.
A five-minute review method that works in any language
Every lesson in this cluster ends with language-specific review guidance. This is the language-independent skeleton they all instantiate.
- Run the fastest honest checkThe compiler, the type checker, the linter — whichever your language gives you. Seconds, and it eliminates a whole category.
- Read every import and dependencyHuman judgementDoes it exist? Is it in the lock file? Is the API in the version you have installed?
- Find the escape hatchesHuman judgementany, unwrap, unsafe, raw casts, null-forgiving operators, @ts-ignore, # type: ignore. Each one is a place the model gave up.
- Check the boundariesHuman judgementEmpty input, single element, maximum size, null, zero, negative, and the timezone.
- Check the error pathHuman judgementWhat happens when the call fails? Is the failure handled, logged, or silently swallowed?
- Ask what a hostile input would doHuman judgementEspecially for anything touching a shell, a query, a filesystem path or rendered output.
- Run the tests — and check the tests are not asserting the bug
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
Steps two through six are the ones that do not automate, and they are the reason this cluster exists. Tooling handles the mechanical half; the judgement half is still yours, and it is language-shaped.
Context engineering, by language
GitHub’s own best-practice guidance for Copilot is essentially about context, and the language changes what “good context” means. What follows is the practical version.
Open the files that define your vocabulary. Copilot draws on the files open in your editor. In a typed language that means the interface, the record, the trait, the struct. In a dynamic language it means the test file and the module that already does something similar.
Put the manifest in reach. package.json, pyproject.toml, go.mod,
Cargo.toml, composer.json, pom.xml, .csproj, the Gemfile. This is the
single highest-value context file in most projects, because it is the difference
between a suggestion that uses your installed library versions and one that uses
whatever was most common in the training data.
State the version when it matters. “Express 5, not Express 4.” “Spring Boot 3.” ”.NET 8 minimal APIs.” “PostgreSQL 16.” Version-specific behaviour is where plausible-looking code most reliably fails.
Show an example of the convention you want followed. One existing handler, one existing test, one existing migration. This is worth more than a paragraph describing the convention.
Use repository-level custom instructions for the rules that never change — the error-handling pattern, the logging library, the banned APIs, the required test framework. Repeating them in every prompt is how they get dropped.
Comments to code, tests, debugging, refactoring
These four workflows appear in every language lesson in this cluster, and the mechanics are genuinely different in each. The shared principles are short:
Comments to code works best when the comment states behaviour and constraints, not intent. “Parse the config” gets you a guess. “Parse a TOML config; unknown keys are an error; missing optional keys take the documented default” gets you something you can check.
Test generation is the highest-value Copilot task in every language in this cluster, with one crucial caveat: tests generated from the implementation encode the implementation’s bugs. A test written by reading the code will assert whatever the code currently does. Describe the required behaviour to Copilot instead, and let the test disagree with the implementation.
Debugging is where Copilot is most useful and least discussed. Pasting a stack trace, a compiler error or a failing assertion into chat and asking for an explanation is reliably good — the error text is precise context, which is exactly what the model is short of the rest of the time.
Refactoring is safe in proportion to your test coverage and your type system. A large mechanical refactor in Rust or TypeScript is a compiler-verified operation. The same refactor in Ruby or Python is a leap of faith unless the tests are real.
Package management, and the dependency that does not exist
Every language lesson in this cluster covers this, because it is the most common serious failure mode of AI-assisted coding and it is language-agnostic in principle and language-specific in practice.
Copilot may suggest a package, module, function, method or version that does not exist, or that exists but not in the version you have installed. It will do so fluently, with a plausible name, correct-looking import syntax and an API shape that matches what such a library would offer if it existed.
The mitigations, in order of how much they cost you:
| Check | Cost | Catches |
|---|---|---|
| Does the import resolve? | Free — the compiler or interpreter tells you | Non-existent packages, in compiled languages |
| Is it in the lock file? | Seconds | Anything added without going through the package manager |
| Does the registry have it? | Seconds | Fabricated names |
| Is the API in this version’s docs? | A minute | Version drift, the most common case |
| Who publishes it, and since when? | A few minutes | Typosquats and abandoned packages |
Static analysis, formatting and linting as the review layer
The most useful mental model for this cluster: your linter and type checker are the first reviewer of Copilot’s output, and they work for free.
Set them up so they run before you read the diff, not after you open the pull request. Concretely, per language, the fastest honest check is:
| Language | Command |
|---|---|
| Python | mypy . && pytest -q |
| JavaScript | node --test && npx eslint . |
| TypeScript | tsc --noEmit |
| Java | mvn -q verify |
| C# | dotnet build -warnaserror && dotnet test |
| C++ | -Wall -Wextra -fsanitize=address,undefined, then run the tests |
| Go | go vet ./... && go test -race ./... |
| Rust | cargo clippy -- -D warnings && cargo test |
| PHP | vendor/bin/phpstan analyse && vendor/bin/phpunit |
| Ruby | bundle exec rubocop && bundle exec rspec |
| Bash | shellcheck script.sh && bash -n script.sh |
| SQL | EXPLAIN the query, then run it inside a transaction you roll back |
None of these replaces reading the code. All of them are faster than reading the code, and each one eliminates a category of problem so you can spend your attention on the category that is left.
Prompt quality: what actually makes the difference
Prompting advice is usually generic enough to be useless. Here is the specific version, with the reasoning attached.
A weak prompt:
Write some Python to calculate percentiles.
A strong one:
Create a Python 3 function that accepts a list of latency measurements and returns p50, p95 and p99.
Use type hints.
Reject an empty list with ValueError.
Include pytest tests for normal, boundary, and invalid input.
The second is better for five specific reasons, and it is worth separating them because each one generalises to every language in this cluster.
It names the language and version. “Python 3” removes a fork in the road that still exists in public code. The equivalents elsewhere: “Express 5”, “Java 21”, ”.NET 8”, “C++20”, “PostgreSQL”, “Bash, not POSIX sh”.
It states the return shape. “p50, p95 and p99” tells the model there are three values, not one and not a list. Without it you get a plausible guess, and the guess is not wrong so much as different from what you needed, which is worse — it will pass a cursory read.
It specifies the error behaviour. “Reject an empty list with ValueError” is
the difference between code that raises IndexError from deep inside a sort and
code that fails at the boundary with a message. Generated code omits error
handling by default because the shortest correct-looking implementation omits it.
It requires the type annotations. In a dynamic language this is the lever
that turns a suggestion into something a static analyser can check. It costs six
words and converts a whole class of runtime bug into a mypy error.
It enumerates the test cases by category. “Normal, boundary, and invalid” gets you three groups. “Write tests” gets you one happy path. Naming the categories is the cheapest way to get coverage of the cases you are least likely to write yourself.
What the strong prompt still leaves ambiguous, and what a really good prompt would pin down: which percentile definition. Nearest-rank and linear interpolation give different answers on small inputs, both are standard, and the model will pick one silently. That is the general lesson — after you have removed the obvious ambiguity, the remaining ambiguity is where the subtle bugs live.
Is this actually working for you?
Worth a short section, because “am I faster?” is a bad question and there are better ones.
The useful signals are not about volume of accepted suggestions. They are:
- How often does a suggestion survive your fastest honest check unmodified? If it is rarely, your context is thin — open more of the right files before concluding the model is bad at your language.
- How often do you find yourself fixing the same category of thing? Missing error handling, wrong library version, a convention your team abandoned. That is a repository-instructions problem, not a prompting problem.
- How often does a generated test fail against generated code? If the answer is never, check whether the tests were written from the implementation rather than from the requirement. Tests that always pass are not tests.
- Is your review time going up? Generating more code than you can review is not a productivity gain; it is deferred work with interest.
Common risks across every language
The list below is the union of what the twelve lessons cover. Each item links to the language where it does the most damage.
- Non-existent APIs and packages — every language; worst in Java and PHP, where a wrong coordinate or Composer package is easy to add and hard to notice.
- Deprecated or superseded APIs — the training data contains years of code written against older versions. Common in PHP, Java and C++.
- Insecure dependencies — see the supply-chain sections of JavaScript and Ruby.
- Poor or absent error handling — endemic; the specific failure differs.
Go discards errors,
Python omits
exceptblocks, Rust reaches forunwrap(). - Missing boundary checks — C++ most dangerously, but empty-collection handling is a universal gap.
- Unsafe input handling — PHP, JavaScript and Ruby carry the most historical baggage here.
- Incorrect concurrency — Go goroutine leaks, Java shared mutable state, C# blocking async, C++ data races.
- Memory-safety errors — C++, and the
unsafeblocks in Rust. - Shell injection — Bash directly, and every language’s subprocess API.
- SQL injection — SQL and PHP.
- Insecure defaults — permissive CORS, disabled certificate verification, debug mode left on. Seen across Python and JavaScript.
- Incorrect version assumptions — every language, and the reason the manifest belongs in your context.
Why there are no ratings in this cluster
It is worth being explicit, because ranked lists of “best Copilot languages” are everywhere and they are fabricated.
There is no published benchmark from GitHub measuring suggestion accuracy per language. There is a documented statement that quality varies with training-data volume and diversity, and a single named example — JavaScript. Everything beyond that is somebody’s impression, and impressions about a model that changes monthly are not durable enough to number.
What is stable and worth knowing is structural: a language with a compiler rejects more wrong suggestions than one without, a language with a strong standard test tool makes verification cheaper, and a language with a widely used static analyser catches a category of mistake before review. Those properties do not change with the model, which is why this cluster is organised around them.
Where to go next
If you write one language, read the pillar you just finished and then that language’s lesson. If you write several, the pairs that share the most useful context are:
- JavaScript then TypeScript — the second is written to cover only what types change, not to repeat the first.
- C++ then Rust — the same problem domain with opposite verification stories.
- Python then Bash then SQL — the automation and data path, and the natural on-ramp to Cluster 4 on DevOps and infrastructure.
If you have not yet read GitHub Copilot Best Practices from Cluster 1 or GitHub Copilot for IDEs from Cluster 2, those two supply the review discipline and the editor mechanics this cluster assumes. The Copilot Chat and code completion lessons cover the two surfaces every example in this cluster uses.
After Cluster 3, the curriculum moves to Copilot for DevOps and infrastructure — Dockerfiles, Kubernetes manifests, Terraform, Ansible and CI pipelines. Three lessons here are the direct on-ramp to it: Bash for the review discipline infrastructure scripting demands, Python for automation and tooling, and Go because most of the infrastructure you will be automating is written in it.
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.