GitHub Copilot CLI for Python Developers

GitHub Copilot CLIAcademy lesson 58Cluster 5 · Lesson 7 of 12Intermediate14 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot CLI for Python DevelopersGitHub Copilot CLI7Intermediate/github-copilot/cli/python/

Python is the language where an agentic CLI most clearly beats an editor assistant, and the reason is narrow: the feedback loop is already a terminal loop. pytest, ruff, mypy — the tools that tell you whether code is correct all run as commands, and an agent that can run them can close the loop itself instead of handing you a suggestion and waiting.

Cluster 3’s Copilot for Python covers what Copilot knows about the language. This lesson is about driving a project from the terminal: environments, failing tests, linting, and the review that happens before anything is committed.

The loop

A terminal-native Python change

The value is in steps four through six happening without you. An agent that runs the suite, reads the failure, and adjusts is doing the same iteration you would, faster. Your job moves to the ends of the loop: framing the problem correctly and reviewing what came out.

Permissions for Python work

Example policyA test-and-fix session: check freely, change with visibility, never install or publish.
Example tool permission policy
PatternDecisionWhy
shell(pytest:*)AllowedThe loop depends on running the suite.
shell(ruff:*)AllowedStatic analysis with no side effects.
shell(git:*)AllowedLocal inspection and staging.
writeAsks firstSource changes surface before they land.
shell(pip:*)DeniedAdding a dependency is a decision, not a step.
shell(git push:*)DeniedPublishing stays human.

Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.

Denying pip is the row worth defending. An agent that hits a problem solvable by a library will reach for the library, and that is often the wrong trade: a new dependency is new supply-chain surface, a new licence, and a new thing to update forever, in exchange for saving twenty lines.

Denying it does not prevent the agent proposing a dependency; it prevents the dependency arriving without a conversation.

Virtual environments, and telling it what you mean

The most common source of baffling behaviour in Python agent sessions is the interpreter. The agent runs pytest, gets ModuleNotFoundError, and starts solving a problem that does not exist — because it invoked the system Python rather than your environment.

Be explicit:

Copilot prompt

This project uses a virtual environment at .venv.

Always run Python tools through it: .venv/bin/python -m pytest and .venv/bin/python -m ruff, not bare pytest or ruff.

Do not create a new environment. Do not install anything.

python -m pytest rather than pytest is worth insisting on generally. It uses the interpreter you named rather than whatever the PATH resolves, which removes a whole class of confusion — including the one where a globally-installed pytest imports a different copy of your package than the one you are editing.

Working from a failing test

The strongest entry point for agentic Python work is a test that already fails, because the definition of done is objective.

Copilot prompt

tests/test_parser.py::test_handles_empty_input is failing.

Read the test and the code under test, explain why it fails, then fix the cause.

Do not modify the test. If you believe the test is wrong, stop and explain why rather than changing it.

Those last two sentences are the entire technique. An agent asked to make a test pass has two routes to success, and editing the assertion is the cheaper one. It does not do this maliciously — it is a legitimate reading of the instruction — but the result is a green suite that verifies nothing.

Running the suite properly

Practical example

A fix-verification loop

Confirm a change fixes the target test without breaking anything else, using output rather than the agent's summary.

Status
Example implementation
Not executed here
This code was written against the documentation cited at the end of the lesson but was not run while writing it. Treat the commands as the ones to run, not as output that has been observed.
.venv/bin/python -m pytest tests/test_parser.py -q     # the target
.venv/bin/python -m pytest -q                          # the whole suite
.venv/bin/python -m ruff check .                       # lint

The two pytest invocations are both necessary. The first confirms the fix; the second confirms it did not break something else, which is the failure mode of a narrowly-focused agent that has not been asked to check.

Ask for the actual output rather than a summary. “All tests pass” and a pasted 142 passed in 3.21s are different kinds of claim.

Linting and formatting

Ruff is fast enough that there is no reason not to run it after every change, and it is a static check, so pre-approving it is safe.

Two distinct operations worth keeping separate in your head:

.venv/bin/python -m ruff check .          # report problems
.venv/bin/python -m ruff format .         # rewrite files

