01Write the signature and docstring examples first, then let Copilot fill in the bodyVerified
Writing the imports, function name, typed parameters and return value, and docstring examples before asking for completion tells Copilot which library to use and what result you expect. The docstring examples double as doctest tests, so you can check at once that the generated body behaves as shown.
- Put the imports for the libraries you want at the top of the file
- Write the function name, typed parameters and return type, and a one-line docstring stating the purpose
- Add >>> examples to the docstring for normal input and edge cases (full-width characters, extra words, no match)
- Let Copilot complete the body, and accept it only after you have read and understood it
- Run the examples with doctest; add any failing case to the prompt and have Copilot try again
"""Skeleton given to Copilot (imports, signature, docstring examples); body kept after review."""
import re
import unicodedata
PATTERN = re.compile(r"R-?(\d{4})-(\d{1,3})")
def normalize_request_id(text: str) -> str | None:
"""Return a request ID as R-YYYY-NNN, or None when the text contains no ID.
>>> normalize_request_id("r-2026-5")
'R-2026-005'
>>> normalize_request_id("\uff32\uff0d\uff12\uff10\uff12\uff16\uff0d\uff10\uff14\uff12 proposal")
'R-2026-042'
>>> normalize_request_id("see R2026-7 for details")
'R-2026-007'
>>> normalize_request_id("no id here") is None
True
"""
t = unicodedata.normalize("NFKC", text or "").upper()
m = PATTERN.search(t)
if not m:
return None
return "R-%s-%03d" % (m.group(1), int(m.group(2)))
if __name__ == "__main__":
import doctest
print("doctest:", doctest.testmod())Output(2026-09-12)
doctest: TestResults(failed=0, attempted=4)Write the body of normalize_request_id so that every docstring example passes. Use only re and unicodedata. Convert full-width letters, digits and symbols with NFKC, and pad the number to three digits.
Copilot guesses how to treat inputs your examples do not cover, so add the formats that actually occur before relying on it. Passing every example says nothing about inputs outside them; also try a slice of real data. doctest compares printed text, so a change in output formatting makes it fail.
Supporting passages from the sources
Use examples to help Copilot understand what you want. You can provide example input data, example outputs, and example implementations.
If you want to use a specific library, set the import statements at the top of the file or specify what library you want to use.
To check that a module’s docstrings are up-to-date by verifying that all interactive examples still work as documented.
02Write assert-based tests first, then ask Copilot for a function that passes themVerified
GitHub's documentation describes writing unit tests before the function and then asking Copilot for a function described by those tests. It suits specs that are quicker to pin down with examples than with prose, such as inconsistent date formats. Run the tests and repeat prompt and generation until all pass.
- List the formats to accept and the ones to reject (missing year, impossible dates, blanks)
- Turn the list into (input, expected) pairs and write a test function that compares them with assert
- Ask Copilot to write a function that passes all the tests, naming the modules it may use
- Run the tests and show Copilot the failing pairs; do not let it rewrite the tests themselves
- When a new format appears in real work, add it to the tests first, then fix the function
"""Tests written first; the function was then generated from them and kept after review."""
import re
from datetime import date
TESTS = [
("2026-09-12", date(2026, 9, 12)),
("2026/9/1", date(2026, 9, 1)),
("2026\u5e749\u670812\u65e5", date(2026, 9, 12)), # 2026 nen 9 gatsu 12 nichi
(" 2026.09.12 ", date(2026, 9, 12)),
("9/12", None), # no year: do not guess
("2026-02-30", None), # not a real date
("", None),
]
def parse_date(text):
m = re.fullmatch(r"\s*(\d{4})[-/.\u5e74](\d{1,2})[-/.\u6708](\d{1,2})\u65e5?\s*", text or "")
if not m:
return None
try:
return date(*map(int, m.groups()))
except ValueError:
return None
def test_parse_date():
for text, want in TESTS:
got = parse_date(text)
assert got == want, (text, got, want)
if __name__ == "__main__":
test_parse_date()
print("all", len(TESTS), "tests passed")Output(2026-09-12)
all 7 tests passedWrite parse_date(text) that passes every case in TESTS below. Use only re and datetime. Return None for inputs without a year and for impossible dates; do not guess. Do not change the test table.
Formats not in the tests are not covered. Handle era-based dates or phrases like next Monday separately, or have a person check them. Copilot sometimes special-cases the exact test values, so read the body before accepting it.
Supporting passages from the sources
Unit tests can also serve as examples. Before writing your function, you can use Copilot to write unit tests for the function. Then, you can ask Copilot to write a function described by those unit tests.
Generating tests for test-driven development
03Split a large task into small functions and prompt for them one at a timeVerified
Instead of asking for a whole ledger report at once, have Copilot write separate functions for loading, filtering, counting and writing, one prompt each. An assert right after each step shows which stage went wrong when results drift. Finally, ask for a function that ties the earlier ones together.
- Split the job into four stages (load, filter, count, write) and describe each stage's input and output in one line
- Use the first prompt for the loading function and assert that the column names are as expected
- In the next prompts, state that the function takes the previous function's return value (a list of dicts), and ask for filtering and counting
- Put a one-line assert after each stage and run it on sample data
- Finally ask for a main that calls the earlier functions in order, and check the counts against the sample
"""Request-ledger report built from small functions, each written from its own prompt."""
import csv
from collections import Counter
STATUS, DEPT, DONE = "\u72b6\u614b", "\u4f9d\u983c\u90e8\u7f72", "\u5b8c\u4e86" # column names / done value
def load_rows(path): # prompt 1
with open(path, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
def open_items(rows): # prompt 2
return [r for r in rows if r[STATUS] != DONE]
def count_by(rows, column): # prompt 3
return Counter(r[column] for r in rows)
def write_report(counter, path): # prompt 4
with open(path, "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
w.writerow(["key", "count"])
w.writerows(counter.most_common())
def main(): # last prompt: use the previous functions
rows = load_rows("samples/ledger.csv")
assert rows and STATUS in rows[0] and DEPT in rows[0], "unexpected columns"
todo = open_items(rows)
assert all(r[STATUS] != DONE for r in todo)
by_dept = count_by(todo, DEPT)
assert sum(by_dept.values()) == len(todo)
write_report(by_dept, "out_open_by_dept.csv")
print("rows:", len(rows), "open:", len(todo))
for dept, n in by_dept.most_common():
print(dept, n)
if __name__ == "__main__":
main()Output(2026-09-12)
rows: 40 open: 26
広報部 14
営業部 7
人事部 5(1) Write load_rows(path) that reads the request ledger CSV (UTF-8 with BOM) into a list of dicts. (2) Write open_items(rows) that takes load_rows's result and keeps rows whose status is not complete. (3) Write count_by(rows, column) that counts rows by a column. (Last) Write a main that uses those functions to write open items per department to a CSV.
Splitting the work does not help if the contract between stages (column names, types, blanks) drifts, so restate it in every prompt. Check first that status values are spelled consistently in the ledger. Reconcile the totals with the source ledger before using them.
Supporting passages from the sources
If you want Copilot to complete a complex or large task, break the task into multiple simple, small tasks.
Write a function that uses the previous functions to generate a 10 by 10 grid of letters that contains at least 10 words.
04Put repository-wide rules in .github/copilot-instructions.mdVerified
A .github/copilot-instructions.md file at the repository root is added automatically to requests you make to Copilot in that repository. Write the rules that apply every time, such as reading IDs from environment variables, DRY_RUN by default for writes and least privilege, as short, self-contained statements.
- Create a .github folder at the repository root and add copilot-instructions.md
- Write the instructions as natural language in Markdown; whitespace between instructions is ignored
- They are sent with every request, so include only what applies to most work, not task-specific steps
- If .github/copilot-instructions.md appears in the references of a Copilot Chat answer, the instructions were used
- Personal instructions take priority, then repository, then organization instructions; avoid conflicts between them
- In IDEs, task-specific reusable prompts can go into prompt files (*.prompt.md, public preview)
# Project overview
Python scripts that read Microsoft 365 data through the Microsoft Graph SDK for Python and write reports to CSV files.
## Security rules
- Read the tenant ID, client ID and all other IDs from environment variables (os.environ). Never write IDs, secrets, tokens or webhook URLs in code.
- For app-only access use a certificate or a managed identity (azure-identity), never a client secret.
- Request the least privileged Graph permission shown on the API page. Read-only scripts must not request ReadWrite permissions.
- Use $select so that only the fields the script needs are fetched.
- Never print or log access tokens, message bodies or e-mail addresses.
## Writes
- Code that sends, updates or uploads anything must define DRY_RUN = os.environ.get("DRY_RUN", "1") != "0" and, when DRY_RUN is true, only print what it would do.
- Do not write code that deletes data.
## Style and tests
- Keep comments short and in English.
- Put logic in small functions and add an assert-based test for each. Tests must run without signing in or using the network.
- On HTTP 429, wait for the Retry-After seconds before retrying.Copilot takes the instructions into account, but you still read the generated code to check they were followed. Never put secrets or internal URLs in the file; anyone who can read the repository can read it. The documentation notes that instructions to consult external resources or to limit answer length may not work as intended.
Supporting passages from the sources
In the root of your repository, create a file named .github/copilot-instructions.md.
Custom instructions consist of natural language instructions and are most effective when they are short, self-contained statements.
Because the instructions are sent with every chat message, they should be broadly applicable to most requests you will make in the context of the repository.
Personal instructions take the highest priority. Repository instructions come next, and then organization instructions are prioritized last.
Prompt files are only available in VS Code, Visual Studio, and JetBrains IDEs.
05Scope instructions to certain folders with .github/instructions/*.instructions.mdVerified
For instructions that apply to only some files, create a file ending in .instructions.md under .github/instructions and put a glob in applyTo at the top. When Copilot works on a matching file, both these and the repository-wide instructions are used, which keeps the repository-wide file short.
- Create the .github/instructions folder; subfolders are fine for organizing
- Name the file NAME.instructions.md after its purpose (for example graph-scripts.instructions.md)
- Add applyTo to the front matter at the top; separate multiple patterns with commas
- To keep an instruction file away from Copilot code review on GitHub.com, add excludeAgent: "code-review" to the front matter
- Path-specific instructions take precedence over repository-wide ones, so check that the two do not conflict
---
applyTo: "scripts/graph/**/*.py,tests/graph/**/*.py"
---
# Graph scripts
- Build credentials with azure-identity: DeviceCodeCredential for delegated scripts, CertificateCredential or ManagedIdentityCredential for app-only jobs.
- Declare the permissions the script needs in a SCOPES constant at the top of the file, using the least privileged one listed on the API page.
- Pass $select with only the properties used later in the script.
- Keep API calls in one async function; keep filtering and counting in plain functions that take lists of dicts, so tests can feed them sample JSON.
- Tests under tests/graph must not sign in or use the network; load the JSON files in tests/graph/samples instead.On GitHub.com, path-specific instructions are currently supported only by Copilot cloud agent and Copilot code review. Check the IDE documentation for how VS Code and other IDEs handle them. If the glob does not match, the instructions are not used, so check the references list in Chat on a target file.
Supporting passages from the sources
Create one or more NAME.instructions.md files, where NAME indicates the purpose of the instructions. The file name must end with .instructions.md.
Currently, on GitHub.com, path-specific custom instructions are only supported for Copilot cloud agent and Copilot code review.
If the path you specify matches a file that Copilot is working on, and a repository-wide custom instructions file also exists, then the instructions from both files are used.
VS Code supports three types of repository custom instructions.
06Screen generated code with ast before reading it yourselfVerified
GitHub advises understanding Copilot's suggestions before using them and checking them with tests and tools such as linters. Using Python's ast, list calls to eval and exec, requests calls without a timeout, and string literals assigned to secret-like names, before anyone reads the code.
- Save the generated code to a file and turn it into a syntax tree with ast.parse, without running it
- Walk every node with ast.walk and look only at calls and assignments
- Use three rules: calls to eval, exec or compile; requests calls without timeout; string literals assigned to names like secret, password or token
- List findings with line numbers as pointers for the human review
- Test the checker on a sample containing both cases it must catch and a case it must not (a call with timeout)
"""Screen generated Python for risky patterns before a human review (ast only; nothing is run)."""
import ast
import sys
RISKY_CALLS = {"eval", "exec", "compile"}
HTTP_VERBS = {"get", "post", "put", "patch", "delete", "request"}
SECRET_WORDS = ("secret", "password", "passwd", "token", "api_key", "apikey")
def findings(source, name="<generated>"):
out = []
for node in ast.walk(ast.parse(source, name)):
if isinstance(node, ast.Call):
f = node.func
if isinstance(f, ast.Name) and f.id in RISKY_CALLS:
out.append((node.lineno, "call to %s()" % f.id))
if (isinstance(f, ast.Attribute) and f.attr in HTTP_VERBS
and isinstance(f.value, ast.Name) and f.value.id == "requests"
and not any(k.arg == "timeout" for k in node.keywords)):
out.append((node.lineno, "requests.%s without timeout" % f.attr))
if (isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant)
and isinstance(node.value.value, str) and len(node.value.value) >= 8):
for t in node.targets:
if isinstance(t, ast.Name) and any(w in t.id.lower() for w in SECRET_WORDS):
out.append((node.lineno, "string literal assigned to %s" % t.id))
return sorted(out)
def main(path):
with open(path, encoding="utf-8") as f:
found = findings(f.read(), path)
for line, msg in found:
print("line %d: %s" % (line, msg))
print("findings:", len(found))
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "samples/security_generated_snippet.txt")Output(2026-09-12)
line 5: string literal assigned to api_key
line 10: requests.get without timeout
line 14: call to eval()
findings: 3Write findings(source) that takes Python source as a string, inspects it with ast only, and returns a list of (line number, message). Three rules: calls to eval, exec or compile; requests calls such as requests.get with no timeout argument; string literals of 8 or more characters assigned to names containing secret, password, token or api_key. Never execute the code.
This check finds only fixed patterns; it misses aliased imports and secrets inside dicts. Zero findings does not mean the code is safe, so keep the human review, the tests and your organization's code scanning. Add rules based on mistakes you have actually seen.
Supporting passages from the sources
Use automated tests and tooling to check Copilot's work. With the help of tools like linting, code scanning, and IP scanning, you can automate an additional layer of security and accuracy checks.
Understand suggested code before you implement it.
Recursively yield all descendant nodes in the tree starting at node