Skip to content

Compile & Interface Errors

Diagnostics that don’t fit any single category above — file IO, parse failures, interface conformance, and unsupported lazy forms.

tyc::io

The compiler could not read a .ty / .dty / typhon.toml file from disk. The message names the offending path and the OS-level cause (missing file, permission denied, …).

error[tyc::io]: cannot read 'src/main.ty': No such file or directory

Fix: check that the file exists, is readable, and that the path you passed tyc check / tyc build is correct.

tyc::parse

A .ty (or .dty) source file failed to parse. The message text is forwarded from the vendored Ruff parser and points at the exact span that broke parsing.

def f(: # ❌ tyc::parse — unexpected `:` after `(`
pass

Fix: correct the syntax error. Parse failures stop the pipeline before any name resolution or type checking, so this is always the first thing to fix when it appears.

tyc::interface_not_conforming

A value of type T is used where an interface Iface is expected, but T is missing one or more required members (or the member signatures don’t match).

interface Drawable:
def draw(self) -> None
class Widget:
name: str
# ❌ no `draw` method
def render(d: Drawable) -> None: ...
render(Widget(name="x")) # ❌ Widget does not conform to Drawable

Fix: add the missing member(s) — typically inside an impl T: block — with matching parameter types and return type:

impl Widget:
def draw(self) -> None:
print(self.name)

The check walks the candidate’s MRO and matches field types; it does not require an explicit Widget(Drawable) base. See Interfaces.

v0.8.0 added parameter-type conformance

The conformance check compares parameter types position-by-position (contravariant on params) in addition to arity since v0.8.0, so a class BadRepo claiming to implement interface Repo: def save(self, item: str) -> bool with a def save(self, item: int) -> bool impl is rejected at conformance time. Before v0.8.0 the arities matched but the parameter-type mismatch went silently.

v0.9.0 arity-message clarification

The arity diagnostic message reads “got N non-self parameter(s), expected M” since v0.9.0 instead of the ambiguous “arity N; expected M”. The previous wording was easy to misread when the impl matched arity but the parameter type was off, or vice versa — the new wording makes it explicit which axis the diagnostic is reporting on.

tyc::incompatible_override

A subclass method overriding a base method with an incompatible signature — different arity, a narrower parameter type, or an unassignable return type. This is the Liskov-substitution violation that mypy and pyright flag: a caller holding a base reference must be able to call the override safely.

class Handler:
def handle(self, event: object) -> int: ...
class StrictHandler(Handler):
def handle(self, event: str) -> int: # ⚠️ narrower param: str ⊂ object
return len(event)

Fix: keep the override signature compatible — accept the same-or-wider parameter types, and return the same-or-narrower type:

class StrictHandler(Handler):
def handle(self, event: object) -> int: # ✅ same param type
return len(str(event))

v0.13.0 argument-range refinement

The check compares required-vs-accepted argument ranges rather than raw parameter counts, so an override that adds an optional parameter is no longer flagged — a base reference can still call it with the original argument set:

class StrictHandler(Handler):
def handle(self, event: object, retries: int = 3) -> int: # ✅ not flagged
return len(str(event))

It fires only when the override requires more arguments than the base accepts, accepts fewer than the base may pass, or adds a required keyword-only parameter the base callers never supply.

tyc::lazy_usage

A lazy construct was written in an unsupported form. The message text names the specific problem.

Supported forms today:

lazy import np = numpy # module-level
lazy let SETTINGS: Settings = load_settings() # module-level value
class Cache:
name: str
impl Cache:
lazy let entries: dict[str, int] = {} # class-level (becomes @cached_property)

Note the class-level form lives in an impl block, not in the class body itself — methods (including lazy let properties) belong in impl per Rule 4. Putting it inside the class body fires the standard tyc::method_in_class_body diagnostic.

Unsupported forms (rejected with tyc::lazy_usage):

lazy from numpy import * # ❌ star imports cannot be lazy (`lazy from` itself needs a 3.15 target)
def f() -> None:
lazy let x: int = 1 # ❌ function-body `lazy let` not supported

Fix: rewrite to one of the supported forms above. For per-call laziness inside a function body, use a plain function or functools.cache directly.

tyc::freeze_not_freezable (v0.9.0)

freeze let X = <expr> where the RHS constructs a value that cannot be deep-frozen — typically a non-frozen user class. Before v0.9.0 the failure surfaced as a runtime TypeError at first import; v0.9.0 pre-validates the RHS at check time so the diagnostic fires before the module ever runs.

class Counter: # not frozen
value: int
freeze let CFG = Counter(value=0) # ❌ tyc::freeze_not_freezable
# `Counter` is not declared `frozen`

Fix: mark the class frozen, switch to a built-in container (list / dict / set are wrapped automatically into tuple / MappingProxyType / frozenset), or use a plain let if mutability is wanted.

class Counter frozen:
value: int
freeze let CFG = Counter(value=0) # ✅ check-time validated

tyc::reserved_module_name

The project defines its own typhon_runtime module or package directly under the source root. That name is reserved for the runtime package tyc build generates next to the emitted Python whenever the program uses Result / Ok / Err, go, lazy, freeze or try_result, or imports typhon_runtime; the generated package replaces the project’s.

src/typhon_runtime/__init__.ty # def helper() -> int: …
src/main.ty # from typhon_runtime import helper … Ok(helper())

tyc build stops with an error when the program imports something from typhon_runtime that the generated runtime does not provide — the program could not start (ImportError: cannot import name 'helper') — or reads such a name off a bare import typhon_runtime (typhon_runtime.helper(), which raises AttributeError where it runs). A read through a name the file rebinds elsewhere, or of an attribute the program sets on the module itself, is not counted. In every other case it, like tyc check, only warns.

Fix: rename the module (for example to runtime_helpers) and update its imports.

tyc::requires_newer_python

The file uses Python syntax or a builtin the project’s [python] target does not have, such as [*xs for xs in lists] (PEP 798), frozendict (PEP 814), sentinel (PEP 661) or lazy from M import … (PEP 810), all Python 3.15, in a project targeting 3.13, or except A, B: / t"..." (Python 3.14). The emitted .py would fail to compile or run on the target interpreter, so tyc check, tyc build, tyc run and the editor reject it. Typhon’s own lazy import ALIAS = MODULE is not affected, and neither is a module that defines its own frozendict or sentinel.

# [python] target = "3.13"
let flat: list[int] = [*xs for xs in lists] # ❌ needs Python 3.15
let flat: list[int] = [x for xs in lists for x in xs] # ✅
let table = frozendict(a=1) # ❌ needs Python 3.15

Fix: raise [python] target, or use a form the target accepts.

tyc::unknown_module (warning)

In a project with a typhon.toml, an import names a module that is not in the standard library, not part of the project, and not declared under [dependencies].

import flask # module `flask` is not in the stdlib, the project, or `typhon.toml` dependencies

Fix: correct the name, add the package to [dependencies] (tyc add flask), or create the missing .ty module. The diagnostic is rendered with the error marker but does not fail tyc check.

Stdlib modules are judged against [python] target: import sre_parse warns on a 3.15 target (the module was removed), import annotationlib is accepted from 3.14, and modules removed before 3.13 (imp, distutils, cgi, …) warn on every target.

tyc::unknown_kwarg

A call passes a keyword argument the callee does not accept (and the callee has no **kwargs), or passes a positional-only parameter by keyword.

def connect(host: str, port: int) -> None:
print(host, port)
def main() -> None:
connect(host="localhost", prot=80) # ❌ unknown keyword argument 'prot' to `connect`

Fix: use the parameter’s real name (port=80); the help text suggests the closest match.

tyc::typevar_import_rejected

from typing import TypeVar. Typhon uses PEP 695 type-parameter syntax instead.

from typing import TypeVar # ❌ `from typing import TypeVar` is not supported in Typhon

Fix: declare the parameter on the function or class:

def first[T](xs: list[T]) -> T:
return xs[0]

tyc::typing_alias_deprecated (warning)

An import of a capitalised typing alias (List, Dict, Tuple, Set, FrozenSet, Type).

from typing import List # `from typing import List` is deprecated in Typhon
def total(xs: List[int]) -> int:
return sum(xs)

Fix: drop the import and use the built-in (list[int]). Like unknown_module, it is rendered with the error marker but does not fail tyc check.

tyc::typing_alias_in_annotation (warning)

An annotation uses a capitalised typing alias, imported or not.

import typing
def total(xs: typing.List[int]) -> int: # ⚠ `List` in an annotation is the deprecated `typing.List` alias
return sum(xs)

Fix: List → list, Dict → dict, Tuple → tuple, Set → set, FrozenSet → frozenset, Type → type.

tyc::unsafe_value_leak

A value from an unsafe: block — or one derived from it (data["name"], data.count + 1, [x for x in data]) — reaches a concretely typed return or binding without being checked.

import json
def parse(raw: str) -> int:
unsafe:
let value = json.loads(raw)
return value # ❌ `value` was introduced inside `unsafe:` and escapes into a concrete `int` return

Fix: check it at the boundary with return value as! int, or annotate the binding inside the block (let value: int = json.loads(raw)). An annotated re-bind outside the block is itself a boundary and is reported the same way.

tyc::invalid_config_value

A typhon.toml key has a value outside its allowed set — for example [emit] class-default = "plain" (only "dataclass" is accepted), an unknown [checker] external, or an invalid [strictness] severity.

tyc::invalid_config_value
× invalid value `plain` for `emit.class-default` in `typhon.toml`: expected one of dataclass
help: Pick one of the listed values in your typhon.toml.

Fix: use one of the listed values. Every command that loads the config refuses to run; tyc build reports it under this code, tyc check prints the same message without the code.

tyc::pub_name_collision

pub * in an __init__.ty aggregates two sibling modules that both export the same pub name, so whichever import ran last would win.

src/mypkg/a.ty
pub def hello() -> str:
return "from a"
# src/mypkg/b.ty
pub def hello() -> str:
return "from b"
# src/mypkg/__init__.ty
pub * # ❌ `pub *` collision: `hello` is exported by both `a` and `b`

Fix: rename one, drop pub from one, or replace pub * with explicit from .a import hello re-exports.

tyc::pub_star_outside_init (advice)

pub * in an ordinary module. It only aggregates a package’s sibling modules, so outside __init__.ty it does nothing.

Fix: move it to the package’s __init__.ty, or delete it.

tyc::stdlib_module_shadow (warning)

A project file is named like a standard-library module (types.ty, json.ty, io.ty, …). The emitted build/types.py sits on sys.path ahead of the standard library and intercepts every import types, including the standard library’s own.

Fix: rename the file (lang_types.ty) and update its imports. Checked only in a project with a typhon.toml.

tyc::orphan_py_import (warning)

A relative import resolves to a .py file outside src/. The build copies only src/, so the import would fail with ModuleNotFoundError once the build output is shipped.

from .helper import do_thing # ⚠ resolves outside `src/` and will not be copied to the build output

Fix: move the .py file under src/, or import it by an absolute name that the build does package.

tyc::main_not_called (advice)

A module defines a top-level def main() but never calls it, so running the script does nothing.

Fix: add the entry guard:

def main() -> None:
print("hello")
if __name__ == "__main__":
main()

Quoted annotations as forward references (v0.13.0)

Quoted annotations now resolve as forward references — the reflexive Python idiom for naming a type that isn’t defined yet — rather than as string-literal singleton types. This covers next: "Node", -> "Tree[T]", and "list[Node]", including a trailing ? ("Node"?).

class Node:
value: int
next: "Node"? # ✅ forward reference — resolves to Node | None

Before v0.13.0 a quoted annotation became a string-literal singleton type, so the example above produced a baffling tyc::type_mismatch when a real Node was assigned to it.

Carve-out — bare quoted scalar builtin names stay literal singletons. A quoted scalar builtin name on its own is still a literal-singleton type, so the literal-union forms are unchanged:

type Mode = "int" | "str" # literal-singleton union (NOT a forward ref)
type Color = "red" | "green" # literal-singleton union, unchanged

Only annotation positions that name a type (a class, an alias, a subscripted generic) resolve as forward references; bare quoted builtin scalar names in a union remain literal singletons.

Where next