check reports. format rewrites, and check --fix also rewrites. That distinction matters in an agent session because a formatting pass across a repository produces an enormous diff that buries the actual change.

Type checking

Type checking catches a category tests routinely miss: an argument that is never passed the wrong type in the tests but could be in production. It is static, so it is safe to run freely.

Copilot prompt

Run the type checker and fix the errors in src/parser.py.

Do not add # type: ignore comments. If an error cannot be fixed without restructuring, explain what the restructuring would be and stop.

Banning # type: ignore is the important constraint. It is the fastest way to make a type error disappear and it removes the information the checker was providing. The same applies to widening a type to Any — technically a fix, practically a deletion of the check.

Refactoring, and keeping it bounded

Refactoring is where agents most reliably exceed their brief. The instruction that works is a boundary, not an adjective.

Copilot prompt

Refactor the validation logic in src/parser.py into a separate module, src/validation.py.

Constraints:

  • Public behaviour must not change. The existing tests must pass unmodified.
  • Only src/parser.py, src/validation.py and the imports that reference them may change.
  • Do not rename any public function.
  • Run the full suite before and after, and show me both results.

If you think a behaviour change is warranted, describe it and stop.

“Before and after” is a small addition with real value: it establishes that the suite was green to begin with. A refactor that ends with two failures is a different conversation if those two were already failing.

Understanding an unfamiliar codebase

Before changing anything in a project you inherited, spend a session that cannot change anything. This is the highest-value use of a read-only agent, and it is cheap.

Copilot prompt

Explain how this project is organised.

Do not modify anything.

Cover: the entry points, how configuration is loaded, where the tests live and how they are run, what the external dependencies are used for, and anything that looks like it was left half-finished.

Started with --deny-tool='write', that session cannot go wrong. What you learn is where things are and, more usefully, whether the agent’s reading of the project matches yours — because if it misreads the architecture now, it will misread it later when it is editing.

The explore built-in agent is designed for this and is read-only by construction:

copilot --agent explore -i "How does authentication work in this project?"

Dependencies, and why pip stays denied

When an agent needs functionality the standard library does not provide, its instinct is to add a package. Sometimes that is right. Often the calculation is worse than it looks.

A dependency brings transitive dependencies, a licence, a maintenance status, a release cadence you now track, and — for anything running in production — supply chain surface. Trading that for code you could write in twenty lines is usually a bad deal, and it is a deal the agent is not positioned to evaluate because it cannot see your constraints.

Denying pip forces the conversation:

Copilot prompt

Before adding any dependency, tell me:

  • What it does that the standard library cannot
  • How many transitive dependencies it brings
  • Roughly how much code it would replace

Then wait for me to decide.

The honest answer is sometimes “this replaces four hundred lines of date parsing and you should absolutely use it”. The point is not to refuse dependencies; it is to make the choice visible rather than discovering it in a diff.

Reading the diff

The habit that matters most, and the one most easily skipped when the tests are green.

git diff --stat        # what changed, and how much
git diff               # what actually changed

Or /diff inside the session. Things worth looking for in Python specifically:

Did it touch tests you did not ask it to touch? --stat answers this instantly.

Did it change a function signature? Adding a parameter with a default is source-compatible and can still break a caller passing positionally.

Did it swallow an exception? A bare except: or except Exception: pass added to make a test pass is a bug with a green light on it.

Did it add an import? Standard library is fine. A third-party import is the dependency conversation you denied pip to force.

Did it change behaviour for input the tests do not cover? This is the one static tools cannot help with, and the reason reading the diff is not optional.

Reviewing before you commit

/review
/security-review

/review runs the code review agent over your changes; /security-review analyses staged and unstaged changes specifically for security problems. Both are read-only.

For Python, the security review is worth running whenever a change touches input handling, subprocess calls, deserialisation, or anything constructing a query or a path from user data. The code review lesson covers what these agents catch and — more importantly — what they do not.

Where the agent is weakest

Async correctness. Generated async code frequently awaits the wrong thing, mixes sync and async in a way that blocks the loop, or forgets that a context manager needs async with. Tests often pass anyway, because the bug is a performance failure rather than a correctness one.

