Type & Inference Errors
Diagnostics in the tyc::* namespace related to type checking and inference.
tyc::missing_annotation
A function parameter or return type is missing its annotation — Rule 1 of the Typhon language: every parameter and return type is annotated.
def add(a, b: int) -> int: ... # ❌ parameter `a` has no annotationdef greet(name: str): # ❌ no return type print(name)def trace(f, *args, **kwargs): # ❌ since v0.9.0 — every variadic param too ...Fix: annotate every parameter and every return type. For a function that returns nothing, write -> None. For genuinely variadic functions (typically generic decorators or **kwargs forwarders), the canonical idiom for *args / **kwargs is object:
def add(a: int, b: int) -> int: ...def greet(name: str) -> None: print(name)def trace[R](f: Callable[..., R], *args: object, **kwargs: object) -> R: return f(*args, **kwargs)This single diagnostic covers both missing-parameter-types and missing-return-types — Typhon does not separate them today. Since v0.9.0 the diagnostic also fires on *args and **kwargs, and the rendered message drops the double-backtick wrapping the previous renderer produced (was rendered as `parameter `x “).
tyc::type_mismatch
A value’s type doesn’t match what’s expected.
let x: int = "hello" # ❌Fix: change the value, the annotation, or convert explicitly (int("...")).
tyc::arg_count
A call site has too few or too many arguments. Fires on free functions, methods, and class constructors.
def add(a: int, b: int) -> int: ...add(1) # ❌ missing badd(1, 2, 3) # ❌ extra argFix: match the signature.
Class constructors
The auto-generated __init__ of class X: (and model X:) requires every field without an = default to be supplied — positionally or by keyword:
class ApiClient: api_key: str base_url: str
def main() -> None: let c: ApiClient = ApiClient(base_url="https://api.example.com") # ❌ tyc::arg_count — expected 2, got 1A field of type T? is still required unless you write = None explicitly — Typhon does not auto-inject the default, so the emitted @dataclass would otherwise crash at runtime.
class Foo: name: str? # required at construction (no auto-default) label: str? = None # optionalimpl methods
Method calls are arity-checked the same way as free functions; the implicit self / cls receiver is excluded from the surface.
class User: name: str
impl User: def greet(self, prefix: str) -> str: return prefix + self.name
def main() -> None: let u: User = User(name="Ada") u.greet() # ❌ tyc::arg_count — expected 1, got 0Cross-module imports
Both from foo import Cls and import foo as f flow through the same arity check. The bare-import dotted form uses module-qualified names in diagnostics so two imports exporting the same class name remain disambiguated:
import clients
def main() -> None: let c = clients.ApiClient(base_url="x") # ❌ tyc::arg_count — wrong number of arguments to `clients.ApiClient`.dty stubs participate on equal footing with .ty source; stubs win on name collisions.
Limitation: dotted-attribute annotations (let c: f.Cls = …) don’t yet resolve to the foreign class shape — the constructor call itself arity-checks correctly, but the local binding lands as Type::Unknown. Workaround: use from foo import Cls or drop the annotation.
tyc::not_callable
Trying to call something that isn’t a function.
let x: int = 42x() # ❌Fix: call the right thing.
tyc::unknown_name
A name isn’t in scope.
def f() -> None: print(undefined_name) # ❌Fix: define or import the name. Check for typos.
tyc::attribute_not_found
An attribute is accessed on a value whose static type doesn’t expose it.
class User: name: str
def f(u: User) -> str: return u.email # ❌ no attribute `email` on `User`Fix: add the field (or method) to the class, fix the typo, or widen the type.
v0.8.0 widening + v0.8.1 carve-out
v0.8.0 extended the firing site from TypeVar-bounded parameters to direct class instances (p: Point) and generic-class receivers (s: Stream[int]). v0.8.1 narrowed it again so venv-introspected third-party classes don’t false-positive: InterfaceShape carries a partial: bool flag set on every shape built from inspect.signature(Cls), and class_hierarchy_fully_known returns false whenever any class in the chain is partial. The net effect — calls like uvicorn.Server.serve(...), httpx.AsyncClient.aclose(...), fastapi.Request.body(...) against third-party libraries stay permissive. Skipped in unsafe: regions and on dunder / leading-underscore names.
extend list: dispatches before attribute_not_found (v0.9.0)
Until v0.9.0 an extend list: method on a list[T]-annotated receiver fell through to attribute_not_found. v0.9.0 consults the synthetic __typhon_builtin_ext_list class shape before the diagnostic fires, so the parameterised forms dispatch correctly:
extend list: def first_or[T](self, default: T) -> T: return self[0] if self else default
def head(xs: list[int]) -> int: return xs.first_or(0) # ✅ since v0.9.0 — was attribute_not_found beforetyc::generic
Generic catch-all used during early-phase type checking. The message text describes the specific problem — typically a generic-instantiation failure, an unresolved TypeVar, or a constraint conflict.
def first[T](xs: list[T]) -> T?: ...
let xs = first([]) # ❌ tyc::generic — cannot infer T from an empty listFix: annotate the call (first[int]([])), annotate the target (let xs: int? = first([])), or feed the function a non-empty input the inference can latch onto.
tyc::typevar_bound
A type argument at a call site doesn’t satisfy its TypeVar’s declared bound.
def largest[T: Comparable](xs: list[T]) -> T: ...
class Widget: ... # not Comparablelet w = largest([Widget(), Widget()]) # ❌ Widget does not satisfy `Comparable`Fix: pass values whose type is a subtype of the bound, or widen the bound on the declaration.
tyc::generator_return_type
A function uses yield (or yield from) but its declared return type isn’t iterator-shaped. The function returns a generator object at runtime, so the annotation is wrong.
def odds(n: int) -> list[int]: # ❌ contains `yield`, so returns a generator for i in range(n): if i % 2: yield iFix: annotate as Iterator[T] / Generator[T, S, R] (or AsyncIterator[T] / AsyncGenerator[T, S] for async def).
from collections.abc import Iterator
def odds(n: int) -> Iterator[int]: for i in range(n): if i % 2: yield ityc::cyclic_type_alias
A type alias chain forms a cycle:
type A = Btype B = A # ❌ no concrete type at the bottomFix: point at least one alias in the cycle at a concrete type, or remove one of the declarations.
tyc::comptime
A comptime binding could not be evaluated at build time. The message text names the binding and explains the specific failure (e.g. unsupported expression, missing required env var, fell-through control flow).
comptime let PORT: int = int(env("PORT")) # ❌ if PORT is not declared in [env].requiredFix: declare the env var in [env] required, supply a default via env("NAME", "fallback"), or simplify the expression to a comptime-supported form (literals, arithmetic, env(...), int() / str() / float()).
tyc::missing_argument
A call leaves one or more required parameters unfilled. The message names
them, which tyc::arg_count cannot do once some arguments
were passed by keyword.
def connect(host: str, port: int, timeout: float = 5.0) -> None: print(host, port, timeout)
def main() -> None: connect(timeout=2.0) # ❌ missing required arguments to `connect`: `host`, `port`Fix: pass the named arguments: connect("localhost", 8080, timeout=2.0).
tyc::missing_return
A function declares a non-None return type, but some path reaches the end
of the body without return or raise — Python would return None there.
def classify(n: int) -> str: if n > 0: return "positive" if n < 0: return "negative" # ❌ falls off the end when n == 0Fix: cover the path (return "zero"), or widen the return type to
str?. A call that cannot return — sys.exit(...), os.abort(),
assert False, a function declared -> NoReturn — counts as an exit. A
match that is not exhaustive (see Match Errors)
leaves a path open and reports this code as well.
tyc::operator_type_mismatch
A binary operator is applied to operand types Python rejects at runtime
(str + int, list + dict, 1 in "abc"), or to two distinct newtypes
that share a base.
def main() -> None: let result: str = "x" + 1 # ❌ unsupported operand types for `+`: `str` and `int` print(result)Fix: convert one side ("x" + str(1)). The check only fires when both
operand types are fully known and neither is a user class that may define
its own operator method.
tyc::tuple_index_out_of_range
A constant index into a fixed-arity tuple is out of range.
def main() -> None: let t: tuple[int, int] = (1, 2) print(t[2]) # ❌ tuple has 2 element(s); index 2 is out of rangeFix: use an index in range, or a homogeneous tuple[int, ...] if the
length really varies.
tyc::kind_mismatch
A higher-kinded type parameter (F[_]) is applied to the wrong number of
type arguments, or bound to two different constructors in one call.
class Functor[F[_]]: pass
impl[F[_]] Functor[F]: def both[A](self, x: F[A], y: F[A]) -> None: return None
def main() -> None: let xs: list[int] = [1, 2, 3] let ss: set[int] = {1, 2, 3} Functor().both(xs, ss) # ❌ type constructor `F` is bound to both `list` and `set`Fix: pass arguments whose outer constructors agree (two lists here),
and apply F to exactly as many arguments as its kind declares.
tyc::implicit_any
A bare collection annotation (list, dict, tuple, set, frozenset)
on a binding has no element type, so it would mean list[Any].
def main() -> None: let names: list = ["a", "b"] # ❌ bare `list` annotation has implicit `Any` element type print(names)Fix: spell out the element types: let names: list[str] = ["a", "b"].
tyc::div_by_zero_literal
/, // or % with a literal zero divisor (0, 0.0, -0, -0.0)
always raises ZeroDivisionError.
def average(values: list[float]) -> float: return sum(values) / 0 # ❌ division by literal zeroFix: divide by the real quantity and guard the empty case. The check is constant-folding only; a variable divisor is never flagged.
def average(values: list[float]) -> float?: if len(values) == 0: return None return sum(values) / len(values)tyc::python_semantic_drift (not currently emitted)
Listed by tyc explain --list and kept for the case where Typhon rejects an
expression CPython accepts — a checker bug, reported as a warning so builds
keep working while the rule is fixed. No check in the current compiler
emits it. If tyc check rejects code that runs correctly on CPython, file
an issue with the snippet.
Suppressed inside unsafe:
Most type-checking diagnostics are suppressed inside an unsafe: block — the type checker tracks an unsafe_depth and stops emitting errors while it is greater than zero. Boundary checks at assignment sites outside the block still apply. See The Unsafe Boundary.
tyc::missing_annotation is not suppressed: it is a structural rule about the function declaration, not a typing decision at a use site.
Where next
- Binding Errors —
missing_binding_kind,immutable_assign,unused_import. - Nullability Errors —
nullable_use. - Class Errors —
duplicate_class,impl_unknown_class,manual_init. - Reading Diagnostics — the format.