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
Static methods (@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, 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(...). @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).
Another example — a utility method that doesn't need instance state:
class MathUtils:
precision: int = 2
@staticmethod
def clamp(value: int, low: int, high: int) -> int:
if value < low:
return low
if value > high:
return high
return value
def main() -> None:
println(MathUtils.clamp(15, 0, 10)) # 10
See also: Decorators reference
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.