Best Practices

Python Type Hints in Code Review: MyPy Integration and Best Practices

Tony Dong
June 23, 2025
11 min read
Share:
Featured image for: Python Type Hints in Code Review: MyPy Integration and Best Practices

Python's type hint system, introduced in Python 3.5, has revolutionized how we write and review Python code. Combined with MyPy's static type checking, type hints catch bugs before they reach production and make code reviews more effective. This guide shows you how to leverage type hints in your code review process for maximum impact.

Key Benefits of Type Hints in Code Review

  • Catch 15% more bugs: Static analysis finds errors that manual review misses
  • Reduce review time: Reviewers focus on logic instead of guessing parameter types
  • Improve documentation: Code becomes self-documenting with clear interfaces
  • Enable better tooling: IDEs provide superior autocomplete and error detection

Why Type Hints Matter in Code Review

Traditional Python code review often involves reviewers mentally tracking variable types, guessing function parameters, and trying to understand complex data flows. Type hints eliminate this cognitive overhead, allowing reviewers to focus on business logic, edge cases, and architectural decisions.

Before and After: Code Review Impact

Without Type Hints
  • • "What type does this function return?"
  • • "Can this parameter be None?"
  • • "What's the structure of this dict?"
  • • "Does this work with strings or bytes?"
  • • Manual testing required to verify types
With Type Hints
  • • Clear function signatures document behavior
  • • Optional types explicitly handled
  • • Complex data structures well-defined
  • • String/bytes distinction clear
  • • MyPy catches type errors automatically

Essential Type Hint Patterns for Code Review

1. Function Signatures

Always include type hints for function parameters and return values. This is the foundation of effective type-checked code:

❌ Hard to review:
def process_user_data(data, include_inactive=False):
    if include_inactive:
        return [user for user in data if user.get('status')]
    return [user for user in data if user.get('status') == 'active']
✅ Clear and reviewable:
from typing import List, Dict, Any

def process_user_data(
    data: List[Dict[str, Any]], 
    include_inactive: bool = False
) -> List[Dict[str, Any]]:
    if include_inactive:
        return [user for user in data if user.get('status')]
    return [user for user in data if user.get('status') == 'active']

2. Optional Types and None Handling

Make None-able values explicit to prevent AttributeError bugs:

❌ Unclear when None is possible:
def get_user_email(user_id):
    user = database.get_user(user_id)
    return user.email  # What if user is None?
✅ Explicit None handling:
from typing import Optional

def get_user_email(user_id: int) -> Optional[str]:
    user: Optional[User] = database.get_user(user_id)
    if user is None:
        return None
    return user.email

3. Complex Data Structures

Use TypedDict, dataclasses, or Pydantic models for complex data structures instead of generic dictionaries:

❌ Unclear data structure:
def calculate_order_total(order):
    return order['items_total'] + order['tax'] + order.get('shipping', 0)
✅ Well-defined structure:
from typing import TypedDict
from decimal import Decimal

class OrderData(TypedDict):
    items_total: Decimal
    tax: Decimal
    shipping: Decimal

def calculate_order_total(order: OrderData) -> Decimal:
    return order['items_total'] + order['tax'] + order['shipping']

MyPy Configuration for Code Review

Essential mypy.ini Settings

Configure MyPy to catch the most common issues without being overly strict for legacy code:

[mypy]
python_version = 3.9
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True

# Start strict, relax for legacy code
[mypy-legacy.*]
disallow_untyped_defs = False

# Third-party libraries without stubs
[mypy-some_third_party_lib.*]
ignore_missing_imports = True

Gradual Adoption Strategy

Implement type checking incrementally to avoid overwhelming your team:

4-Phase Adoption Plan

Phase 1: Add type hints to new functions and public APIs
Phase 2: Enable MyPy on new modules with strict settings
Phase 3: Gradually add types to existing critical modules
Phase 4: Enforce type checking on entire codebase

Code Review Checklist for Python Type Hints

Function-Level Checks

Module-Level Checks

Advanced Type Hint Patterns

1. Protocol for Duck Typing

Use Protocol to define interfaces for dependency injection and testing:

from typing import Protocol

class Emailer(Protocol):
    def send_email(self, to: str, subject: str, body: str) -> bool:
        ...

class UserService:
    def __init__(self, emailer: Emailer) -> None:
        self.emailer = emailer
    
    def notify_user(self, user_id: str, message: str) -> bool:
        # Works with any object that implements send_email
        return self.emailer.send_email(
            to=f"{user_id}@example.com",
            subject="Notification", 
            body=message
        )

2. Generic Types for Reusability

Create reusable, type-safe components with generics:

from typing import TypeVar, Generic, List, Optional

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: List[T] = []
    
    def add(self, item: T) -> None:
        self._items.append(item)
    
    def get_by_id(self, id: int) -> Optional[T]:
        if 0 <= id < len(self._items):
            return self._items[id]
        return None

# Usage with specific types
user_repo = Repository[User]()  # Type checker knows this stores Users
order_repo = Repository[Order]()  # This stores Orders

3. Literal Types for Constants

Use Literal types to restrict values to specific constants:

from typing import Literal

Status = Literal['pending', 'approved', 'rejected']
LogLevel = Literal['DEBUG', 'INFO', 'WARNING', 'ERROR']

def update_order_status(order_id: str, status: Status) -> bool:
    # MyPy will error if status isn't one of the literal values
    return database.update_order(order_id, status)

