Skip to content

Classes → dataclass

class → @dataclass(slots=True)

class User:
id: int
name: str = "anon"
email: str?
  • slots=True — instances don’t carry a per-object __dict__; saves memory and catches typo’d attribute writes.
  • from dataclasses import dataclass is auto-injected.
  • Fields without defaults come first; fields with defaults follow (Python’s rule).

frozen modifier

class Point frozen:
x: float
y: float

Field reassignment is a hard error (tyc::frozen_assign); runtime raises FrozenInstanceError.

frozen + inheritance

The frozen modifier sits between the class name and the parenthesised base list:

class Square frozen(Shape):
side: float

class Square(Shape) frozen: (with the modifier after the base list) does not parse. The same rule applies to generics: class Stack[T] frozen:, class Stack[T] frozen(BaseStack):.

Mutable-default factory rewrite

Mutable-literal defaults ([], {}, set(), list(), dict()) on a class body are rewritten to dataclasses.field(default_factory=...) so each instance gets its own fresh container:

class Bucket:
items: list[str] = []
tags: dict[str, int] = {}

The rewrite applies to class and class frozen (the per-instance factories work with frozen=True’s object.__setattr__-driven __init__). It is not applied to model (Pydantic handles mutable defaults itself), interface (Protocol — no __init__), or class! (escape hatch — user owns the body verbatim). Since v0.9.0 the in-process VM (tyc run) also runs the factory per instance — before v0.9.0 the VM shared one container across every instance, diverging from the compiled path.

tyc::class_attr_shadows_slot correspondingly does not fire on mutable-literal defaults — those become per-instance fields, not class-level slot descriptors. The warning still fires on immutable literals (int = 3, str = "x") where the slot-descriptor pitfall applies; annotate those as ClassVar[T].

impl ... : merges into class body

class User:
id: int
name: str
impl User:
def display(self) -> str:
return f"{self.name} (#{self.id})"

Multiple impl blocks (across files) all merge.

model → Pydantic

model ApiUser:
id: int
email: str

extra="forbid" is the default (Pydantic’s stock extra="ignore" is rejected by Typhon’s safety posture). The configurable [emit] model-extra knob is roadmapped.

plain class — no-decoration escape hatch

plain class Bag:
items: list[str]
label: str = "unsorted"

plain class:

  • No @dataclass decorator.
  • No synthesised __init__. The body emits verbatim — instances acquire attributes when user code assigns them, exactly like a hand-written Python class.
  • Reach for plain class when a metaclass-driven library (Textual, Django ORM, SQLAlchemy declarative) owns the attribute layout.

class! — escape hatch with synthesised constructor

class! MyModel(nn.Module):
layer: nn.Linear
dropout: float
def forward(self, x):
return self.layer(x)

class!:

  • No @dataclass decorator.
  • __init__ is auto-synthesised — calls super().__init__() first, then assigns annotated fields in source order. The compiler strips class-level defaults that would otherwise be evaluated twice (once at class-definition time as a shared class attribute, then again per-instance in __init__).
  • Hand-written __init__ is preserved verbatim for cases where the base class needs configuration arguments that aren’t 1:1 with declared fields.

Typed exceptions

A common class! pattern is a typed exception subclass — extending Exception with structured fields the handler can read:

class! HttpError(Exception):
code: int
message: str
def fetch(url: str) -> str:
raise HttpError(code=404, message="not found")

Since v0.9.0 the synthesised __init__ also runs under the in-process VM (tyc run); before v0.9.0 the VM didn’t construct user-class instances for class!-declared exception types, so except HttpError as e: print(e.code) raised AttributeError at runtime. Exception-type matching also walks the MRO under the VM since v0.9.0, so except HttpError matches subclasses.

Auto-skip for framework bases

A plain class Foo(Base): (no marker) whose base is Enum / IntEnum / StrEnum / Flag / IntFlag / ABC / ABCMeta is automatically emitted without @dataclass. The same applies to Protocol / TypedDict / NamedTuple (which were always skipped). Project-specific bases can be added via [emit] skip-decoration-bases. Auto-skip drops only the decorator; it does not synthesise an __init__ — escalate to class! if you need one.

Generic classes

class Box[T]:
value: T
impl[T] Box[T]:
def get(self) -> T:
return self.value

PEP 695 generic syntax is preserved (Python 3.13+ default target).

Where next