Skip to content

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 annotation
def 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 b
add(1, 2, 3) # ❌ extra arg

Fix: 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 1

A 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 # optional

impl 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 0

Cross-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 = 42
x() # ❌

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 before

tyc::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 list

Fix: 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 Comparable
let 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 i

Fix: 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 i

tyc::cyclic_type_alias

A type alias chain forms a cycle:

type A = B
type B = A # ❌ no concrete type at the bottom

Fix: 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].required

Fix: 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 == 0

Fix: 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 range

Fix: 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 zero

Fix: 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