def log_message(message: str, level: LogLevel = 'INFO') -> None:
    # Type checker ensures only valid log levels are used
    logger.log(level, message)

Common Type Hint Mistakes in Code Review

1. Using Any Too Liberally

❌ Avoid Any without justification

from typing import Any

def process_data(data: Any) -> Any:  # Defeats the purpose of type hints
    return data.do_something()

✅ Be specific about types

from typing import Union, Dict, List

def process_data(data: Union[Dict[str, str], List[str]]) -> str:
    if isinstance(data, dict):
        return ','.join(data.values())
    return ','.join(data)

2. Forgetting to Handle None

❌ Missing None checks

def get_user_name(user: Optional[User]) -> str:
    return user.name  # Potential AttributeError if user is None

✅ Explicit None handling

def get_user_name(user: Optional[User]) -> str:
    if user is None:
        return "Anonymous"
    return user.name

3. Incorrect Generic Usage

❌ Bare containers without element types

def process_items(items: list) -> dict:  # What kind of list? What dict structure?
    return {'{item.id: item for item in items}'}

✅ Specific generic types

from typing import List, Dict

def process_items(items: List[Item]) -> Dict[str, Item]:
    return {'{item.id: item for item in items}'}

Integrating MyPy into Your Review Process

1. Pre-commit Hooks

Automatically run MyPy before commits to catch type errors early:

.pre-commit-config.yaml:
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.991
    hooks:
      - id: mypy
        additional_dependencies: [types-requests, types-PyYAML]
        exclude: ^(docs/|scripts/)

2. CI/CD Integration

Add MyPy checks to your continuous integration pipeline:

GitHub Actions example:
name: Type Check
on: [push, pull_request]

jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    - run: pip install mypy types-requests
    - run: mypy src/
    - run: mypy --strict new_modules/

3. IDE Integration

Configure your team's IDEs to show MyPy errors in real-time. Popular options include:

  • VS Code: Python extension with MyPy linting enabled
  • PyCharm: Built-in type checking with configurable strictness
  • Vim/Neovim: ALE or coc-pyright plugins
  • Sublime Text: LSP-pyright plugin

Measuring Type Hint Adoption

Coverage Metrics

Track type hint adoption across your codebase:

MyPy Coverage Report

# Generate coverage report
mypy --html-report mypy-report/ src/

# Check specific coverage percentage
mypy --linecount-report linecount-report/ src/

Quality Metrics

  • Type coverage percentage: Aim for 80%+ on new code
  • MyPy error count: Track reduction over time
  • # type: ignore usage: Should decrease as code improves
  • Runtime type errors: AttributeError, TypeError should decrease

Frequently Asked Questions

Should we type hint internal/private functions?

Yes, type hints help with maintainability and debugging even for internal functions. The only exception might be very short, obvious utility functions.

How do we handle third-party libraries without type stubs?

Use `# type: ignore` for import lines, or create your own stub files for frequently used libraries. Check if stubs exist in the typeshed project first.

What about performance impact of type hints?

Type hints have zero runtime performance impact in Python. They're stored in `__annotations__` and ignored during execution unless explicitly accessed.

Can we use type hints with older Python versions?

Yes! Use `from __future__ import annotations` for Python 3.7+ or string annotations for older versions. MyPy supports Python 3.6+.

Advanced MyPy Features

1. Stub Files for Gradual Typing

Create .pyi stub files to add types to existing code without modifying the source:

legacy_module.pyi:
from typing import List, Dict, Any

def process_data(data: List[Dict[str, Any]]) -> bool: ...
def get_config() -> Dict[str, str]: ...

class LegacyProcessor:
    def __init__(self, config: Dict[str, Any]) -> None: ...
    def run(self) -> List[str]: ...

2. Plugin System

Use MyPy plugins for framework-specific type checking:

  • Django: django-stubs for model and form type checking
  • SQLAlchemy: sqlalchemy2-stubs for ORM type safety
  • Pydantic: Built-in MyPy plugin for model validation

3. Custom Type Checkers

Write custom MyPy plugins for domain-specific type checking needs, such as validating business rules or API contracts.

Supercharge Your Python Code Reviews

Propel's AI understands Python type hints and can automatically catch type-related issues in your code reviews. Get intelligent feedback that complements MyPy's static analysis.

Conclusion

Type hints and MyPy transform Python from a dynamically typed language into one with optional static typing—giving you the best of both worlds. By incorporating type checking into your code review process, you'll catch more bugs, improve code documentation, and make your codebase more maintainable.

Start with new code, gradually expand coverage, and watch as your code reviews become more focused on business logic rather than basic type safety. Your future self (and your teammates) will thank you.

Quick Start Checklist

  • ✓ Install MyPy: pip install mypy
  • ✓ Add type hints to your next function
  • ✓ Run mypy your_file.py
  • ✓ Configure mypy.ini with basic settings
  • ✓ Add MyPy to your CI pipeline

Enhance Your Python Code Reviews

See how Propel's AI-powered reviews help catch Python type errors and enforce best practices automatically.

Explore More

Propel AI Code Review Platform LogoPROPEL

The AI Tech Lead that reviews, fixes, and guides your development team.

SOC 2 Type II Compliance Badge - Propel meets high security standards

Company

© 2025 Propel Platform, Inc. All rights reserved.