Adding type hints to untyped Python code is one of the most time-consuming tasks when modernizing legacy codebases. Both Cursor and GitHub Copilot offer AI-assisted type annotation features, but they approach the problem differently. This comparison examines how each tool performs when you need to add type hints to existing Python code.
Legacy Python code often lacks type annotations, making it harder to maintain, refactor, and debug. Manually adding type hints to a large codebase can take weeks or months. AI coding assistants promise to automate this process, but the quality of their type inferences varies significantly.
Cursor leverages Claude (and optionally GPT) for code generation and modification. When you ask Cursor to add type hints, it analyzes the function’s behavior, variable usage, and return values to infer appropriate types.
# Before type hints (untyped)
def process_user_data(user_data):
results = []
for item in user_data:
if item.get("active"):
results.append({
"id": item["id"],
"name": item["name"],
"email": item["email"]
})
return results
When you ask Cursor to add type hints, it typically produces:
from typing import TypedDict, List
class UserData(TypedDict):
id: int
name: str
email: str
active: bool
class ProcessedUser(TypedDict):
id: int
name: str
email: str
def process_user_data(user_data: List[UserData]) -> List[ProcessedUser]:
results: List[ProcessedUser] = []
for item in user_data:
if item.get("active"):
results.append({
"id": item["id"],
"name": item["name"],
"email": item["email"]
})
return results
Cursor often suggests using TypedDict for dictionary structures and correctly infers List types. However, it sometimes over-uses Any when the context is unclear.
GitHub Copilot uses OpenAI’s models and integrates directly into VS Code. Its type hint generation is more conservative, often defaulting to basic types without creating custom type definitions.
# Before type hints
def calculate_metrics(sales_data, discount_rate):
total = 0
for sale in sales_data:
total += sale['amount'] * (1 - discount_rate)
return {
'net_total': total,
'discount_amount': sum(s['amount'] for s in sales_data) * discount_rate
}
Copilot might suggest:
from typing import List, Dict, Any
def calculate_metrics(sales_data: List[Dict[str, Any]], discount_rate: float) -> Dict[str, float]:
total = 0.0
for sale in sales_data:
total += sale['amount'] * (1 - discount_rate)
return {
'net_total': total,
'discount_amount': sum(s['amount'] for s in sales_data) * discount_rate
}
Copilot tends to use Dict[str, Any] more frequently, which provides less type safety than custom TypedDicts.
| Aspect | Cursor | GitHub Copilot |
|---|---|---|
| Type specificity | Higher (TypedDict, custom types) | Lower (Any, generic Dict) |
| Context awareness | Analyzes broader file context | Focuses on local function |
| Union types | Infers Optional, Union correctly | Sometimes misses Optional |
| Custom types | Creates TypedDict automatically | Rarely suggests custom types |
| Performance on large files | Processes faster with Claude | Slower for complex inference |
# Python function needing type hints
def transform_records(records, filter_key, transform_fn):
return [transform_fn(r) for r in records if r.get(filter_key)]
In Cursor, you would use the Composer to ask:
Add proper type hints to this function. Use List[T] syntax and infer types from usage.
Cursor typically produces:
from typing import TypeVar, Callable, List, Dict, Any, Optional
T = TypeVar('T')
R = TypeVar('R')
def transform_records(
records: List[Dict[str, Any]],
filter_key: str,
transform_fn: Callable[[Dict[str, Any]], R]
) -> List[R]:
return [transform_fn(r) for r in records if r.get(filter_key)]
Copilot works differently—it provides inline suggestions as you type. For type hints, you often need to trigger suggestions explicitly:
# Type the function signature with basic types
def transform_records(records, filter_key, transform_fn) -> list:
# Copilot might suggest completing with types
pass
Copilot’s suggestions are less comprehensive than Cursor’s explicit type hint generation.
# Untyped function with complex logic
def aggregate_orders(orders, customer_id=None):
if customer_id:
orders = [o for o in orders if o['customer_id'] == customer_id]
by_status = {}
for order in orders:
status = order['status']
if status not in by_status:
by_status[status] = {'count': 0, 'total': 0}
by_status[status]['count'] += 1
by_status[status]['total'] += order['total']
return by_status
Cursor’s output:
from typing import Dict, List, Optional, Literal
Order = Dict[str, Any] # You could define a TypedDict here for better safety
OrderStatus = Literal['pending', 'processing', 'shipped', 'delivered', 'cancelled']
def aggregate_orders(
orders: List[Order],
customer_id: Optional[int] = None
) -> Dict[OrderStatus, Dict[str, float]]:
if customer_id:
orders = [o for o in orders if o['customer_id'] == customer_id]
by_status: Dict[OrderStatus, Dict[str, float]] = {}
for order in orders:
status: OrderStatus = order['status']
if status not in by_status:
by_status[status] = {'count': 0, 'total': 0.0}
by_status[status]['count'] += 1
by_status[status]['total'] += order['total']
return by_status
Copilot’s output:
from typing import Dict, List, Any, Optional
def aggregate_orders(orders: List[Dict[str, Any]], customer_id: Optional[int] = None) -> Dict[str, Dict[str, float]]:
if customer_id:
orders = [o for o in orders if o['customer_id'] == customer_id]
by_status: Dict[str, Dict[str, float]] = {}
for order in orders:
status = order['status']
if status not in by_status:
by_status[status] = {'count': 0, 'total': 0.0}
by_status[status]['count'] += 1
by_status[status]['total'] += order['total']
return by_status
Cursor produces more specific types (Literal for order status), while Copilot uses generic string keys.
When working with database results, both tools handle type hints differently:
# Mock database fetch function
def fetch_users(conn, active_only=True):
cursor = conn.cursor()
query = "SELECT id, name, email, created_at FROM users"
if active_only:
query += " WHERE active = 1"
cursor.execute(query)
return cursor.fetchall()
Cursor’s type hint suggestion:
from typing import List, Tuple, Optional
from datetime import datetime
import MySQL.connections
def fetch_users(conn: MySQL.connections.Connection, active_only: bool = True) -> List[Tuple[int, str, str, datetime]]:
cursor = conn.cursor()
query = "SELECT id, name, email, created_at FROM users"
if active_only:
query += " WHERE active = 1"
cursor.execute(query)
return cursor.fetchall()
Copilot’s suggestion:
from typing import List, Any
def fetch_users(conn: Any, active_only: bool = True) -> List[Any]:
cursor = conn.cursor()
query = "SELECT id, name, email, created_at FROM users"
if active_only:
query += " WHERE active = 1"
cursor.execute(query)
return cursor.fetchall()
Cursor correctly identifies the return type as a tuple with specific types, while Copilot defaults to List[Any].
After generating type hints, run mypy to verify correctness:
# Install mypy
pip install mypy
# Run type checking
mypy your_module.py
Both tools sometimes generate type hints that fail mypy strict mode checks. Cursor tends to produce more mypy-friendly code, but you should always verify with:
# Example that needs # type: ignore comments
result = json.loads(user_input) # mypy might complain here
Choose Cursor if you:
Choose GitHub Copilot if you:
For adding type hints to untyped Python code, Cursor generally produces higher-quality type annotations with better specificity. It creates TypedDict definitions, correctly infers Optional types, and generates more mypy-friendly code. GitHub Copilot is faster for simple type hints but often defaults to Any, which provides less type safety.
If you’re serious about adding types to a Python codebase, Cursor’s more comprehensive approach saves time in the long run—even though you might need to review and refine its suggestions.
Built by theluckystrike — More at zovo.one