Audience: Developers
Purpose: Learn to write effective prompts that generate high-quality code with GitHub Copilot
Duration: 20-30 minute read with hands-on practice
- The 4Ss: Core Principles
- Basic Prompt Structure
- Prompt Crafting Techniques
- Comment-Driven Development
- Context is King
- Function & Method Naming
- Iterative Refinement
- Advanced Techniques
- Common Patterns
- Anti-Patterns (What NOT to Do)
- Practice Exercises
Before we explore specific strategies, let's first understand the basic principles of prompt engineering, summed up in the 4 Ss below. These core rules are the basis for creating effective prompts with GitHub Copilot.
Always focus your prompt on a single, well-defined task or question. This clarity is crucial for eliciting accurate and useful responses from Copilot.
# ❌ Multiple tasks: Trying to do too much at once
# Create a user, validate their email, send a welcome message, and log the activity
# ✅ Single task: One clear, focused action
# Create a new user in the database with the given email and name
def create_user(email: str, name: str) -> User:Ensure that your instructions are explicit and detailed. Specificity leads to more applicable and precise code suggestions.
# ❌ Vague: What kind of validation? What rules?
# Validate the password
# ✅ Specific: Explicit requirements and constraints
# Validate password: min 8 chars, at least one uppercase, one lowercase,
# one digit, and one special character (!@#$%^&*)
def validate_password(password: str) -> bool:While being specific, keep prompts concise and to the point. This balance ensures clarity without overloading Copilot or complicating the interaction.
# ❌ Too verbose: Excessive explanation dilutes the intent
# I would like you to please create a function that will take a number
# and then check if that number is even or not and then return the result
# as a boolean value indicating whether it's even
# ✅ Short and clear: Essential information only
# Return True if n is even, False otherwise
def is_even(n: int) -> bool:Utilize descriptive filenames and keep related files open. This provides Copilot with rich context, leading to more tailored code suggestions.
# ✅ Copilot reads context from:
# - Current file: user_repository.py
# - Open files: user.py (User model), database.py (DB connection)
# - Existing patterns in the file
class UserRepository:
def get_by_id(self, user_id: int) -> Optional[User]:
"""Fetch user by ID from database."""
# Copilot sees the User model and database patterns
query = "SELECT * FROM users WHERE id = ?"
result = self.db.execute(query, (user_id,))
return User(**result) if result else None
# When you start typing the next method, Copilot uses the surrounding
# context to suggest consistent patterns and return types
def get_by_email(self, email: str) -> Optional[User]:Pro Tip: Keep your workspace organized with descriptive filenames like user_repository.py, order_service.py, or payment_validator.py. Copilot uses these names to understand your intent.
A well-structured prompt helps GitHub Copilot understand exactly what you need. Great prompts often contain up to four elements:
| Element | Purpose | Example |
|---|---|---|
| Instruction | What you want Copilot to do | "Calculate the compound interest" |
| Context | Background or constraints | "for a savings account with monthly compounding" |
| Input | The data to process | "given principal, rate, and time in years" |
| Output Format | How the result should be presented | "return as a float rounded to 2 decimal places" |
# Instruction: Calculate the compound interest
# Context: for a savings account with monthly compounding (12 times per year)
# Input: principal amount, annual interest rate (as decimal), and time in years
# Output: return the final amount as a float rounded to 2 decimal places
def calculate_compound_interest(principal: float, rate: float, years: int) -> float:Result: Copilot generates accurate code with the formula A = P(1 + r/n)^(nt).
# [INSTRUCTION] Validate an email address
# [CONTEXT] using regex pattern matching for standard email format
# [INPUT] email string to validate
# [OUTPUT] return True if valid, False otherwise; do not raise exceptions
def is_valid_email(email: str) -> bool:💡 Tip: You don't always need all four elements. Simple tasks may only require an instruction, while complex operations benefit from the full structure.
❌ Bad Prompt:
# Calculate something
def process():✅ Good Prompt:
# Calculate the total shipping cost based on weight (kg), distance (km),
# and shipping type (standard or express). Express costs 2x standard rate.
def calculate_shipping_cost(weight: float, distance: float, is_express: bool) -> float:Why it works: Copilot understands the business logic, units, and pricing rules.
❌ Bad Prompt:
# Divide two numbers
def divide(a, b):✅ Good Prompt:
# Divide a by b. Raise ValueError if b is zero.
# Return result rounded to 2 decimal places.
def divide(a: float, b: float) -> float:Result: Copilot generates defensive code with proper error handling.
❌ Bad Prompt:
# Get user info
def get_user():✅ Good Prompt:
# Fetch user data from the database by user_id.
# Returns dict with keys: 'name', 'email', 'created_at'.
# Returns None if user not found.
def get_user(user_id: int) -> Optional[Dict[str, Any]]:Why it works: Type hints + comment description = better suggestions.
Copilot reads comments above your code to understand intent. Write comments as specifications.
✅ Effective Pattern:
def validate_password(password: str) -> bool:
"""
Validate password strength.
Requirements:
- At least 8 characters long
- Contains uppercase and lowercase letters
- Contains at least one digit
- Contains at least one special character (!@#$%^&*)
Returns True if valid, False otherwise.
"""
# Copilot will implement all requirementsResult: Copilot generates complete validation logic checking all criteria.
✅ Show Examples:
# Parse a date string in multiple formats and return a datetime object.
# Examples:
# "2025-12-17" -> datetime(2025, 12, 17)
# "12/17/2025" -> datetime(2025, 12, 17)
# "Dec 17, 2025" -> datetime(2025, 12, 17)
# Raise ValueError if format is not recognized.
def parse_flexible_date(date_str: str) -> datetime:Why it works: Examples clarify ambiguous requirements.
Copilot reads surrounding code to understand patterns. Use this to your advantage.
class UserRepository:
def get_by_id(self, user_id: int) -> Optional[User]:
"""Fetch user by ID from database."""
query = "SELECT * FROM users WHERE id = ?"
result = self.db.execute(query, (user_id,))
return User(**result) if result else None
# Copilot will now follow the same pattern for new methods!
def get_by_email(self, email: str) -> Optional[User]:
# Copilot suggests: query, execute, return patternResult: Consistent code style across all repository methods.
# Existing code establishes pattern
def add(self, a: float, b: float) -> float:
"""Add two numbers and return the result."""
result = a + b
self.history.append(f"add({a}, {b}) = {result}")
return result
def subtract(self, a: float, b: float) -> float:
"""Subtract b from a and return the result."""
result = a - b
self.history.append(f"subtract({a}, {b}) = {result}")
return result
# Now just type the signature for multiply
def multiply(self, a: float, b: float) -> float:
# Copilot automatically includes docstring AND history tracking!Names are prompts! Descriptive names generate better code.
❌ Vague Names:
def process(data): # What kind of processing?
def handle(item): # Handle how?
def do_stuff(x, y): # What stuff?✅ Descriptive Names:
def sanitize_user_input(raw_input: str) -> str:
# Copilot knows to remove HTML, SQL injection attempts, etc.
def calculate_compound_interest(principal: float, rate: float, years: int) -> float:
# Copilot generates the compound interest formula
def retry_on_network_failure(func: Callable, max_attempts: int = 3):
# Copilot creates retry logic with exponential backoffUse clear verb-noun combinations:
| Pattern | Example | Copilot Understands |
|---|---|---|
get_* |
get_user_profile() |
Fetch/retrieve operation |
set_* |
set_cache_timeout() |
Update/modify operation |
is_* / has_* |
is_valid_email() |
Boolean check, returns True/False |
calculate_* |
calculate_tax() |
Math/computation operation |
parse_* |
parse_json_config() |
Convert from one format to another |
validate_* |
validate_credit_card() |
Check rules, return bool or raise error |
format_* |
format_currency() |
Transform data for display |
You don't need the perfect prompt on the first try. Iterate!
Iteration 1:
# Sort a list of users
def sort_users(users):Iteration 2: (Add constraints)
# Sort a list of user dictionaries by 'last_login' date, most recent first
def sort_users(users: List[Dict]) -> List[Dict]:Iteration 3: (Add edge cases)
# Sort a list of user dictionaries by 'last_login' date, most recent first.
# Users with no 'last_login' should appear at the end.
# Handle None values gracefully.
def sort_users(users: List[Dict[str, Any]]) -> List[Dict[str, Any]]:For complex logic, use Copilot Chat to refine:
👤 "Generate a function that validates a JSON schema with custom error messages"
🤖 [Copilot generates basic version]
👤 "Add support for nested objects and array validation"
🤖 [Copilot refines with nested support]
👤 "Include line numbers in error messages"
🤖 [Copilot adds line tracking]
For complex functions, use detailed docstrings:
def process_payment(
user_id: int,
amount: float,
payment_method: str,
currency: str = "USD"
) -> Dict[str, Any]:
"""
Process a payment transaction with fraud detection.
Workflow:
1. Validate user exists and is active
2. Check payment method is valid and not expired
3. Run fraud detection algorithm
4. If fraud score > 0.8, flag for manual review
5. Process payment through payment gateway
6. Update user balance and transaction history
7. Send confirmation email
Args:
user_id: Database ID of the user
amount: Payment amount (must be positive)
payment_method: One of ['credit_card', 'paypal', 'bank_transfer']
currency: ISO currency code (default: USD)
Returns:
Dict containing:
- transaction_id: str
- status: str ('success', 'pending', 'failed')
- fraud_score: float (0.0 to 1.0)
- timestamp: datetime
Raises:
ValueError: If amount is negative or zero
UserNotFoundError: If user_id doesn't exist
PaymentGatewayError: If external payment service fails
"""
# Copilot generates comprehensive implementation following all steps!def format_phone_number(phone: str) -> str:
"""
Format phone number to international standard.
Input formats accepted:
"1234567890" -> "+1 (123) 456-7890"
"123-456-7890" -> "+1 (123) 456-7890"
"+11234567890" -> "+1 (123) 456-7890"
Assumes US numbers if no country code provided.
"""
# Copilot handles all format variationsWrite test first, implementation second:
def test_calculate_fibonacci():
"""Test Fibonacci sequence generation."""
assert fibonacci(0) == []
assert fibonacci(1) == [0]
assert fibonacci(5) == [0, 1, 1, 2, 3]
assert fibonacci(10) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Now implement - Copilot knows the exact requirements from tests!
def fibonacci(n: int) -> List[int]:
"""Generate first n Fibonacci numbers."""
# Copilot generates code that passes all tests above# Implement the Builder pattern for creating User objects
# Similar to how we built the PaymentBuilder class in payment.py
class UserBuilder:Why it works: Copilot can reference other files in your workspace.
# Transform raw CSV data through multiple stages:
# 1. Parse CSV into list of dicts
# 2. Filter out rows where 'status' != 'active'
# 3. Convert 'created_at' strings to datetime objects
# 4. Sort by 'priority' (high to low)
# 5. Return top 10 results
def process_customer_data(csv_path: str) -> List[Dict[str, Any]]:def fetch_api_data(url: str, retries: int = 3) -> Dict:
"""
Fetch data from external API with retry logic.
Retry on: ConnectionError, Timeout (with exponential backoff)
Don't retry on: 4xx errors (client errors)
Log all attempts and failures.
Raise custom APIError after all retries exhausted.
"""
# Copilot generates robust error handling# Create a configuration validator that:
# - Loads from YAML file
# - Validates required keys: ['database', 'cache', 'logging']
# - Provides default values for optional keys
# - Raises ConfigError with helpful message if validation fails
# - Returns a typed ConfigObject with dot notation access (config.database.host)
class ConfigLoader:# Do the thing
def process():Problem: Copilot has no context about what "thing" is.
# Return a list of users
def get_user() -> User: # Says list, returns User???Problem: Comment and signature contradict each other.
# In a file with no other code:
def helper(): # What does it help with?Problem: No surrounding code to establish patterns.
def util(x): # Utility for what?
def manager(): # Manages what?
def data(obj): # What kind of data?Problem: Names provide zero semantic meaning.
def process(items): # items is what? List? Dict? Custom class?
return itemsBetter:
def process_orders(items: List[Order]) -> List[ProcessedOrder]:Try these prompts in the demo project to see Copilot in action:
# Calculate the area of a circle given its radius.
# Use π = 3.14159. Return result rounded to 2 decimal places.
def calculate_circle_area(radius: float) -> float:# Validate an email address format.
# Must contain @ symbol, domain name, and valid TLD (.com, .org, etc.)
# Return True if valid, False otherwise.
# Examples: "user@example.com" -> True, "invalid.email" -> False
def is_valid_email(email: str) -> bool:# Parse a log file and extract error messages.
# Each line format: "TIMESTAMP [LEVEL] MESSAGE"
# Return list of tuples: [(timestamp, message), ...]
# Only include lines where LEVEL is "ERROR"
# Skip malformed lines without raising exceptions
def parse_error_logs(log_file_path: str) -> List[Tuple[str, str]]:# Fetch weather data from OpenWeather API for a given city.
# Return dict with: temperature (celsius), humidity (%), description
# Handle API errors gracefully, return None on failure
# Cache results for 5 minutes to avoid excessive API calls
def get_weather(city: str, api_key: str) -> Optional[Dict[str, Any]]:# Write tests first:
def test_merge_sorted_lists():
assert merge_sorted_lists([1, 3, 5], [2, 4, 6]) == [1, 2, 3, 4, 5, 6]
assert merge_sorted_lists([], [1, 2]) == [1, 2]
assert merge_sorted_lists([1], []) == [1]
assert merge_sorted_lists([], []) == []
# Now implement - let Copilot use the tests as specification
def merge_sorted_lists(list1: List[int], list2: List[int]) -> List[int]:
"""Merge two sorted lists into one sorted list."""- Be Specific - Detailed prompts = better code
- Use Type Hints - Help Copilot understand data structures
- Provide Examples - Show input/output expectations
- Establish Patterns - Surrounding code teaches Copilot your style
- Iterate - Refine prompts based on initial suggestions
- Name Intentionally - Function names are prompts themselves
- Document Edge Cases - Error handling and boundaries matter
- Use Tests as Specs - Write tests, let Copilot implement
- GitHub Copilot Best Practices
- OpenAI Prompt Engineering Guide
- Practice with the demo project:
src/calculator.py,src/data_processor.py
- Practice - Complete the exercises above in the demo project
- Experiment - Try different prompt styles for the same function
- Compare - See how prompt changes affect Copilot's suggestions
- Apply - Use these techniques in your real projects
Remember: Prompt engineering is a skill that improves with practice. Start with these patterns and develop your own style over time!