Skip to content

Enums in Incan

Enums in Incan are algebraic data types (ADTs): a type with a closed set of variants, where each variant can carry different data.

You use enums when a value can be one of a few well-defined shapes and you want the compiler to enforce that you handle every case.

Enums can also own behavior. Put methods and associated functions in the enum body when the behavior belongs to the closed set itself, and use with TraitName when the enum should participate in the same trait-based protocols as models and classes.

Coming from Python?

Python’s Enum is mainly “named constants”. When Python code needs variants with data it often ends up using class hierarchies and isinstance(...) checks, which are not exhaustive and are easy to break during refactors.

Here’s one representative before/after:

Python (common workaround):

class Shape:
    pass

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

def area(shape: Shape) -> float:
    if isinstance(shape, Circle):
        return 3.14159 * shape.radius * shape.radius
    elif isinstance(shape, Rectangle):
        return shape.width * shape.height
    raise ValueError("unknown shape")

Note: Python 3.10+ has match/case, but it still won’t enforce exhaustiveness the way Incan does.

Incan (enum + exhaustive match):

enum Shape:
    Circle(float)
    Rectangle(float, float)

def area(shape: Shape) -> float:
    match shape:
        Circle(r) => return 3.14159 * r * r
        Rectangle(w, h) => return w * h

If you add a new variant later, the compiler will point you at the match sites that must be updated.

Coming from Rust?

This is the same concept as Rust’s enum + exhaustive match (sum types with payload-carrying variants).

Differences are mostly surface syntax: - Variants are declared in an indented block under enum (no braces/commas). - Construction uses Incan’s syntax (e.g. Status.Active / Message.Move(1, 2)), rather than Rust’s Type::Variant(...).

A motivating example

enum Shape:
    Circle(float)          # radius
    Rectangle(float, float)  # width, height
    Triangle(float, float)   # base, height

def area(shape: Shape) -> float:
    match shape:
        Circle(r) => return 3.14159 * r * r
        Rectangle(w, h) => return w * h
        Triangle(b, h) => return 0.5 * b * h

If you add a new variant later, the compiler will point you at the match sites that must be updated.

Basic syntax

Simple Enum (No Data)

enum Status:
    Pending
    Active
    Completed
    Cancelled

Usage:

status = Status.Active

match status:
    Pending => println("Waiting...")
    Active => println("In progress")
    Completed => println("Done!")
    Cancelled => println("Aborted")

Enum with Data (Variants)

Each variant can carry different types and amounts of data:

enum Message:
    Quit                           # No data
    Move(int, int)                 # Two ints (x, y)
    Write(str)                     # A string
    ChangeColor(int, int, int)     # RGB values

Usage:

msg = Message.Move(10, 20)

match msg:
    Quit => println("Goodbye")
    Move(x, y) => println(f"Moving to ({x}, {y})")
    Write(text) => println(f"Message: {text}")
    ChangeColor(r, g, b) => println(f"RGB({r}, {g}, {b})")

Declaration shape

The optional with clause belongs on the enum header:

enum ResultState with Describable:
    Success
    Failure(str)

For generic enums, put with after the type parameters:

enum Maybe[T] with Describable:
    Some(T)
    None

For value enums, put with after the backing type:

enum Environment(str) with Describable:
    Development = "development"
    Production = "production"

Inside the enum body, declare variants first and then methods. After the first method declaration, the rest of the body is method territory; do not add more variants below methods. That keeps the visual shape predictable: data cases first, behavior second.


Value enums

Use a value enum when each variant needs one canonical external str or int representation:

enum Environment(str):
    Development = "development"
    Production = "production"

enum HttpStatus(int):
    Ok = 200
    NotFound = 404

Value enum variants are still enum values, not primitive values. Environment.Production has type Environment, not str, and HttpStatus.NotFound has type HttpStatus, not int.

Value enums gain two helper methods that cover both directions between typed enum variants and their external values, value and from_value; value() returns the backing primitive type. from_value(...) accepts the backing primitive type and returns Option[Enum] so unknown external values are explicit.

For example:

def status_code(status: HttpStatus) -> int:
    return status.value()

def parse_status(code: int) -> Option[HttpStatus]:
    return HttpStatus.from_value(code)

Value enums are for closed value tables, not for variants that carry additional data. Each variant is a single named case with one raw str or int value:

enum Environment(str):
    Development = "development"
    Production = "production"

If a variant needs payload fields, use a regular enum instead:

enum JobState:
    Queued
    Running(str)      # worker id
    Failed(str, int)  # message, retry count

