GitHub Copilot for Programming: Language Guide

GitHub Copilot Programming LanguagesAcademy lesson 26Cluster 3 · Lesson 1 of 13Beginner → Intermediate26 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot for Programming: Language GuideGitHub Copilot Programming Languages1Beginner → Intermediate/github-copilot/languages/

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.

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:

  1. Wrong in a way a tool catches instantly. A type error, an undefined symbol, a borrow-checker violation, a syntax error. Cost: seconds.
  2. Wrong in a way a test catches. An off-by-one, a mishandled empty input, an inverted condition. Cost: minutes, if the test exists.
  3. 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.

Twelve languages: tooling and the first thing to checkTooling verified August 21, 2026
Twelve languages: tooling and the first thing to check. One row per language. The table states tooling and the single most important thing to review in generated code; it does not rank languages, because no published benchmark supports that.
LanguageType systemPrimary package toolCommon testsTypical Copilot usesKey review risk
PythonLesson 27Dynamic, with optional static typingpip (PyPI); uv and Poetry are common front-endspytest, unittestHTTP endpoints, data transforms, automation scripts, pytest suites, docstringsSilent type mismatches — nothing fails until the wrong value reaches production
JavaScriptLesson 28Dynamicnpm (also pnpm, yarn)node:test, Vitest, JestExpress routes, DOM handlers, async data fetching, test scaffolding, build configBrowser and Node APIs mixed in one file — the suggestion runs in the wrong runtime
TypeScriptLesson 29Gradualnpm (also pnpm, yarn)Vitest, node:test, JestTyped API clients, interface and generic definitions, discriminated unions, typed testsAssertions and `any` used to silence the compiler instead of fixing the type
JavaLesson 30StaticMaven or GradleJUnit 5, AssertJ, MockitoDTOs and records, Spring controllers, stream pipelines, JUnit tests, builder boilerplateA dependency coordinate that does not exist, or a version that predates the API being called
C#Lesson 31Static, memory-safeNuGet via the dotnet CLIxUnit, NUnit, MSTestMinimal API endpoints, LINQ queries, DI registration, records and DTOs, xUnit testsAsync code that blocks, or LINQ that silently runs in memory instead of in the database
C++Lesson 32Static, manual memory managementvcpkg or Conan; often noneGoogleTest, Catch2, doctestContainer manipulation, algorithm calls, CMake targets, test fixtures, RAII wrappersOwnership and lifetime — code that compiles cleanly and is still undefined behaviour
GoLesson 33Static, memory-safeGo modulesthe standard testing packageHTTP handlers, table-driven tests, struct definitions, context plumbing, CLI flagsDiscarded errors, and goroutines with no cancellation path
RustLesson 34Static, ownership-basedCargo (crates.io)the built-in test harness, proptestEnum and trait definitions, match arms, iterator chains, Serde derives, unit testsFighting the borrow checker with clone() and unwrap() rather than fixing the design
PHPLesson 35Dynamic, with optional static typingComposer (Packagist)PHPUnit, PestControllers, Eloquent and Doctrine queries, form validation, PHPUnit tests, migrationsTwo decades of insecure PHP in the training data: string-built SQL and unescaped output
RubyLesson 36DynamicBundler (RubyGems)RSpec, MinitestEnumerable chains, service objects, RSpec specs, Rake tasks, Rails scaffoldingGenerated metaprogramming, and Rails parameter handling that trusts user input
BashLesson 37Untyped shellNone — the system package manager is the dependencybats-coreHealth checks, CI steps, log processing, backup and deployment scriptsA destructive command run against the wrong path, and unquoted expansions
SQLLesson 38Declarative, schema-typedNone — the database engine is the runtimepgTAP, dbt tests, application-level assertionsAnalytical SELECTs, joins, CTEs, window functions, migrations, query explanationA 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
    Lesson 27 — GitHub Copilot for Python
  • 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
    Lesson 28 — GitHub Copilot for JavaScript
  • 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
    Lesson 29 — GitHub Copilot for TypeScript
  • 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
    Lesson 30 — GitHub Copilot for Java
  • 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
    Lesson 31 — GitHub Copilot for C#
  • 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
    Lesson 32 — GitHub Copilot for C++
  • 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
    Lesson 33 — GitHub Copilot for Go
  • 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
    Lesson 34 — GitHub Copilot for Rust
  • 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
    Lesson 35 — GitHub Copilot for PHP
  • 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
    Lesson 36 — GitHub Copilot for Ruby
  • 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
    Lesson 37 — GitHub Copilot for Bash
  • 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
    Lesson 38 — GitHub Copilot for SQL

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 mypy run 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.
Copilot promptConstrain a dynamic-language suggestion with typesCopilot Chat, inline

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: as assertions and any inserted 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_cast or a raw pointer where ownership was the question.
  • Go: interface{} / any where a concrete type would do.
  • Rust: clone(), unwrap(), and in the worst case an unsafe block.

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:

The review gate for generated shell
  1. Read every command in the suggestionHuman judgementEspecially rm, mv, dd, chmod, chown, sudo, and anything piping a remote URL into a shell.
  2. Run ShellCheckIt finds unquoted expansions, useless subshells and unassigned variables mechanically.
  3. 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.
  4. Dry-run where the tool supports itrsync --dry-run, terraform plan, kubectl --dry-run=client, apt --simulate.
  5. Execute in a non-production environment firstA container, a VM, or a scratch directory you can afford to lose.
  6. 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   390

Neither 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:

  1. Name the framework and major version in the prompt, every time, until it is in your repository instructions.
  2. Keep the manifest visible. requirements.txt, package.json, pom.xml and their siblings are the cheapest possible disambiguation.
  3. 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.

Reviewing a Copilot suggestion
  1. Run the fastest honest checkThe compiler, the type checker, the linter — whichever your language gives you. Seconds, and it eliminates a whole category.
  2. Read every import and dependencyHuman judgementDoes it exist? Is it in the lock file? Is the API in the version you have installed?
  3. 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.
  4. Check the boundariesHuman judgementEmpty input, single element, maximum size, null, zero, negative, and the timezone.
  5. Check the error pathHuman judgementWhat happens when the call fails? Is the failure handled, logged, or silently swallowed?
  6. Ask what a hostile input would doHuman judgementEspecially for anything touching a shell, a query, a filesystem path or rendered output.
  7. 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:

CheckCostCatches
Does the import resolve?Free — the compiler or interpreter tells youNon-existent packages, in compiled languages
Is it in the lock file?SecondsAnything added without going through the package manager
Does the registry have it?SecondsFabricated names
Is the API in this version’s docs?A minuteVersion drift, the most common case
Who publishes it, and since when?A few minutesTyposquats 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:

LanguageCommand
Pythonmypy . && pytest -q
JavaScriptnode --test && npx eslint .
TypeScripttsc --noEmit
Javamvn -q verify
C#dotnet build -warnaserror && dotnet test
C++-Wall -Wextra -fsanitize=address,undefined, then run the tests
Gogo vet ./... && go test -race ./...
Rustcargo clippy -- -D warnings && cargo test
PHPvendor/bin/phpstan analyse && vendor/bin/phpunit
Rubybundle exec rubocop && bundle exec rspec
Bashshellcheck script.sh && bash -n script.sh
SQLEXPLAIN 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:

Copilot promptWeak — leaves every decision to the modelCopilot Chat

Write some Python to calculate percentiles.

A strong one:

Copilot promptStrong — every ambiguity resolvedCopilot Chat

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 except blocks, Rust reaches for unwrap().
  • Missing boundary checksC++ most dangerously, but empty-collection handling is a universal gap.
  • Unsafe input handlingPHP, JavaScript and Ruby carry the most historical baggage here.
  • Incorrect concurrencyGo goroutine leaks, Java shared mutable state, C# blocking async, C++ data races.
  • Memory-safety errorsC++, and the unsafe blocks in Rust.
  • Shell injectionBash directly, and every language’s subprocess API.
  • SQL injectionSQL 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.

Primary sources