Skip to content

Derives and traits (Explanation)

This page explains the mental model behind derives, dunder overrides, and traits in Incan. For the exact catalog of supported derives and signatures, see the Reference:

The three mechanisms (and when to use each)

Incan uses three mechanisms to implement behavior:

Derive (out-of-the-box behavior)

Use @derive(...) when the default, structural behavior is what you want (for example: field-based equality).

Incan derives are intentionally “Python-first”:

  • You get common behaviors without writing boilerplate
  • Behaviors are still explicit in your type definition

Read more about @derive(...): out-of-the-box behavior.

Dunder methods (custom behavior)

Use dunder methods when you need custom semantics for a built-in capability:

  • __str__: custom string output
  • __eq__: custom equality
  • __lt__: custom ordering
  • __hash__: custom hashing

Incan treats “derive + corresponding dunder” as a conflict. The idea is to avoid ambiguity and keep the mental model simple: “either it’s the default behavior, or it’s my behavior.”

Read more about dunder methods: custom behavior.

Traits (domain capabilities)

Use traits for reusable, domain-specific capabilities:

  • You define a contract once
  • Models, classes, enums, and other concrete type declarations can opt into it via with TraitName
  • Traits can include default method bodies
  • Traits are always abstract, so the trait name itself can be used directly in annotations
  • Traits can build capability hierarchies with trait Sub with Base:

Traits are not “the derive system.” Derives are a convenience for a small set of built-in capabilities; traits are a general language feature for authoring reusable behavior.

That gives Incan a simple mental model: a trait is both a capability declaration and an abstract accepted type. If a function says it accepts Collection[Order], that means “any concrete adopter of Collection[Order]”, not “rewrite this API as a hidden generic bound first”.

Read more about traits: domain capabilities.

Enums use the same adoption model when a closed set of variants should satisfy a protocol. The required behavior lives in the enum body, next to the variants it interprets:

trait Describable:
    def describe(self) -> str: ...

enum Outcome with Describable:
    Success
    Failure(str)

    def describe(self) -> str:
        match self:
            Outcome.Success => return "success"
            Outcome.Failure(message) => return message

That makes enum adoption useful at API boundaries:

def render[T with Describable](value: T) -> str:
    return value.describe()

message = render(Outcome.Failure("timed out"))

Use this when the enum itself owns the behavior. Keep a free function when the behavior combines several independent types or belongs to a higher-level service, view, or adapter layer.

Generic methods on types

Methods can also introduce their own type parameters. This works on class, model, trait, enum, and newtype declarations, using the same syntax as generic top-level functions:

class Box:
    def get[T with Clone](self, value: T) -> T:
        return value

The important distinction is that these are method-scoped type parameters, not hidden type parameters on the enclosing type. A generic model Shelf[U] may still define a method like def swap[T](...), where U belongs to the type and T belongs only to that method.

For enums, owner type parameters and method type parameters are separate in the same way:

enum Maybe[U]:
    Some(U)
    None

    def echo[T](self, value: T) -> T:
        return value

For the rationale behind explicit call-site generics (f[T](...) / obj.m[T](...)), see:

Debug vs Display: two string representations

Incan intentionally separates two kinds of “stringification”:

  • Debug ({:?}): developer-facing, structured, and not user-overridable
  • Display ({}): user-facing output; you can override via __str__

This mirrors the common “logs vs user output” split: Debug is stable and structural; Display is designed for human-friendly formatting.

@rust.extern (Rust-backed functions)

You may see @rust.extern used in stdlib sources and Rust-backed libraries. It marks functions whose body are provided by a Rust module (declared via rust.module()).

The intended meaning is:

  • the function's signature is defined in Incan (.incn source)
  • the function's implementation lives in a Rust crate, mapped via rust.module()
  • the compiler emits a call to the Rust implementation instead of compiling the ... body

This lets the stdlib (and third-party libraries) wrap Rust crates with Incan-shaped APIs while keeping most logic in pure Incan.

See also:

Field defaults and construction (pydantic-like ergonomics)

Field defaults (field: T = expr) are part of Incan’s “pydantic-like” ergonomics:

  • If you omit a field and it has a default, the default is used
  • If a field has no default, you must provide it at construction time

Separately, @derive(Default) provides Type.default() as a baseline constructor. It uses field defaults when present, and otherwise falls back to type defaults.

See also