Flake8 plugin that detects the absence of PEP 3107-style function annotations in Python code
npx @tessl/cli install tessl/pypi-flake8-annotations@3.1.0A plugin for Flake8 that detects the absence of PEP 3107-style function annotations in Python code. The plugin provides comprehensive type annotation checking for functions, methods, arguments, and return types with configurable warning levels and advanced features including suppression options for None-returning functions, dummy arguments, and dynamically typed expressions.
pip install flake8-annotationsThe package is primarily used as a flake8 plugin and does not require direct imports in most cases. For programmatic access:
from flake8_annotations import __version__
from flake8_annotations.checker import TypeHintChecker, FORMATTED_ERROR
from flake8_annotations.error_codes import Error, ANN001, ANN201, ANN401
from flake8_annotations.enums import FunctionType, AnnotationType, ClassDecoratorType
from flake8_annotations.ast_walker import Function, Argument, FunctionVisitorAfter installation, the plugin runs automatically with flake8:
# Standard flake8 usage - plugin runs automatically
flake8 myproject/
# Enable opinionated warnings
flake8 --extend-select=ANN401,ANN402 myproject/
# Configure plugin options
flake8 --suppress-none-returning --allow-untyped-defs myproject/# setup.cfg or tox.ini
[flake8]
extend-select = ANN401,ANN402
suppress-none-returning = True
suppress-dummy-args = True
allow-untyped-nested = True
mypy-init-return = True
dispatch-decorators = singledispatch,singledispatchmethod,custom_dispatch
overload-decorators = overload,custom_overload
allow-star-arg-any = True
respect-type-ignore = Trueimport ast
from flake8_annotations.checker import TypeHintChecker
# Parse source code
source_code = '''
def add_numbers(a, b):
return a + b
'''
lines = source_code.splitlines(keepends=True)
tree = ast.parse(source_code)
# Create checker instance
checker = TypeHintChecker(tree, lines)
# Configure options (normally done by flake8)
checker.suppress_none_returning = False
checker.suppress_dummy_args = False
checker.allow_untyped_defs = False
checker.allow_untyped_nested = False
checker.mypy_init_return = False
checker.allow_star_arg_any = False
checker.respect_type_ignore = False
checker.dispatch_decorators = {"singledispatch", "singledispatchmethod"}
checker.overload_decorators = {"overload"}
# Run checks and collect errors
errors = list(checker.run())
for error in errors:
line, col, message, checker_type = error
print(f"{line}:{col} {message}")The plugin follows a modular design with clear separation of concerns and a sophisticated AST analysis pipeline:
FunctionVisitor uses context switching to track nested functions, class methods, and decoratorsFunction object with complete metadata including visibility, decorators, and argument detailsReturnVisitor analyzes return statements to determine if functions only return Nonetyping.overload decorator handling per typing documentationfunctools.singledispatch and similar patterns# type: ignore comments at function and module levelstyping.Any usage warningsThe checker integrates seamlessly with flake8's plugin system, leveraging Python's AST module for comprehensive source code analysis while maintaining high performance through caching and efficient context tracking.
Core package constants and type definitions.
__version__: str
# Package version string (e.g., "3.1.1")
FORMATTED_ERROR: TypeAlias = Tuple[int, int, str, Type[Any]]
# Type alias for flake8 error tuple format: (line_number, column_number, message, checker_type)The package registers itself as a flake8 plugin through setuptools entry points.
# Entry point configuration (automatic via setuptools)
[tool.poetry.plugins."flake8.extension"]
"ANN" = "flake8_annotations.checker:TypeHintChecker"Core flake8 plugin implementation that performs type annotation checking.
class TypeHintChecker:
"""Top level checker for linting the presence of type hints in function definitions."""
name: str # "flake8-annotations"
version: str # Plugin version
def __init__(self, tree: Optional[ast.Module], lines: List[str]):
"""
Initialize checker with AST tree and source lines.
Args:
tree: AST tree (required by flake8 but not used)
lines: Source code lines for analysis
"""
def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
"""
Perform type annotation checks on source code.
Yields:
Tuples of (line_number, column_number, message, checker_type)
"""
@classmethod
def add_options(cls, parser: OptionManager) -> None:
"""Add custom configuration options to flake8."""
@classmethod
def parse_options(cls, options: Namespace) -> None:
"""Parse custom configuration options from flake8."""Configuration Attributes (set by flake8):
# Boolean configuration options
suppress_none_returning: bool # Skip errors for None-returning functions
suppress_dummy_args: bool # Skip errors for dummy arguments named '_'
allow_untyped_defs: bool # Skip all errors for dynamically typed functions
allow_untyped_nested: bool # Skip errors for dynamically typed nested functions
mypy_init_return: bool # Allow omission of return type hint for __init__
allow_star_arg_any: bool # Allow typing.Any for *args and **kwargs
respect_type_ignore: bool # Respect # type: ignore comments
# Set configuration options
dispatch_decorators: Set[str] # Decorators to treat as dispatch decorators
overload_decorators: Set[str] # Decorators to treat as typing.overload decoratorsFunctions for mapping missing annotations to specific error codes.
def classify_error(function: Function, arg: Argument) -> Error:
"""
Classify missing type annotation based on Function & Argument metadata.
Args:
function: Function object containing metadata
arg: Argument object with missing annotation
Returns:
Error object with appropriate error code
"""Classes for parsing and analyzing function definitions from Python AST.
class Argument:
"""Represent a function argument & its metadata."""
argname: str # Name of the argument
lineno: int # Line number where argument is defined
col_offset: int # Column offset where argument is defined
annotation_type: AnnotationType # Type of annotation (from enums)
has_type_annotation: bool # Whether argument has type annotation
has_type_comment: bool # Whether argument has type comment
is_dynamically_typed: bool # Whether argument is typed as Any
@classmethod
def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> "Argument":
"""Create an Argument object from an ast.arguments node."""
def __str__(self) -> str:
"""String representation of argument."""
class Function:
"""Represent a function and its relevant metadata."""
name: str # Function name
lineno: int # Line number where function is defined
col_offset: int # Column offset where function is defined
decorator_list: List[Union[ast.Attribute, ast.Call, ast.Name]] # List of decorators
args: List[Argument] # List of function arguments including return
function_type: FunctionType # Classification of function visibility
is_class_method: bool # Whether function is a class method
class_decorator_type: Optional[ClassDecoratorType] # Type of class decorator if applicable
is_return_annotated: bool # Whether function has return annotation
has_type_comment: bool # Whether function has type comment
has_only_none_returns: bool # Whether function only returns None
is_nested: bool # Whether function is nested
def is_fully_annotated(self) -> bool:
"""Check that all of the function's inputs are type annotated."""
def is_dynamically_typed(self) -> bool:
"""Determine if the function is dynamically typed (completely lacking hints)."""
def get_missed_annotations(self) -> List[Argument]:
"""Provide a list of arguments with missing type annotations."""
def get_annotated_arguments(self) -> List[Argument]:
"""Provide a list of arguments with type annotations."""
def has_decorator(self, check_decorators: Set[str]) -> bool:
"""
Determine whether function is decorated by any of the provided decorators.
Args:
check_decorators: Set of decorator names to check for
Returns:
True if function has any of the specified decorators
"""
@classmethod
def from_function_node(
cls,
node: Union[ast.FunctionDef, ast.AsyncFunctionDef],
lines: List[str],
**kwargs: Any
) -> "Function":
"""Create a Function object from ast.FunctionDef or ast.AsyncFunctionDef nodes."""
@staticmethod
def get_function_type(function_name: str) -> FunctionType:
"""Determine the function's FunctionType from its name."""
@staticmethod
def get_class_decorator_type(
function_node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
) -> Optional[ClassDecoratorType]:
"""Get the class method's decorator type from its function node."""
@staticmethod
def colon_seeker(node: Union[ast.FunctionDef, ast.AsyncFunctionDef], lines: List[str]) -> Tuple[int, int]:
"""
Find the line & column indices of the function definition's closing colon.
Args:
node: AST function node to analyze
lines: Source code lines
Returns:
Tuple of (line_number, column_offset) for the closing colon
"""
class FunctionVisitor(ast.NodeVisitor):
"""AST visitor for walking and describing all contained functions."""
lines: List[str] # Source code lines
function_definitions: List[Function] # Collected function definitions
def __init__(self, lines: List[str]):
"""Initialize visitor with source lines."""
def switch_context(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]) -> None:
"""Context-aware node visitor for tracking function context."""
**Module Type Aliases and Constants:**
```python { .api }
AST_DECORATOR_NODES: TypeAlias = Union[ast.Attribute, ast.Call, ast.Name]
AST_DEF_NODES: TypeAlias = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]
AST_FUNCTION_TYPES: TypeAlias = Union[ast.FunctionDef, ast.AsyncFunctionDef]
AST_ARG_TYPES: Tuple[str, ...] = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg")class ReturnVisitor(ast.NodeVisitor): """Specialized AST visitor for visiting return statements of a function node."""
def __init__(self, parent_node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
"""Initialize with parent function node."""
@property
def has_only_none_returns(self) -> bool:
"""Return True if the parent node only returns None or has no returns."""
def visit_Return(self, node: ast.Return) -> None:
"""Check each Return node to see if it returns anything other than None."""### Error Code Classes
All error codes inherit from a base Error class and represent specific type annotation violations.
```python { .api }
class Error:
"""Base class for linting error codes & relevant metadata."""
argname: str # Argument name where error occurred
lineno: int # Line number of error
col_offset: int # Column offset of error
def __init__(self, message: str):
"""Initialize with error message template."""
@classmethod
def from_argument(cls, argument: Argument) -> "Error":
"""Set error metadata from the input Argument object."""
@classmethod
def from_function(cls, function: Function) -> "Error":
"""Set error metadata from the input Function object."""
def to_flake8(self) -> Tuple[int, int, str, Type[Any]]:
"""Format the Error into flake8-expected tuple format."""Function Argument Error Codes:
class ANN001(Error):
"""Missing type annotation for function argument"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
def to_flake8(self) -> Tuple[int, int, str, Type[Any]]:
"""Custom formatter that includes argument name in message."""
class ANN002(Error):
"""Missing type annotation for *args"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
def to_flake8(self) -> Tuple[int, int, str, Type[Any]]:
"""Custom formatter that includes argument name in message."""
class ANN003(Error):
"""Missing type annotation for **kwargs"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
def to_flake8(self) -> Tuple[int, int, str, Type[Any]]:
"""Custom formatter that includes argument name in message."""Method Argument Error Codes:
class ANN101(Error):
"""Missing type annotation for self in method"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN102(Error):
"""Missing type annotation for cls in classmethod"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...Return Type Error Codes:
class ANN201(Error):
"""Missing return type annotation for public function"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN202(Error):
"""Missing return type annotation for protected function"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN203(Error):
"""Missing return type annotation for secret function"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN204(Error):
"""Missing return type annotation for special method"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN205(Error):
"""Missing return type annotation for staticmethod"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN206(Error):
"""Missing return type annotation for classmethod"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...Opinionated Warning Error Codes:
class ANN401(Error):
"""Dynamically typed expressions (typing.Any) are disallowed"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...
class ANN402(Error):
"""Type comments are disallowed"""
def __init__(self, argname: str, lineno: int, col_offset: int): ...Enumerations for categorizing functions and annotations.
class FunctionType(Enum):
"""Represent Python's function types."""
PUBLIC = auto() # Regular public functions
PROTECTED = auto() # Functions with single underscore prefix
PRIVATE = auto() # Functions with double underscore prefix
SPECIAL = auto() # Functions with double underscore prefix and suffix
class ClassDecoratorType(Enum):
"""Represent Python's built-in class method decorators."""
CLASSMETHOD = auto() # @classmethod decorator
STATICMETHOD = auto() # @staticmethod decorator
class AnnotationType(Enum):
"""Represent the kind of missing type annotation."""
POSONLYARGS = auto() # Positional-only arguments
ARGS = auto() # Regular arguments
VARARG = auto() # *args
KWONLYARGS = auto() # Keyword-only arguments
KWARG = auto() # **kwargs
RETURN = auto() # Return typeThe plugin provides extensive configuration options to customize its behavior:
--suppress-none-returning: bool
# Suppress ANN200-level errors for functions with only None returns
# Default: False
--suppress-dummy-args: bool
# Suppress ANN000-level errors for dummy arguments named '_'
# Default: False
--allow-untyped-defs: bool
# Suppress all errors for dynamically typed functions
# Default: False
--allow-untyped-nested: bool
# Suppress all errors for dynamically typed nested functions
# Default: False
--mypy-init-return: bool
# Allow omission of return type hint for __init__ if at least one argument is annotated
# Default: False
--allow-star-arg-any: bool
# Suppress ANN401 for dynamically typed *args and **kwargs
# Default: False
--respect-type-ignore: bool
# Suppress errors for functions with '# type: ignore' comments
# Default: False--dispatch-decorators: List[str]
# Comma-separated list of decorators to treat as dispatch decorators
# Functions with these decorators skip annotation checks
# Default: ["singledispatch", "singledispatchmethod"]
--overload-decorators: List[str]
# Comma-separated list of decorators to treat as typing.overload decorators
# Implements overload pattern handling per typing documentation
# Default: ["overload"]The plugin provides 14 distinct error codes organized by category:
Function Arguments (ANN001-ANN003):
Method Arguments (ANN101-ANN102):
Return Types (ANN201-ANN206):
Opinionated Warnings (ANN401-ANN402, disabled by default):
The plugin automatically skips annotation checks for functions decorated with dispatch decorators (configurable via --dispatch-decorators), supporting patterns like functools.singledispatch.
Implements proper handling of typing.overload decorator patterns where a series of overload-decorated definitions must be followed by exactly one non-overload-decorated definition.
When --respect-type-ignore is enabled, the plugin respects # type: ignore comments at both function and module levels, including mypy-style ignore patterns.
The plugin can detect and optionally suppress warnings for dynamically typed expressions (typing.Any) with configurable patterns for different annotation contexts.