Value enums also cannot be generic. The backing value table is concrete, so every variant value must be explicit, must match the declared backing type, and must be unique within the enum.


Generic enums

Enums can be generic — parameterized over types:

enum Option[T]:
    Some(T)
    None

enum Result[T, E]:
    Ok(T)
    Err(E)

Note: These are Incan's built-in types for handling optional values and errors.

Custom Generic Enum

enum Tree[T]:
    Leaf(T)
    Node(Tree[T], Tree[T])

# A binary tree of integers
tree = Node(
    Leaf(1),
    Node(Leaf(2), Leaf(3))
)

Methods and associated functions

Enum bodies may declare methods after their variants. Use instance methods when the operation depends on the selected variant:

enum Direction:
    North
    South
    East
    West

    def is_horizontal(self) -> bool:
        match self:
            Direction.East => return true
            Direction.West => return true
            _ => return false

    def opposite(self) -> Direction:
        match self:
            Direction.North => return Direction.South
            Direction.South => return Direction.North
            Direction.East => return Direction.West
            Direction.West => return Direction.East

Call enum methods on enum values:

dir = Direction.East

if dir.is_horizontal():
    println("moving sideways")

Methods may use the enum's type parameters. This is useful for small helpers on Option-like enums:

enum Maybe[T]:
    Some(T)
    None

    def unwrap_or(self, fallback: T) -> T:
        match self:
            Maybe.Some(value) => return value
            Maybe.None => return fallback

Use associated functions for constructors or helpers that belong to the enum type rather than a particular instance. An associated enum function has no self receiver and is called through the enum type:

enum Direction:
    North
    South
    East
    West

    def default() -> Self:
        return Direction.North

Call it with type-name method syntax:

dir = Direction.default()

You can also mark no-receiver helpers with @staticmethod when that makes the intent clearer:

enum Direction:
    North
    South
    East
    West

    @staticmethod
    def all() -> list[Direction]:
        return [Direction.North, Direction.South, Direction.East, Direction.West]

Enum methods follow the same receiver model as methods on models and classes. They do not change matching or construction semantics; variants are still the closed set of cases, and match remains exhaustive.

Rules to keep in mind:

  • Instance methods take self or mut self.
  • Associated functions take no self receiver and are called as EnumName.method(...).
  • Self means the declaring enum type, including active generic type arguments.
  • Variants remain constructors or values; adding methods does not make an enum open-ended.

Trait adoption

Enums can adopt traits with with:

trait Describable:
    def describe(self) -> str: ...

enum BuildState with Describable:
    Queued
    Running(str)
    Failed(str)

    def describe(self) -> str:
        match self:
            BuildState.Queued => return "queued"
            BuildState.Running(worker) => return f"running on {worker}"
            BuildState.Failed(message) => return f"failed: {message}"

This uses the same trait-adoption surface as models and classes. The enum must provide the required trait behavior, and values of the enum are accepted where the adopted trait is expected:

def log_state(state: Describable) -> None:
    println(state.describe())

log_state(BuildState.Queued)

Explicit enum adoption also satisfies generic trait bounds:

def keep_describable[T with Describable](value: T) -> T:
    return value

state = keep_describable(BuildState.Queued)

This matters when a library API is written against a reusable capability instead of one concrete enum type.

Value enums can adopt traits too:

trait ExternalValue:
    def external(self) -> str: ...

enum Environment(str) with ExternalValue:
    Development = "development"
    Production = "production"

    def external(self) -> str:
        return self.value()

Trait adoption is additive. An enum without a with clause behaves exactly like before, and adopting a trait does not make the enum open-ended; its variants remain closed.

Rules to keep in mind:

  • Required trait methods must be implemented in the enum body.
  • Trait methods with default bodies can be inherited when their requirements are satisfied.
  • Generic traits use the same syntax as other adopters: enum Lookup with Index[str, int]:.
  • Traits that require adopter fields with @requires(...) are usually a model/class fit; enum variant payloads are not shared fields on the enum itself.

Pattern matching

The match expression is how you work with enums. It's exhaustive — the compiler ensures you handle all variants.

Basic Match

enum Direction:
    North
    South
    East
    West

def describe(dir: Direction) -> str:
    match dir:
        North => return "Going up"
        South => return "Going down"
        East => return "Going right"
        West => return "Going left"

Extracting Data

enum ApiResponse:
    Success(str, int)        # (data, status_code)
    Error(str)               # error message
    Loading

