Type Hints Best Practices with mypy Strict Mode
Core Principles
All Python code in Vibekit MUST pass mypy in strict mode with zero errors. Type hints are not optional—they are a fundamental requirement for code quality and maintainability.
Explicit Return Type Annotations
Every exported function must declare its return type:
# ✅ REQUIRED: Explicit return types for all exported functions
def calculate_total(items: list[float]) -> float:
"""Calculate sum of items."""
return sum(items)
def get_user_by_id(user_id: int) -> User | None:
"""Retrieve user or None if not found."""
result = database.query(user_id)
return result
def process_data(data: str) -> None:
"""Process data with no return value."""
print(data.upper())
# ❌ FORBIDDEN: Implicit Any return type
def bad_function(x): # Returns Any - rejected by strict mypy
return x * 2
# ❌ FORBIDDEN: No return type annotation
def also_bad(x: int): # Missing return type
return x * 2
# ✅ GOOD: Explicit return type
def good_function(x: int) -> int:
return x * 2
Using Built-in Generic Types
Use built-in types for generic annotations (Python 3.9+):
# ✅ REQUIRED: Use built-in generics
def process_items(items: list[str]) -> dict[str, int]:
"""Process items and return counts."""
return {item: len(item) for item in items}
def merge_configs(
config1: dict[str, any],
config2: dict[str, any]
) -> dict[str, any]:
"""Merge two configuration dictionaries."""
return {**config1, **config2}
def get_first_item(items: list[int]) -> int | None:
"""Get first item or None."""
return items[0] if items else None
# ❌ FORBIDDEN: Legacy typing imports
from typing import List, Dict, Optional
def old_style(items: List[str]) -> Dict[str, int]: # Don't use these!
pass
Type Aliases for Complex Types
Use the type statement for readable type aliases:
# ✅ REQUIRED: Use type statement for type aliases (Python 3.12+)
type UserId = int
type UserData = dict[str, str | int | bool]
type ValidationResult = tuple[bool, str]
def validate_user(user_id: UserId, data: UserData) -> ValidationResult:
"""Validate user data."""
if user_id < 0:
return False, "Invalid user ID"
return True, "Valid"
# For Python 3.9-3.11, use TypeAlias
from typing import TypeAlias
UserIdLegacy: TypeAlias = int
UserDataLegacy: TypeAlias = dict[str, str | int | bool]
# ❌ FORBIDDEN: Inline complex types
def process(
data: dict[str, str | int | bool | list[dict[str, any]]] # Too complex!
) -> tuple[bool, str, dict[str, any]]:
pass
# ✅ GOOD: Named type alias
type ComplexData = dict[str, str | int | bool | list[dict[str, any]]]
type ProcessResult = tuple[bool, str, dict[str, any]]
def process(data: ComplexData) -> ProcessResult:
pass
Generic Types and Classes
Create reusable generic functions and classes:
from typing import TypeVar, Generic
# ✅ REQUIRED: Use TypeVar for generic functions
T = TypeVar('T')
def get_first(items: list[T]) -> T | None:
"""Get first item from list, preserving type."""
return items[0] if items else None
# Usage preserves types
first_int: int | None = get_first([1, 2, 3]) # Type: int | None
first_str: str | None = get_first(["a", "b"]) # Type: str | None
# Generic class
class Stack(Generic[T]):
"""Generic stack implementation."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
"""Add item to stack."""
self._items.append(item)
def pop(self) -> T | None:
"""Remove and return top item."""
return self._items.pop() if self._items else None
# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
Protocol for Structural Typing
Use Protocol to define interfaces without inheritance:
from typing import Protocol
# ✅ REQUIRED: Use Protocol for structural subtyping
class Closable(Protocol):
"""Protocol for objects that can be closed."""
def close(self) -> None:
"""Close the resource."""
...
class Serializable(Protocol):
"""Protocol for serializable objects."""
def to_dict(self) -> dict[str, any]:
"""Convert to dictionary."""
...
def cleanup_resource(resource: Closable) -> None:
"""Clean up any closable resource."""
resource.close()
# Any class with a close() method satisfies the protocol
class FileHandler:
def close(self) -> None:
print("Closing file")
class DatabaseConnection:
def close(self) -> None:
print("Closing connection")
# Both work with cleanup_resource
cleanup_resource(FileHandler()) # ✅ Works
cleanup_resource(DatabaseConnection()) # ✅ Works
mypy Strict Mode Configuration
Configure mypy for maximum type safety:
# pyproject.toml [tool.mypy] python_version = "3.13" strict = true warn_return_any = true warn_unused_configs = true disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_unreachable = true strict_equality = true # For gradual typing of legacy code [[tool.mypy.overrides]] module = "legacy.module.*" disallow_untyped_defs = false # Temporarily disable for legacy
Handling Third-Party Libraries Without Stubs
Deal with untyped third-party code:
# ❌ BAD: Global ignore_missing_imports
[tool.mypy]
ignore_missing_imports = true # Don't do this!
# ✅ GOOD: Install type stubs when available
# pip install types-requests types-redis
# ✅ GOOD: Per-module ignore when stubs don't exist
[[tool.mypy.overrides]]
module = "untyped_library.*"
ignore_missing_imports = true
# ✅ GOOD: Create custom stub file
# untyped_library.pyi
def some_function(arg: str) -> int: ...
class SomeClass:
def method(self) -> None: ...
Specific Error Code Ignores
When type ignore is necessary, use specific error codes:
# ❌ FORBIDDEN: Bare type ignore result = legacy_function() # type: ignore # Too broad! # ✅ REQUIRED: Specific error code result = legacy_function() # type: ignore[no-any-return] # ✅ REQUIRED: Inline type annotation when possible result: dict[str, any] = legacy_function() # Better than ignore # Common error codes: # [no-any-return] - Function returns Any # [attr-defined] - Attribute not defined # [arg-type] - Wrong argument type # [assignment] - Type mismatch in assignment # [override] - Override signature mismatch # [misc] - Miscellaneous errors
Union Types with | Operator
Use the modern union syntax:
# ✅ REQUIRED: Use | for union types
def process_value(value: str | int | float) -> str:
"""Handle multiple types."""
return str(value)
def find_user(query: str) -> User | None:
"""Return User or None if not found."""
result = database.find(query)
return result
# ✅ REQUIRED: None always comes last in unions
def get_config(key: str) -> str | int | None:
"""Get configuration value."""
return config.get(key)
# ❌ FORBIDDEN: Legacy Union and Optional
from typing import Union, Optional
def old_style(value: Union[str, int]) -> Optional[User]: # Don't use
pass
# ✅ GOOD: Modern style
def new_style(value: str | int) -> User | None:
pass
Literal Types for Specific Values
Use Literal for exact value matching:
from typing import Literal
# ✅ REQUIRED: Use Literal for specific string/int values
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
"""Set logging level to specific value."""
logging.setLevel(level)
def get_status_code(status: Literal[200, 404, 500]) -> str:
"""Get status message for specific codes."""
messages = {200: "OK", 404: "Not Found", 500: "Error"}
return messages[status]
# Usage
set_log_level("DEBUG") # ✅ OK
set_log_level("INFO") # ✅ OK
set_log_level("TRACE") # ❌ Type error: "TRACE" not in Literal
TypedDict for Structured Dictionaries
Define exact dictionary structures:
from typing import TypedDict
# ✅ REQUIRED: Use TypedDict for structured dicts
class UserDict(TypedDict):
"""Typed dictionary for user data."""
id: int
email: str
username: str
is_active: bool
def create_user(data: UserDict) -> User:
"""Create user from typed dict."""
return User(
id=data["id"],
email=data["email"],
username=data["username"],
is_active=data["is_active"]
)
# Usage
user_data: UserDict = {
"id": 1,
"email": "user@example.com",
"username": "user",
"is_active": True
}
user = create_user(user_data)
# ❌ Type error: missing required key
bad_data: UserDict = {"id": 1, "email": "user@example.com"} # Missing username
# Optional keys
class PartialUserDict(TypedDict, total=False):
"""User dict with optional fields."""
id: int # Still required (would need NotRequired for truly optional)
metadata: dict[str, str] # Optional
Any vs object
Use object instead of Any when possible:
from typing import Any
# ❌ BAD: Any disables type checking
def log_anything(value: Any) -> None:
print(str(value)) # No type safety
# ✅ GOOD: object is more precise
def log_value(value: object) -> None:
"""Log any object by converting to string."""
print(str(value)) # object has __str__, so this is safe
# Any should only be used when truly dynamic
def dynamic_dispatch(operation: str, *args: Any) -> Any:
"""Truly dynamic operation dispatcher."""
return getattr(operations, operation)(*args)
Anti-Patterns to Avoid
❌ Missing Return Type Annotations
# BAD: Implicit Any return
def calculate(x: int): # Missing return type
return x * 2
# GOOD: Explicit return type
def calculate(x: int) -> int:
return x * 2
❌ Using Legacy typing Imports
# BAD
from typing import List, Dict, Optional, Union
def process(items: List[str]) -> Optional[Dict[str, int]]:
pass
# GOOD
def process(items: list[str]) -> dict[str, int] | None:
pass
❌ Bare type: ignore Comments
# BAD: Too broad result = untypedFunction() # type: ignore # GOOD: Specific error code result = untypedFunction() # type: ignore[no-any-return]
❌ Global ignore_missing_imports
# BAD: pyproject.toml [tool.mypy] ignore_missing_imports = true # Disables too many checks # GOOD: Per-module overrides [[tool.mypy.overrides]] module = "specific_untyped_lib.*" ignore_missing_imports = true
❌ Any Instead of object
# BAD: Disables type checking
def print_value(value: Any) -> None:
print(value.upper()) # No type safety!
# GOOD: Use specific type
def print_value(value: str) -> None:
print(value.upper())
# GOOD: Use object if truly any type
def print_value(value: object) -> None:
print(str(value)) # Safe conversion
Gradual Typing Strategy
Adopt strict typing incrementally for legacy code:
# pyproject.toml - Gradual typing approach [tool.mypy] # Global strict mode strict = true # Disable strict for legacy modules [[tool.mypy.overrides]] module = "legacy.old_module.*" disallow_untyped_defs = false disallow_incomplete_defs = false # Re-enable strict for new code in legacy area [[tool.mypy.overrides]] module = "legacy.new_feature.*" disallow_untyped_defs = true # Strategy: # 1. Enable strict=true globally # 2. Disable specific checks for legacy code # 3. Gradually remove overrides as code is updated
Running mypy in CI/CD
Ensure type safety in continuous integration:
# Run mypy with strict mode mypy src/ # Fail CI if mypy finds errors # (exit code non-zero on errors) # Generate type coverage report mypy --html-report ./mypy-report src/ # Check specific files only mypy src/module.py src/another.py # Show error context mypy --show-error-context src/
When to Use This Skill
Activate this skill when:
- •Writing new Python modules with type hints
- •Configuring mypy for a project
- •Debugging type errors
- •Implementing generic types or protocols
- •Migrating untyped code to typed
- •Setting up CI/CD type checking
Integration Points
This skill is a required dependency for:
- •All Python-based Vibekit plugins
- •
pydantic-v2-strict- Type hints are foundational for Pydantic - •
python-code-quality-automation- mypy is part of quality gates
Related Resources
For additional information:
- •mypy Documentation: https://mypy.readthedocs.io/
- •Python typing Documentation: https://docs.python.org/3/library/typing.html
- •Type Hints Best Practices: https://typing.python.org/en/latest/reference/best_practices.html