Skip to content

← Interfaces with Protocols and ABCs step 17 of 26

Medium Framework

Find Where Any Leaks into Function Signatures

You are about to put a teammate's module under mypy --strict. Strict mode already rejects a parameter with no annotation and a bare list. It accepts dict[str, Any] and Callable[..., int], and both let unchecked values in. Any is a type that turns checking off. mypy accepts any operation on an Any value and lets it go anywhere. Callable[..., int] accepts a function that takes any arguments at all.

Say a signature leaks when it lets an unchecked value in. Write an auditor that lists the public functions whose signatures leak. It reads the source with Python's ast module, so it never imports or runs the code.

source = '''
from typing import Any

def load(path: str) -> dict[str, Any]: ...
def save(path, data: dict[str, str]) -> None: ...
def count(items: list) -> int: ...
def size(items: list[int]) -> int: ...
def _helper(x): ...
'''
solve(source)  # ["count", "load", "save"]

What to write

Fill in the four functions in the starter.

  • tail_name(node) returns node.id for an ast.Name, node.attr for an ast.Attribute, and None for anything else. Any and typing.Any both give "Any".
  • annotation_leaks(annotation) returns True if any node inside the annotation, at any depth, matches one of the three rules below. So dict[str, list[Any]] leaks and dict[str, list[int]] does not.
  • function_leaks(fn) returns True if a parameter or the return type has no annotation, or if any annotation leaks. Check positional-only, ordinary and keyword-only parameters, plus *args and **kwargs. A parameter with a default still needs an annotation, and so does the return type of a function that returns nothing.
  • solve(source) parses the source with ast.parse and returns the names of the leaking functions in sorted order.

An annotation leaks when it contains any of these:

  • A name whose tail_name is Any.
  • A Callable subscript whose first argument is the literal ..., as in Callable[..., int]. A tuple[int, ...] does not count.
  • A name from GENERIC_NAMES that is not subscripted. list leaks and list[int] does not. In list[list], the inner list leaks.

Which functions to check

  • Check only def and async def statements directly in the module body. Skip methods, nested functions, and functions inside an if or try.
  • Skip names that start with an underscore.
  • If two definitions share a name and both leak, list the name twice.
  • An empty source gives []. Let a SyntaxError propagate.

Match by name only

Treat every Any alike, whichever module it comes from, and keep GENERIC_NAMES exactly as the starter gives it.

Leave string annotations alone. To ast, the "Any" in x: "Any" is a string constant, so it does not leak.