def handle(response: ApiResponse) -> None:
    match response:
        Success(data, code) =>
            println(f"Got {code}: {data}")
        Error(msg) =>
            println(f"Failed: {msg}")
        Loading =>
            println("Please wait...")

Wildcard Pattern

Use _ to match any remaining variants:

match status:
    Active => println("Working on it")
    _ => println("Not active")  # Matches Pending, Completed, Cancelled

Warning: Wildcards can hide bugs when you add new variants. Prefer explicit matches.

Guards

Add conditions to patterns:

enum Temperature:
    Celsius(float)
    Fahrenheit(float)

def describe(temp: Temperature) -> str:
    match temp:
        Celsius(c) if c > 30 => return "Hot (Celsius)"
        Celsius(c) if c < 10 => return "Cold (Celsius)"
        Celsius(_) => return "Moderate (Celsius)"
        Fahrenheit(f) if f > 86 => return "Hot (Fahrenheit)"
        Fahrenheit(f) if f < 50 => return "Cold (Fahrenheit)"
        Fahrenheit(_) => return "Moderate (Fahrenheit)"

Common patterns

For practical recipes (state machines, commands, error types, expression trees), see:


Derives and docstrings

Enums support @derive(...) decorators and docstrings:

@derive(Serialize, Deserialize)
enum Status:
    """Represents the current state of a task."""
    Pending
    Active
    Completed

For serialization details, see Derives: Serialization.


Common pitfall: enums are not lookup tables

Incan enums are algebraic types — each variant is a fixed tag, optionally carrying data. Ordinary enums are not key-value mappings or integer-valued constants. If you need one canonical string or integer representation per variant, use a value enum.

The compiler will catch the mistake early with a targeted error message:

# Rejected: ordinary enums cannot have mapped values.
enum Categories:
    GROCERIES => Category("Groceries")   # "cannot have mapped values"

# Rejected: variants cannot contain dotted names.
enum FlowType:
    Cash.Inflow                           # "cannot contain dots"

# Rejected: ordinary enums cannot assign raw values.
enum Color:
    Red = 1                               # "cannot have assigned values"

Instead, use plain variants for the enum and a separate model for rich data:

enum CategoryKey:
    Groceries
    Utilities

model Category:
    key: CategoryKey
    description: str

def all_categories() -> list[Category]:
    return [
        Category(key=CategoryKey.Groceries, description="Food items"),
        Category(key=CategoryKey.Utilities, description="Gas, electric"),
    ]

Enums vs models vs classes

Use Case Enum Model Class
Fixed set of variants
Data that can be one of several shapes
Exhaustive handling required
Behavior tied to a closed set
Trait adoption with with
Simple data container (DTO, config)
Serialization (@derive)
Validation and defaults
Inheritance/polymorphism needed
Mutable state with methods
Open extension (new types later)
# Enum: closed set, exhaustive matching
enum PaymentMethod:
    CreditCard(str, str)     # number, expiry
    PayPal(str)              # email
    BankTransfer(str, str)   # account, routing

# Model: data-first, serialization
@derive(Serialize, Deserialize)
model PaymentRequest:
    method: PaymentMethod
    amount: float
    currency: str = "USD"

# Class: behavior-first, inheritance
class PaymentProcessor:
    def process(self, amount: float) -> Result[Receipt, Error]:
        ...

See also: Models and Classes Guide


Built-in enums

Incan provides these enums in the standard library:

Option[T]

Represents an optional value:

enum Option[T]:
    Some(T)
    None

See: Error Handling Guide

Result[T, E]

Represents success or failure:

enum Result[T, E]:
    Ok(T)
    Err(E)

See: Error Handling Guide

Ordering

Comparison result:

enum Ordering:
    Less
    Equal
    Greater

Summary

Concept Description
enum Define a type with fixed variants
Variants Each case of an enum, optionally with data
Value enum Enum with canonical str / int raw values
Generic enum Enum parameterized over types: Option[T]
Methods Behavior declared inside the enum body
with Trait Trait adoption for enum values
match Exhaustive pattern matching on enums
Destructuring Extract data from variants: Some(x) =>

Enums are one of Incan's most powerful features — use them for:

  • Modeling states and state machines
  • Error types with rich context
  • Command/message types
  • Any "one of these things" scenario

The compiler guarantees you handle all cases, eliminating a whole class of bugs caused by missing or forgotten cases.


See Also

  • Error Handling — Using Result and Option
  • Match expressions: see the language docs and examples in this section
  • Models and Classes — When to use class vs enum