Anything depending on runtime state. Database contents, environment differences, timing. The agent reasons from code, and code does not contain the state.

Dependency versions. It will suggest APIs from a version you may not be on. Pinning matters, and so does telling it what you actually have installed.

Test quality. Generated tests trend towards asserting that the implementation is the implementation. A test that mirrors the code’s structure passes whenever the code is unchanged and tells you nothing about whether it is right.

A project layout that helps

None of this requires restructuring a project, but a few conventions make agent sessions noticeably smoother.

A src/ layout helps because it makes the import path unambiguous — the agent cannot accidentally import the source directory instead of the installed package, which is a real source of confusing test failures.

Configuration centralised in pyproject.toml helps because the agent reads it and follows the settings. If Ruff’s line length and rule selection are declared there, generated code matches them without being told.

And path-scoped instructions under .github/instructions/ let you say things about tests that should not apply to source. A tests.instructions.md with applyTo: "tests/**/*.py" can specify pytest conventions, fixture usage and determinism requirements without those rules leaking into application code. The custom instructions lesson covers the frontmatter syntax.

Async, and why it needs extra scrutiny

Async Python is where generated code is least reliable, and the reason is worth understanding rather than just noting.

Async bugs frequently do not fail. A blocking call inside a coroutine — a synchronous HTTP request, a file read, a time.sleep — works correctly. It returns the right answer. It simply blocks the event loop while it does so, serialising everything the loop was supposed to be overlapping. Tests pass, behaviour is correct, and throughput quietly collapses under load.

Because there is no error, none of the checks in this lesson catch it. Not tests, not Ruff, not the type checker.

The things worth checking by eye in any async change:

  • Every await is on something awaitable, and everything awaitable is awaited. A missing await returns a coroutine object, which is truthy and often flows on harmlessly until something inspects it.
  • No synchronous IO inside a coroutine — requests, open, time.sleep.
  • Async context managers use async with, async iteration uses async for.
  • Anything CPU-bound is not sitting on the event loop.
Copilot prompt

Review the async code in this module.

Identify: any blocking call inside a coroutine, any awaitable that is not awaited, and any place where async with or async for should be used and is not.

For each, explain the runtime consequence rather than just naming the pattern.

Asking for the consequence is what separates a real finding from pattern matching — and in async code, the consequence is usually the only way to tell whether something matters.

What to carry forward

Name the interpreter. Most confusing Python agent sessions are environment problems wearing a costume.

Pre-approve the checks, prompt on writes, deny installs.

Say “fix the cause, do not modify the test” every time, or once in your instructions file.

Read git diff --stat before git diff. It catches scope expansion in one line.

Verify new tests fail against broken code before trusting them. A test that passes regardless of whether the implementation is correct is worse than no test, because it carries the authority of a green suite.

One closing observation about where this workflow fits. The terminal loop suits work with an objective finish line — a failing test, a type error, a lint violation — because the agent can tell whether it is done. Work without one, such as “make this module nicer”, suits an editor better, where you are reading each change as it lands rather than reviewing a batch at the end.

Choosing the surface by whether the task has a checkable definition of done is a more useful heuristic than choosing by habit.

A note on notebooks

Jupyter notebooks are a common Python workflow and a poor fit for a terminal agent, which is worth saying plainly rather than discovering.

Notebooks are JSON containing code, output and execution counts. An agent editing one is editing JSON, diffs are close to unreadable, and cell execution order — which frequently determines whether the code works at all — is invisible in the file.

If your work is notebook-based, the CLI is still useful for everything around the notebook: the package it imports, the tests, the data pipeline, the environment. For the notebook itself, an editor with notebook support is the better surface, and Cluster 2 covers that ground.

Where a notebook holds logic worth testing, the durable answer is to move that logic into a module the notebook imports. That is good practice independently, and it happens to put the important code somewhere an agent can work on it properly.

Next

DevOps applies the same validate-before-execute discipline where the validation commands touch infrastructure rather than code. Custom instructions is how the environment and toolchain rules above stop needing to be repeated.

For Copilot’s Python knowledge outside the CLI, Cluster 3’s Copilot for Python is the companion, and code review goes deeper on /review.

Sources

Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.

Primary sources