Classes¶
A class is Incan’s behavior-first type: it models an object with methods, mutable state, and inheritance.
If you’re deciding between model and class, start with the Models & classes overview. This page focuses
on how classes behave once you’ve chosen them.
Quick start¶
class Counter:
value: int # (1)
def get(self) -> int: # (2)
return self.value
def increment(mut self) -> None: # (3)
self.value += 1
def main() -> None:
c = Counter(value=0) # (4)
c.increment()
println(c.get()) # (5)
println(c.value) # (6)
- Fields define object state. See Defining fields.
- Read-only methods take
self. See Methods and receivers (selfvsmut self). - Mutating methods take
mut self. See Methods and receivers (selfvsmut self). - Construction is keyword-only (named arguments). See Constructing a class.
- Running a method on an object returns the result of the method. See Methods and receivers (
selfvsmut self). - Member access uses canonical field names. See Field access.
The key ideas:
- A
classis behavior-first: methods are the primary API. - A
classcan be stateful: usemut selffor methods that modify fields. - A
classcan inherit: single inheritance withextendsfor behavior reuse and method overrides. - Schema mapping is not a class feature: classes don’t support field aliases/metadata; use a model for wire mapping.
Glossary
- field: a named piece of state stored on the object (e.g.
value: int) - method: a function defined on the class (e.g.
def increment(...)) - receiver: the first parameter of a method (
selformut self) - override: redefining a method in a child class so the child version is used
- composition: building larger types by putting one value inside another (e.g. a class holding a model)
Coming from Rust?
Incan classes compile to Rust structs + impls, but the surface syntax is closer to Python.
def m(self)corresponds to an immutable receiver (roughly like&self)def m(mut self)corresponds to a mutable receiver (roughly like&mut self)extendsis compile-time reuse + explicit overrides; it does not introduce subtyping
Coming from Python?
selfworks like Python’sself.mut selfis explicit: methods that modify fields must takemut self.- If you’re looking for Pydantic-like “data models” (DTOs, wire formats, schema mapping), prefer
model.
Coming from TypeScript / JavaScript?
selfis the object (roughly like JS/TSthis).- Construction is keyword-only (
Point(x=..., y=...)); there is noconstructor(...)method. extendsdoes not introduce subtyping (a child value can’t be used where the parent type is required); use traits/enums for polymorphism.
Defining fields¶
A class declares object state as typed fields.
- Syntax:
name: Type - Optional defaults:
name: Type = expr
Field metadata and aliases ([alias="..."], [description="..."], or as "...") are not supported on classes.
class UserService:
repo: UserRepository
logger_name: str
Constructing a class¶
Class construction is keyword-only (named arguments).
class Point:
x: int
y: int
def main() -> None:
p = Point(x=10, y=20)
This keeps call sites explicit and stable as you add/reorder fields.
Constructor keys are the declared field names (including inherited fields).
Coming from Python?
In Incan you don’t write an __init__/init method for classes; the declared fields (including inherited fields)
define the constructor keys.
Rules:
- No positional args:
Point(10, 20)is not supported. - Unknown fields are errors:
Point(z=1)is a type error. - Duplicates are errors:
Point(x=1, x=2)is a type error. - Missing required fields are errors: if a field has no default, you must pass it.
Field access¶
Access a field with dot syntax:
def main() -> None:
c = Counter(value=0)
println(c.value)
Because classes don’t support field aliases/metadata (the way a model would), member access always uses the canonical
field name.
Methods and receivers (self vs mut self)¶
Use self for read-only methods and mut self for methods that modify the object.
class Counter:
value: int
def get(self) -> int:
return self.value
def increment(mut self) -> None:
self.value += 1
Inheritance (extends) and overrides¶
Classes support single inheritance:
class Animal:
name: str
def speak(self) -> str:
return "..."
class Dog extends Animal:
breed: str
def speak(self) -> str:
return "Woof!"
A child class constructor includes inherited fields:
def main() -> None:
d = Dog(name="Rex", breed="Labrador") # `name` comes from `Animal`
println(d.speak())
Notes:
- Inheritance is for behavior reuse and method overrides.
extendsdoes not introduce subtyping: you cannot use aDogvalue where anAnimalvalue is required.- Overrides are explicit: if you define a method with the same name in a child class, it overrides the parent method.
If you only need a larger data shape, prefer composition with models. See Using models inside classes.
Trait composition (with ...)¶
Both a class and a model can implement traits:
trait Loggable:
def log(self, msg: str) -> None: ...
class Service with Loggable:
name: str
def log(self, msg: str) -> None:
println(f"[{self.name}] {msg}")
Traits are behavior-only (no storage). A trait default method may assume certain fields exist on the adopter; use
@requires(...) to declare that contract in a trait.
See:
Using models inside classes (common pattern)¶
This is composition: it’s common to use models for data and classes for behavior.
A simple example:
model AnimalData:
name: str
species: str
class AnimalService:
data: AnimalData
def describe(self) -> str:
return f"{self.data.name} ({self.data.species})"
A more complex example:
@derive(Serialize, Deserialize)
model User:
id: int
email: str
class UserService:
users: list[User]
def get_emails(self) -> list[str]:
return [u.email for u in self.users]
def main() -> None:
# defining the users (models)
alice = User(id=1, email="alice@example.com")
bob = User(id=2, email="bob@example.com")
zephod = User(id=42, email="zephod@example.com")
# defining the service (class)
service = UserService(users=[alice, bob, zephod])
println(service.get_emails()) # ["alice@example.com", "bob@example.com", "zephod@example.com"]
Reflection helpers¶
Classes provide:
__class_name__() -> str__fields__() -> FrozenList[FieldInfo]
Unlike models, classes do not support per-field aliases/metadata, so FieldInfo.alias and FieldInfo.description are
always None and FieldInfo.wire_name == FieldInfo.name.
Common pitfalls¶
- Using a class for schema mapping: if you need wire keys/aliases, use a model (and embed it in a class if needed).
- Forgetting
mut self: if a method assigns toself.field, it must takemut self. - Using inheritance for data reuse: prefer composition for data shapes; use
extendsfor behavior reuse and overrides.