CQ009: TypeHintCoverage
Overview
| Property | Value |
|---|---|
| ID | CQ009 |
| Name | TypeHintCoverage |
| Group | code-quality |
| Severity | NOTE |
Description
Measures the percentage of public functions that have complete type annotations.
Type hints provide:
- Better documentation: Parameters and return types are self-documenting
- IDE support: Enables autocomplete, refactoring, and inline type information
- Static analysis: Allows tools like mypy to catch type errors before runtime
- Code quality: Forces explicit thinking about data types
This check uses AST parsing to analyze functions and verify they have type annotations.
What it checks
The check scans all Python files in the package (excluding private modules and hidden directories), identifies public functions and methods, and calculates type hint coverage:
- PASSED: Coverage is at or above the threshold (default: 80%)
- FAILED: Coverage is below the threshold
- NOT_APPLICABLE: No package found or no public functions found
What counts as fully typed?
A function is considered fully typed if:
- All parameters (except
selfandcls) have type annotations - The return type is annotated
# Fully typed - counts as covered
def greet(name: str) -> str:
return f"Hello, {name}"
def add(a: int, b: int) -> int:
return a + b
class Calculator:
def multiply(self, x: float, y: float) -> float:
# self doesn't need annotation
return x * y
@classmethod
def from_string(cls, value: str) -> "Calculator":
# cls doesn't need annotation
return cls()
# Not fully typed - missing annotations
def untyped(x): # Missing param and return type
return x
def partial(x: int, y): # Missing annotation for y
return x + y
def no_return(x: int): # Missing return type
return xWhat’s public vs private?
A function is considered public if its name does NOT start with an underscore (_):
# Public - counted
def public_function(x: int) -> int:
return x
# Private - not counted
def _private_helper(x):
return xHow to fix
Add type annotations to functions
# Before
def calculate_price(quantity, unit_price, discount):
return quantity * unit_price * (1 - discount)
# After
def calculate_price(quantity: int, unit_price: float, discount: float) -> float:
return quantity * unit_price * (1 - discount)Add return type annotations
# Before
def get_user_name(user_id: int):
return database.get_name(user_id)
# After
def get_user_name(user_id: int) -> str:
return database.get_name(user_id)Use Optional for nullable returns
from typing import Optional
def find_user(user_id: int) -> Optional[User]:
"""Returns None if user not found."""
return database.get_user(user_id)Handle complex types
from typing import List, Dict, Tuple, Callable
def process_items(items: List[str]) -> Dict[str, int]:
return {item: len(item) for item in items}
def get_handler(name: str) -> Callable[[int], str]:
return handlers[name]Make truly internal functions private
If a function is internal and doesn’t need type hints:
def _internal_helper(data):
# Internal helper, no type hints required
...Configuration
Adjust coverage threshold
[tool.pycmdcheck.checks.CQ009]
coverage_threshold = 90 # Require 90% coverageInclude private modules
[tool.pycmdcheck.checks.CQ009]
include_private_modules = true # Check _internal.py files tooSkip this check
[tool.pycmdcheck]
skip = ["CQ009"]CLI
pycmdcheck --skip CQ009