10. Models vs classes¶
Incan has two “types with fields”:
model: data-first (great for DTOs, configs, payloads)class: behavior-first (methods + possible mutation)
This is not just a naming convention
model and class are fundamentally different:
- Models define data shapes and support schema-focused features like field metadata/aliases.
- Classes define behavior and support inheritance and method overrides.
Choosing between model and class¶
Start with a model when:
- you’re defining a data shape (DTOs, configs, payloads)
- you care about wire/schema mapping (field aliases/metadata)
Start with a class when:
- you’re defining an object with behavior (methods are the primary API)
- you need mutable state (
mut self) or inheritance (extends)
Coming from Python?
A model is closest to a Python @dataclass (or a Pydantic Basemodel) in spirit: you declare fields, and you get
a constructor automatically.
In Incan you don’t write an __init__/init method. You construct values with keyword arguments matching the declared
fields (as shown below), and you can give fields default values in the declaration when needed. For validation or alternate
construction, write a separate helper/factory function (often returning Result).
A model (data-first)¶
A simple model¶
model User:
name: str
age: int
def main() -> None:
u = User(name="Alice", age=30)
println(f"{u.name} age={u.age}")
How models work (what users trip over)¶
Construction is field-based (named args + defaults)¶
Models are constructed by naming fields. Defaults make a field optional at construction time.
model Config:
host: str = "localhost"
port: int = 8080
def main() -> None:
c1 = Config() # ok (all defaults)
c2 = Config(port=3000) # ok
c3 = Config(host="0.0.0.0") # ok
Schema-safe field names (aliases) are model-only¶
When a wire format uses keywords like "type" or "from", keep a safe canonical name in code and map the wire key with
an alias.
@derive(Serialize, Deserialize)
model Account:
type_ as "type": str
def demo(a: Account) -> str:
# In code, use the canonical name.
return a.type_
Classes do not support field metadata/aliases. If you need schema mapping, use a model (or embed a model in a class).
For the full alias behavior (including where aliases can be written in code), see: Models.
A class (behavior-first)¶
A simple class¶
class Counter:
value: int
def increment(mut self) -> None:
self.value += 1
def main() -> None:
c = Counter(value=0)
c.increment()
c.increment()
println(f"value={c.value}") # outputs: value=2
Try it¶
- Create a
model Productwithnameandprice. - Create a
class Cartwith a list of products and a method to compute a total. - Print the total.
One possible solution
model Product:
name: str
price: float
class Cart:
items: list[Product]
def total(self) -> float:
total = 0.0
for item in self.items:
total = total + item.price
return total
def main() -> None:
cart = Cart(items=[
Product(name="Book", price=10.0),
Product(name="Pen", price=2.5),
])
println(f"total={cart.total()}")
Where to learn more¶
This chapter covers the basics. For the full feature surface—model schema mapping (aliases/metadata), derives
(serialization/validation), reflection (__fields__()), and class features like inheritance and trait composition—see the
guides and reference pages below.
- Full guide and decision table: Models & Classes
- Models (deep dive): Models
- Classes (deep dive): Classes
- Derives (reference): Derives: Serialization and Derives: Validation
- Reflection (reference): Reflection
- Error handling (deep dive): Error handling
Next¶
Back: 9. Enums and better match
Next chapter: 11. Traits and derives