Skip to content

Derives and Traits

This page is a Reference for Incan derives, dunder overrides, and trait authoring. For guided learning, use:


Conflicts & precedence

Rules that resolve “derive vs dunder” (these are intentional and strict):

  • Dunder overrides are explicit behavior: if you write a dunder (__str__, __eq__, __lt__, __hash__), that is the behavior for that capability.
  • Conflicts are errors: you must not combine a dunder with the corresponding @derive(...).
  • Auto-added traits are the exception: some traits are automatically added to model / class / enum / newtype (see Automatic derives). You don’t need to spell them out.

Automatic derives

The compiler automatically adds these derives:

Construct Auto-added derives
model Debug, Display, Clone
class Debug, Display, Clone
enum Debug, Display, Clone, Eq
newtype Debug, Display, Clone (and Copy if underlying is Copy)

Notes:

  • Debug and Display are always available for these constructs.
  • Display has a default representation (like Python’s default __str__) unless you define __str__.

Derive catalog (quick index)

Only the items usable in @derive(...) are derives:

Derive Capability Override Notes
Debug Debug formatting ({:?}) Auto-added
Display Display formatting ({}) __str__ Auto-added
Eq == / != __eq__ Conflicts are errors
Ord Ordering + sorted(...) __lt__ Conflicts are errors
Hash Set / Dict keys __hash__ Conflicts are errors
Clone .clone() Auto-added
Copy Implicit copy Marker trait
Default Type.default() Baseline constructor
Serialize JSON stringify json_stringify(value)
Deserialize JSON parse T.from_json(str)

Detailed pages:

Derives:

Stdlib traits:


Derive dependencies and requirements

Some derives imply prerequisite derives (the compiler adds them automatically):

If you request Compiler also adds
Eq PartialEq
Ord Eq, PartialEq, PartialOrd

Semantic requirements (not “auto-added”):

  • If you derive Hash, you almost always also want Eq. Prefer @derive(Eq, Hash).
  • If you provide custom equality via __eq__, your hashing (derived or custom) must remain consistent.

Common compiler diagnostics

Unknown derive

@derive(Debg)  # Typo
model User:
    name: str

Expected: “unknown derive” with a list of valid derive names.

Deriving a non-trait

model User:
    name: str

@derive(User)  # wrong: User is a model, not a derive/trait
model Admin:
    level: int

Expected: “cannot derive a model/class” with a hint to use with TraitName for trait implementations.


Traits (authoring)

Traits define reusable capabilities. You opt in with with TraitName. Methods can be required (...) or have defaults.

trait Describable:
    def describe(self) -> str:
        return "An object"

class Product with Describable:
    name: str

def main() -> None:
    p = Product(name="Laptop")
    println(p.describe())

@requires(...) (adopter contract)

@requires(...) is a decorator you can put on a trait to declare which adopter fields must exist (and what types they must have).

Syntax:

@requires(field_a: TypeA, field_b: TypeB)
trait MyTrait:
    ...

What the compiler enforces:

  • When a class/model adopts a trait (with MyTrait), it must provide all required fields with compatible types.
  • Trait default methods may access adopter fields like self.field only if that field is declared in @requires(...).
  • Mutating adopter fields still requires mut self (same as normal methods).

Example:

@requires(name: str)
trait Loggable:
    def log(self, msg: str) -> None:
        println(f"[{self.name}] {msg}")

class Service with Loggable:
    name: str

Example (mutation):

@requires(count: int)
trait Counter:
    def bump(mut self) -> None:
        self.count += 1

See also: Traits (authoring).


Debug (Automatic)

Format: {value:?} (Debug)

Override: not supported (Debug is compiler-generated).

model Point:
    x: int
    y: int

def main() -> None:
    p = Point(x=10, y=20)
    println(f"{p:?}")  # Point { x: 10, y: 20 }

Display (Custom with __str__)

Format: {value} (Display)

Default behavior: custom types have a default Display representation.

Custom behavior: define __str__(self) -> str.

Conflict rule: if you define __str__, you must not also @derive(Display).

model User:
    name: str
    email: str

    def __str__(self) -> str:
        return f"{self.name} <{self.email}>"

def main() -> None:
    u = User(name="Alice", email="alice@example.com")
    println(f"{u}")    # Alice <alice@example.com>
    println(f"{u:?}")  # User { name: "Alice", email: "alice@example.com" }

Eq (Equality)

What it does: enables == / !=.

Custom behavior: define __eq__(self, other: Self) -> bool.

Conflict rule: if you define __eq__, you must not also @derive(Eq).

model User:
    id: int
    name: str

    def __eq__(self, other: User) -> bool:
        return self.id == other.id

Ord (ordering)

What it does: enables ordering operators and sorted(...).

Custom behavior: define __lt__(self, other: Self) -> bool.

Conflict rule: if you define __lt__, you must not also @derive(Ord).

model Task:
    priority: int
    name: str

    def __lt__(self, other: Task) -> bool:
        return self.priority < other.priority

Hash

What it does: enables use as Set members and Dict keys.

Custom behavior: define __hash__(self) -> int.

Conflict rule: if you define __hash__, you must not also @derive(Hash).

Consistency rule: if a == b, then a.__hash__() == b.__hash__().

@derive(Eq, Hash)
model UserId:
    id: int

Clone

What it does: enables .clone() (deep copy).

Auto-added for model/class/enum/newtype.


Copy

What it does: enables implicit copying (marker trait).

Use only for small value types; all fields must be Copy.


Default

What it does: provides Type.default() baseline construction.

Field defaults vs Default:

  • Field defaults (field: T = expr) are used by normal constructors when omitted.
  • @derive(Default) adds Type.default(); it uses field defaults when present, otherwise type defaults.

Constructor rule:

  • If a field has no default, it must be provided when constructing the type.
@derive(Default)
model Settings:
    theme: str = "dark"
    font_size: int = 14

def main() -> None:
    a = Settings()               # OK: all omitted fields have defaults
    b = Settings(font_size=16)   # OK
    c = Settings.default()       # OK

Serialize

What it does: enables JSON serialization.

API:

  • json_stringify(value)str
@derive(Serialize)
model User:
    name: str
    age: int

def main() -> None:
    u = User(name="Alice", age=30)
    println(json_stringify(u))

Deserialize

What it does: enables JSON parsing into a type.

API:

  • T.from_json(input: str)Result[T, str]
@derive(Deserialize)
model User:
    name: str
    age: int

def main() -> None:
    result: Result[User, str] = User.from_json("{\"name\":\"Alice\",\"age\":30}")

Compiler errors (reference)

Unknown derive (example)

@derive(Debg)
model User:
    name: str
type error: Unknown derive 'Debg'

Deriving a non-trait (example)

model User:
    name: str

@derive(User)
model Admin:
    email: str
type error: Cannot derive 'User' - it is a model, not a trait

Reflection (automatic)

Models and classes provide:

  • __fields__() -> List[str]
  • __class_name__() -> str
model User:
    name: str

def main() -> None:
    u = User(name="Alice")
    println(u.__class_name__())
    println(u.__fields__())