This document describes the rules and approach we will use to make the codebase type-safe with ty.
It uses a small set of examples from the ty docs to anchor our conventions, then lays out a
reproducible plan for getting to a clean ty check.
Sources (examples below are based on these pages):
Use rule-level settings to gradually tighten checks:
ty check \
--warn unused-ignore-comment \
--ignore redundant-cast \
--error possibly-missing-attribute \
--error possibly-missing-importEquivalent pyproject.toml:
[tool.ty.rules]
unused-ignore-comment = "warn"
redundant-cast = "ignore"
possibly-missing-attribute = "error"
possibly-missing-import = "error"Prefer narrow, rule-specific suppressions:
sum_three_numbers("one", 5) # ty: ignore[missing-argument, invalid-argument-type]Multi-line suppression can go on the first or last line of the violation:
sum_three_numbers( # ty: ignore[missing-argument]
3,
2
)Use @no_type_check only when a function is intentionally dynamic:
from typing import no_type_check
@no_type_check
def main():
sum_three_numbers(1, 2) # no error for the missing argumentThese rules are what we will follow as we make the codebase type-safe. They are aligned with Python 3.13+ and current typing guidance.
- Use builtin generics and PEP 604 unions:
list[str],dict[str, int],X | None. - For unused features or parameters, check call sites/usage before removal and confirm with the user before deleting behavior, even if it appears unused.
- For command parsing, prefer a discriminated
TypedDictunion (e.g.,kindfield) over ad-hoc nested dicts. If the command surface is shared across modules or grows over time, keep the payload types in a small dedicated module; otherwise colocate them with the parser. - For dynamic/optional collection attributes, narrow to
collections.abc.CollectionorSizedwhen you only needlen()or membership; avoid materializing unless multiple passes or sorting is required. Excludestr/byteswhen treating a value as a general collection. - Use
getattronly for truly dynamic attributes (plugin/duck-typed objects); immediately narrow the result withisinstance/helper guards and avoid masking real missing attributes. - For core agent types with concrete classes, prefer
isinstancenarrowing against those classes overgetattr/duck-typing to keep behavior explicit and enforceable by the type checker. - Avoid
hasattrchecks when accessing attributes that all implementations share. Instead, add the attribute to the Protocol so the type checker can verify access. This eliminates dynamic lookups and makes the interface explicit. - When accepting an
Iterablebut needing multiple passes or sorting, materialize tolistonce at the boundary to avoid exhausting generators. - Annotate public APIs and module boundaries first (CLI entry points, FastAPI routes, shared utils).
- Avoid
Anyunless crossing untyped boundaries; when unavoidable, localize it and add a comment. - Prefer
TypedDictorProtocolover loosedict[str, object]andAnyfor structured data. - Use
LiteralorEnumfor fixed choices; useFinalfor constants. - Prefer
collections.abctypes for inputs (Sequence,Mapping,Iterable) and concrete types for outputs (e.g.,list,dict) when callers rely on mutability. - If a capability is optional, pick a single shape (property or method) and document it in the protocol; avoid supporting both unless required by existing implementations.
- For pydantic models, prefer explicit field types and
Annotated[...]where validation metadata is needed. - Use
Selffor fluent APIs andTypeAliasfor complex, reused types. - Use
type: ignoreonly when interacting with third-party APIs that are untyped or known-broken; otherwise preferty: ignore[rule]with the specific rule. - For tests, prefer
assert isinstance(x, ConcreteType)(orassert x is not None) to narrow types instead ofcast(...)when the runtime behavior enforces the type. - When dealing with untyped dict-like payloads in tests, add
assert isinstance(payload, dict)before passing into helpers instead of casting todict[str, Any].
- Baseline: run
ty checkonsrc/fast_agentand capture the initial error set. - Triage: group issues by module and rule; fix the highest-signal errors first.
- Configure: set rule levels in
pyproject.tomlso low-signal rules arewarnwhile we converge; keeppossibly-missing-attributeandpossibly-missing-importaterror. - Annotate: add types to public APIs, then internal helpers, then tests.
- Refine: replace broad
AnyorobjectwithTypedDict,Protocol,Literal, orEnumas appropriate. - Suppress sparingly: use
# ty: ignore[rule]only when the type system cannot express a valid pattern; include a short reason. - Enforce: add
ty checkto CI once warnings are near-zero; tighten rules toerroras we converge.
When replacing def func(*args, **kwargs) with explicit parameters to satisfy the type checker,
always verify all call sites first. The *args pattern accepts any number of positional
arguments, and removing it can silently break callers that pass positional args you didn't capture.
Before:
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)After (WRONG - missed read_timeout):
def __init__(self, read_stream, write_stream, **kwargs) -> None:
super().__init__(read_stream, write_stream, **kwargs)After (CORRECT):
def __init__(self, read_stream, write_stream, read_timeout=None, **kwargs) -> None:
super().__init__(read_stream, write_stream, read_timeout, **kwargs)Checklist before removing *args:
- Grep for all call sites of the function/class
- Check if any caller passes positional arguments beyond what you're capturing
- Check type hints or protocols that define expected signatures (e.g.,
Callable[[A, B, C], R]) - When in doubt, keep
*argsand pop known params from it, or add explicit params for all positional args callers use
This error is insidious because it causes runtime TypeError failures, not type-check failures.
- We will use
ty: ignore[rule]over barety: ignoreand avoidtype: ignoreunless an external dependency forces it. - We will prefer modern syntax (
X | Y, builtin generics) given Python 3.13+. - We will represent parsed UI commands as a lightweight discriminated
TypedDictunion (with akindfield) in a dedicated module, while leaving free-form user input as plainstr.