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:
DebugandDisplayare always available for these constructs.Displayhas 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) |
| Validate | Validated construction | — | Models only |
Detailed pages:
Derives:
Stdlib traits:
- Overview
- Collection protocols
- Indexing and slicing
- Callable objects
- Operator traits
- Conversion 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 |
Note: you may also request PartialEq and PartialOrd explicitly via @derive(PartialEq) / @derive(PartialOrd).
Semantic requirements (not “auto-added”):
- If you derive
Hash, you almost always also wantEq. 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.
Decorators (@staticmethod, @classmethod, @requires)¶
Incan has several built-in decorators with different roles.
@derive(...)is covered above.@rust.externand@rust.allow(...)belong to Rust interop and are documented in the Rust interop reference.- This section covers the method and trait decorators you will use when authoring ordinary Incan types and traits.
@staticmethod¶
A static method belongs to the type rather than an instance — it has no self receiver.
class Temperature:
celsius: float
@staticmethod
def from_fahrenheit(f: float) -> Temperature:
return Temperature(celsius=(f - 32.0) / 1.8)
def main() -> None:
t = Temperature.from_fahrenheit(98.6)
println(t.celsius)
Applies to: methods on class, model, enum, and newtype declarations.
Rules:
- A
@staticmethodmethod must not have aselformut selfparameter. The compiler rejects this. - Call static methods via the type name:
TypeName.method_name(...). - Generic static factory methods can return
Self; call them with explicit type arguments when inference needs the owner type, such asBox[int].make(...). @staticmethodcan be combined with@rust.externfor Rust-backed static methods on types.
Use cases:
- Factory methods: alternative constructors (
from_fahrenheit,from_json,parse). - Utility functions: logic scoped to a type that doesn't need instance state.
- Rust interop:
@rust.externon type methods requires@staticmethod(instance delegation is not supported).
Generic static factories can use the declaring type constructor directly:
class Box[T with Clone]:
value: T
@staticmethod
def make(value: T) -> Self:
return Box(value=value)
def main() -> None:
boxed = Box[int].make(1)
println(str(boxed.value))
See also: Classes: Static methods
@classmethod¶
Use @classmethod for methods that are called on the type rather than on an instance, but still conceptually belong to that type.
This is commonly used for constructor-style APIs:
model UserId:
value: int
@classmethod
def from(cls, value: str) -> Self:
return cls(value=int(value))
Unlike an instance method, a class method does not take self. Its first parameter is conventionally named cls and can be called like a constructor for the declaring type. Unlike a static method, it is written as a type-associated constructor-style hook and returns Self naturally.
For generic types, Self keeps the active type arguments at the call site:
class Box[T with Clone]:
value: T
@classmethod
def make(cls, value: T) -> Self:
return cls(value=value)
def main() -> None:
boxed = Box[int].make(1)
println(str(boxed.value))
@requires(...)¶
Covered below in Traits (authoring).
Generic instance methods¶
Instance methods on class, model, trait, enum, and newtype may declare method-level type parameters using the same syntax as top-level generic functions:
class Box:
def get[T with Clone](self, value: T) -> T:
return value
This is method-level polymorphism: method type parameters belong to the method, not to the enclosing type.
This does not replace normal method signatures. Non-generic methods still use the standard form:
def describe(self, verbose: bool) -> str:
...
Rules to keep in mind:
- Method type parameters appear after the method name:
def name[T, U with Trait](...). - Method type parameters are scoped to that method only.
- Enclosing type parameters and method type parameters may both be used in the same signature.
- Trait methods may also be generic, whether they are required (
...) or provide a default body.
Examples:
model Shelf[U]:
item: U
def swap[T with Clone](self, value: T) -> T:
return value
trait Echo:
def echo[T with Clone](self, value: T) -> T:
return value
enum Slot[U]:
Filled(U)
Empty
def echo[T](self, value: T) -> T:
return value
type Wrapper[U] = newtype U:
def echo[T with Clone](self, value: T) -> T:
return value
Method generic syntax is additive and aligned with function generics: def method[T](...) extends, but does not replace, def method(...).
Call-site type arguments¶
Generic calls normally infer type parameters from value arguments. You may also provide explicit type arguments at the call site.
Why this feature exists (design rationale):
Call-site type arguments go in square brackets immediately after the function or method name and before value arguments.
Syntax:
- Function:
callee[type_args](value_args...) - Method:
receiver.method[type_args](value_args...)
type_args is comma-separated. Each entry is either a type expression or _. If brackets are present, arity must match the callee's type parameter count, including _ slots.
Single type parameter:
You may call with no brackets (fully inferred) or one explicit type argument:
rows_inferred = session.read_csv(str("orders.csv")) # inferred when context/value args are enough
rows_typed = session.read_csv[Order](str("orders.csv")) # explicit row type at the API boundary
Multiple type parameters:
For multi-parameter generics, either infer all slots or provide one bracket entry per slot:
parsed = decode_rows(str("orders.csv")) # T and E inferred
parsed_typed = decode_rows[Order, CsvDecodeError](str("orders.csv")) # both explicit
parsed_partial = decode_rows[Order, _](str("orders.csv")) # T explicit, E inferred via `_`
The _ placeholder:
_ means "infer this slot". It still counts toward arity: decode_rows[Order](...) is invalid for a two-parameter generic, while decode_rows[Order, _](...) is valid.
Type checking order:
Explicit slots are applied first. _ slots are inferred from value arguments and normal compatibility checks. If a slot remains unresolved, the compiler reports a call-site type error.
What is not supported:
Explicit brackets are supported only for direct calls resolved as Incan functions/methods. Using brackets on other call shapes is an error (not ignored), for example:
- Built-in calls like
len[int](...) - Calls to functions imported from Rust (
from rust::...) - Calling a generic function through a variable (e.g.
read = session.read_csv; read[Order](...)), where the callee is not a direct name or method
Traits (authoring)¶
Traits define reusable capabilities. Traits are always abstract: you opt concrete types in with with TraitName, and you may also use the trait name itself directly in annotations. Methods can be required (...) or have defaults.
Models, classes, enums, and other concrete type declarations can adopt traits. Traits may adopt other traits with the same with syntax to form capability hierarchies. That means a narrower trait can refine a broader one, and any concrete adopter of the narrower trait is also accepted where the broader trait is expected.
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())
trait Renderable:
def render(self) -> str: ...
enum Token with Renderable:
Text(str)
Break
def render(self) -> str:
match self:
Token.Text(value) => return value
Token.Break => return "\n"
trait Collection[T]:
def first(self) -> T: ...
trait OrderedCollection[T] with Collection[T]:
def sorted(self) -> Self: ...
def first_item(values: Collection[int]) -> int:
return values.first()
Rules to keep in mind:
- Traits are abstract and must not be constructed directly with
TraitName(...). - A value annotated as
Collection[int]may be any concrete adopter of that trait instantiation. - Enum trait adoption uses the same
with TraitNameclause as model and class adoption. - For enum adopters, required trait methods must be declared in the enum body.
- Enum adopters should satisfy behavior through methods;
@requires(...)field contracts are usually for models/classes because enum payloads are variant data, not shared fields on the enum. - Supertrait relationships are transitive: if
OrderedCollection[T]adoptsCollection[T], adopters ofOrderedCollection[T]also satisfyCollection[T].
When an operation should only be available for values with specific capabilities, express that constraint in the type system with generic bounds instead of selectively hiding inherited trait methods:
def require_ordering[T with OrderedCollection[int]](values: T) -> T:
return values
The compiler enforces these bounds at call sites using nominal trait conformance, including transitive supertrait relationships.
Multiple instantiations of one generic trait¶
Models, classes, and enums may adopt the same generic trait more than once when each adoption uses different type arguments. This is useful when one type naturally supports the same capability for more than one static shape: indexing by str and by int, converting into multiple result types, or serializing through a generic format trait.
The repeated adoptions must be distinct:
trait Convert[T]:
def convert(self) -> T: ...
model Reading with Convert[int], Convert[float]: # OK
value: int
def convert(self) -> int:
return self.value
def convert(self) -> float:
return 1.0
This is trait dispatch, not general-purpose method overloading. The same method name is allowed here because each convert method satisfies a different Convert[T] adoption on the same type. Two ordinary methods with the same name are still rejected when they are not backed by distinct trait instantiations.
Dispatch from argument types¶
When the same trait method takes different value parameter types, the compiler selects the matching instantiation from the call arguments:
trait Reader[T]:
def read(self, key: T) -> str: ...
model Source with Reader[str], Reader[int]:
name: str
def read(self, key: str) -> str:
return key
def read(self, key: int) -> str:
return str(key)
source = Source(name="events")
by_name = source.read("latest") # Reader[str]
by_index = source.read(0) # Reader[int]
Named arguments participate in the same selection. source.read(key=0) selects the Reader[int] method because the named key argument has type int.
Dispatch from an expected return type¶
When the value arguments do not distinguish the candidates, the compiler may use an explicit expected return type. A typed binding is the most direct way to provide that context:
trait Convert[T]:
def convert(self) -> T: ...
model Reading with Convert[int], Convert[float]:
value: int
def convert(self) -> int:
return self.value
def convert(self) -> float:
return 1.0
reading = Reading(value=1)
as_float: float = reading.convert()
as_int: int = reading.convert()
Expected return type context can also come from a function argument, an annotated return position, or equivalent explicit type context. If no rule selects exactly one candidate, the call is ambiguous:
reading = Reading(value=1)
value = reading.convert() # error: the expected result type is not known
Generic bounds with trait type arguments¶
Generic bounds may carry trait type arguments. This lets a generic function say that a receiver type T must support a trait instantiation chosen by another type parameter:
trait Serializable[F]:
def serialize(self, format: F) -> bytes: ...
model JsonFormat:
name: str
model Event with Serializable[JsonFormat]:
message: str
def serialize(self, format: JsonFormat) -> bytes:
return b"{}"
def encode[F, T with Serializable[F]](value: T, format: F) -> bytes:
return value.serialize(format)
bytes = encode[JsonFormat, Event](Event(message="created"), JsonFormat(name="json"))
The bound T with Serializable[F] is not just a trait-name check. If F is JsonFormat, then T must adopt Serializable[JsonFormat]; adopting Serializable[YamlFormat] alone would not satisfy the bound.
Enum adopters¶
Enum declarations use the same rules as models and classes. Each repeated generic-trait adoption must use distinct type arguments, and each same-name method must satisfy a distinct adopted trait instantiation:
trait Label[T]:
def label(self) -> T: ...
enum Token with Label[str], Label[int]:
Identifier(str)
Number(int)
def label(self) -> str:
return "token"
def label(self) -> int:
return 1
token: Token = Token.Number(1)
text: str = token.label()
code: int = token.label()
Rejected cases¶
The compiler rejects repeated identical trait instantiations:
model BadReading with Convert[int], Convert[int]: # error
value: int
The compiler also rejects same-name methods that are not backed by distinct trait instantiations:
model Parser:
def parse(self, value: str) -> str: ...
def parse(self, value: int) -> str: ... # error
Same-name methods from unrelated trait families are rejected even if their value parameter types differ. Return-type and argument-type disambiguation only applies within one generic trait family:
trait ReadsInt:
def read(self, value: int) -> int: ...
trait ReadsStr:
def read(self, value: str) -> str: ...
model Source with ReadsInt, ReadsStr:
def read(self, value: int) -> int: ...
def read(self, value: str) -> str: ... # error: unrelated trait families use the same method name
Use distinct method names or distinct trait families with non-conflicting method names when you need two unrelated capabilities today. Explicit qualification and aliasing are future language design work.
@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/modeladopts a trait (with MyTrait), it must provide all required fields with compatible types. - Trait default methods may access adopter fields like
self.fieldonly if that field is declared in@requires(...). - Mutating adopter fields still requires
mut self(same as normal methods). - Required fields propagate through trait hierarchies: if
SubadoptsBase, adopters ofSubmust also satisfyBase's@requires(...)contract.
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, thena.__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)addsType.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)→strvalue.to_json()→strwhen the type imports and adoptsstd.serde.json.Serialize
@derive(Serialize)
model User:
name: str
age: int
def main() -> None:
u = User(name="Alice", age=30)
println(json_stringify(u))
from std.serde.json import Serialize
model User with Serialize:
name: str
age: int
def main() -> None:
println(User(name="Alice", age=30).to_json())
Deserialize¶
What it does: enables JSON parsing into a type.
API:
T.from_json(input: str)→Result[T, str]
Note: explicit with Deserialize adoption still needs either @derive(Deserialize) or a user-defined from_json(input) implementation.
@derive(Deserialize)
model User:
name: str
age: int
def main() -> None:
result: Result[User, str] = User.from_json("{\"name\":\"Alice\",\"age\":30}")
Validate (Models only)¶
What it does: enables validated construction for models.
API:
TypeName.new(...)→Result[TypeName, E]
Rule:
- If a
modelderivesValidate, you must construct it viaTypeName.new(...). Raw construction viaTypeName(...)is a compile-time error.
@derive(Validate)
model EmailUser:
email: str
def validate(self) -> Result[EmailUser, str]:
if "@" not in self.email:
return Err("invalid email")
return Ok(self)
def make_user(email: str) -> Result[EmailUser, str]:
return EmailUser.new(email=email)
See: Derives: Validation
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__() -> FrozenList[FieldInfo]__class_name__() -> str
Note:
- Field metadata like
[alias="..."]and[description="..."]is model-only. Forclass,FieldInfo.aliasandFieldInfo.descriptionare alwaysNoneandFieldInfo.wire_name == FieldInfo.name. u.__fields__()is typed asFrozenList[FieldInfo]directly by the compiler. ImportFieldInfoonly when you need to spell that type in an annotation.
See Reflection (Reference) for FieldInfo structure details.
model User:
name: str
def main() -> None:
u = User(name="Alice")
println(u.__class_name__())
println([f.name for f in u.__fields__()])
Stdlib derive boundaries¶
Traits under std.derives.* are source-defined capability contracts.
Clone,Default,Debug,Eq,Ord, andHashare declared in.incnsource.- Implementations for adopting types come from ordinary Rust
#[derive(...)]expansion during codegen. - These traits are not modeled as runtime helper calls through
incan_stdlib::derives::*.
For the curated stdlib-family view, see Standard library reference: std.derives.*.