Models and Classes in Incan¶
Incan provides two ways to define types with fields: model and class.
Understanding when to use each is key to writing idiomatic Incan code.
Quick Comparison¶
| Aspect | model |
class |
|---|---|---|
| Purpose | Data containers | Objects with behavior |
| Focus | Fields first | Methods first |
| Inheritance | No (cannot inherit) | Yes (single inheritance) |
| Traits | Yes (behavior via traits) | Yes (behavior via traits) |
| Use when | “This is data” | “This is an object/service” |
When to Use Which¶
| Use Case | Choose | Why |
|---|---|---|
| Config, settings | model |
Pure data, no behavior needed |
| DTO, API payload | model |
Data transfer, serialization focus |
| Database record | model |
Represents stored data |
| Service with methods | class |
Has operations/behavior |
| Stateful controller | class |
Methods that modify state |
| Needs inheritance | class |
Only class supports extends |
Coming from Python or Rust?
A rough mapping (just to orient yourself):
Python -> Incan
- Python
@dataclass/ “plain data” / "pydantic BaseModel" ->model - Python “class with behavior” ->
class
Rust -> Incan
Both model and class compile to Rust structs + impls.
The difference is semantic:
classcan add inheritance (extends) and trait composition (with ...)- the compiler resolves this at compile time by generating fields/methods/trait impls
In practice, this is still “zero-cost”: choose model vs class for clarity and API design, not for performance.
Why no struct keyword?
If you're coming from Rust, you might wonder: "Where's struct?"
Incan uses model instead because:
- Python familiarity — Python developers know
class, notstruct - Clearer semantics —
modelsays "this is data", not "this is a memory layout" - Tooling conventions — ORMs, validators, serializers all use "model" terminology
Under the hood, both model and class compile to Rust structs.
Visibility (pub)¶
Control whether declarations are importable from other modules.
- Items are private by default (just like in Rust).
- Prefix a declaration with
pub(publicly visible) to make it importable from other modules. - For
modelandclass,pubalso makes fields public by default.
model User: # private by default
name: str
pub model PublicUser: # public model, fields are now public by default
name: str
class Service: # private by default
repo: Repo
def work(self) -> None:
...
Coming from Python?
In Python, attribute visibility is a convention.
In Incan, item visibility is part of the language surface: items are private by default unless you mark them pub.
You have to specifically opt-in to make items public.
Model: Data-First¶
Models are for data containers — types where the fields are the primary concern.
# Simple data model
model Point:
x: int
y: int
# With derives for common behaviors
@derive(Eq, Hash, Serialize)
model User:
id: int
name: str
email: str
# With default values
model Config:
host: str = "localhost"
port: int = 8080
debug: bool = false
Coming from Python?
An Incan model can be compared to a Python dataclass or Pydantic BaseModel in spirit.
What Models Get Automatically¶
By default, models (and classes) get Debug and Clone. Use @derive(...) to add additional behavior.
# Debug + Clone are implicit:
model User:
name: str
# You only need @derive(...) when you want extra traits:
@derive(Eq, Hash, Serialize)
model User:
name: str
Models Cannot Inherit¶
Models intentionally do not support inheritance. Incan keeps model as a data-first construct:
- inheritance makes the data shape implicit (fields come from “somewhere else”),
- complicates defaults/constructors and derives/serialization, and
- introduces override-style complexity that belongs in
class.
Prefer composition to keep the structure explicit and tooling predictable.
model Base:
id: int
# ❌ Inheritance is NOT allowed
model Child extends Base: # Error!
name: str
model Child:
base: Base # ✅ Use composition instead
name: str
Class: Behavior-First¶
Classes are for objects with behavior — types where methods are the primary concern.
trait Loggable:
def log(self, msg: str) -> None: ...
class UserService with Loggable:
repo: UserRepository
logger_name: str
def log(self, msg: str) -> None:
println(f"[{self.logger_name}] {msg}")
def create_user(self, name: str, email: str) -> Result[User, ServiceError]:
self.log(f"Creating user: {name}")
user = User(id=next_id(), name=name, email=email)
return self.repo.save(user)
def find_user(self, id: int) -> Option[User]:
return self.repo.find_by_id(id)
Classes Support Inheritance¶
class Animal:
name: str
def speak(self) -> str:
return "..."
class Dog extends Animal:
breed: str
def speak(self) -> str:
return "Woof!"
class Cat extends Animal:
indoor: bool
def speak(self) -> str:
return "Meow!"
Mutable Methods¶
Use mut self when a method modifies the object:
class Counter:
value: int
def get(self) -> int:
return self.value
def increment(mut self) -> None:
self.value = self.value + 1
def add(mut self, n: int) -> None:
self.value = self.value + n
Both Can Implement Traits¶
trait Describable:
def describe(self) -> str: ...
# Model implementing a trait
model Product with Describable:
name: str
price: float
def describe(self) -> str:
return f"{self.name}: ${self.price}"
# Class implementing a trait
class Employee with Describable:
name: str
title: str
def describe(self) -> str:
return f"{self.name}, {self.title}"
Design Philosophy¶
Incan's type system follows the principle: Python's vocabulary, Rust's guarantees.
flowchart LR
subgraph Python
py_dataclass["@dataclass"]
py_class["class"]
py_protocol["ABC / Protocol"]
end
subgraph Incan
in_model["model"]
in_class["class"]
in_trait["trait"]
end
subgraph Rust
rs_struct["struct"]
rs_struct_impl["struct + impl"]
rs_trait["trait"]
end
py_dataclass --> in_model --> rs_struct
py_class --> in_class --> rs_struct_impl
py_protocol --> in_trait --> rs_trait
You think in Python terms, but get Rust's:
- Zero-cost abstractions
- Memory safety without GC (Garbage Collection)
- Compile-time guarantees
See Also¶
- Derives and Traits — Adding behaviors with
@derive - Error Handling — Using
ResultandOption - Example: models_vs_classes.incn