Shaping Data
Most programs get easier to read once the data has names. A loose bag of strings and integers becomes a Job with an id, a queue, and an attempts counter. A value that is "sometimes a number and sometimes an error" becomes a Result with two variants. Shared behaviour lives on the type.
This chapter introduces Aura's two data shapes — classes and enums — together with methods, copy classes, and generics. It is deliberately not a feature checklist. The through-line is how to decide which shape fits your domain.
When To Use What
A useful first cut:
- Use a class when every field is present at the same time.
- Use an enum when exactly one variant is present at a time.
- Use a method when behaviour belongs to the type.
- Use a free function when behaviour coordinates several types.
The rest of the chapter fills those decisions in.
Start With A Class
Imagine a small job runner. A job has an identifier, a queue name, and an attempt count:
class Job:
id: int32
queue: str
attempts: int32 = 0Construct an instance with named fields:
job = Job(id=42, queue="image")Fields can have defaults. The caller above did not supply attempts, so it starts at 0.
By default, classes are move types. A bare class parameter borrows; write own to transfer ownership:
def consume(job: own Job):
print(job.id)
job = Job(id=42, queue="image")
consume(job)
# job has been moved into consume; using it again is a compile error.When a helper only needs to look at a job, borrow it:
def describe(job: Job) -> str:
return job.queue + "#" + job.id.to_string()The caller keeps the value and can use it again. The call site writes describe(job); Aura reads the borrow form from the parameter type.
Add Methods
Methods are functions declared inside a class. The receiver — how self is named in the signature — says what the method is allowed to do.
class Job:
id: int32
queue: str
attempts: int32 = 0
def bump(mut self):
self.attempts += 1
def label(self) -> str:
return self.queue + "#" + self.id.to_string()Use it:
mut job = Job(id=42, queue="image")
job.bump()
print(job.label())Receiver forms:
| Receiver | What it can do |
|---|---|
self | Read fields without taking ownership; this is the default spelling. |
self | Explicit synonym for shared self. |
mut self | Mutate fields on a mutable receiver. |
own self | Consume the instance. |
| no receiver | Associated method called on the type, not an instance. |
A borrowed method cannot move an owned field out of self. When the field type supports cloning, clone when you need to return an owned copy:
class User:
name: str
def name_copy(self) -> str:
return self.name.clone()Returning self.name directly would move the str through a shared borrow, which the compiler rejects. The clone makes the intention explicit and the reader does not have to guess.
An associated method is called on the type itself — useful for constructors and factories:
class Counter:
value: int32 = 0
def zero() -> Counter:
return Counter()counter = Counter.zero()Copy Classes
Some records are so small that treating them as move values is more ceremony than it is worth. When every field is itself a copy type, declare the class copy class:
copy class Offset:
x: int32
y: int32Copy classes duplicate on assignment:
a = Offset(x=1, y=2)
b = a
print(a.x)
print(b.x)This is not a way to opt out of ownership when it feels inconvenient. Reach for copy class when duplication is part of the type's nature — coordinates, simple numeric measurements, identifiers made entirely of copyable fields.
Model Alternatives With Enums
An enum describes a value that is exactly one of several shapes. A job in flight, for instance, is always in one of four states: queued, running, done, or failed.
enum JobState:
Queued
Running(worker: str)
Done(duration: Duration)
Failed(message: str)Construct a variant by naming it:
state = JobState.Running(worker="worker-a")match then inspects the variant exhaustively:
def render_state(state: JobState) -> str:
return match state:
case JobState.Queued:
"queued"
case JobState.Running(worker):
"running on " + worker
case JobState.Done(_duration):
"done"
case JobState.Failed(message):
"failed: " + messageTwo details are worth noticing. match state inspects the enum without taking ownership, which is important because state is itself a JobState. And the _duration name uses the leading underscore convention for a pattern binding that the body does not read.
When each state carries different data, an enum almost always reads better than a class with many optional fields.
Combine Classes And Enums
A class can own an enum, and often should. This shape — a stable record with a changing state — is one of the cleanest patterns in Aura.
class TrackedJob:
job: Job
state: JobState = JobState.Queued
def mark_running(mut self, worker: own str):
self.state = JobState.Running(worker=worker)
def mark_failed(mut self, message: own str):
self.state = JobState.Failed(message=message)The fields that never change live on the class. The field that does change is an enum, so the compiler can help make sure every transition is handled.
Generic Data
Classes and enums can be parameterised by type. Box[T] holds some T; Load[T] represents a value that has either arrived, is still absent, or has failed:
class Box[T]:
value: T
enum Load[T]:
Ready(value: T)
Empty
Failed(message: str)Generic types let you write utility data structures without giving up the type of the stored value. Generics And Traits in the Manual covers the details.
Design Notes
Three habits keep Aura data types clean:
- Prefer small classes with meaningful fields. A class with ten unrelated fields is often two classes waiting for names.
- Prefer enums for domain states.
JobState.Failed(message=...)is harder to misuse than a"failed"string plus a maybe-empty error field. - Prefer methods for type-local behaviour. A function that reads one class's fields usually belongs to that class. A function that coordinates several types is usually a free function.
The next chapter takes the same ideas into Aura's standard collections — where the classes and enums we just built start to form programs.
Reference: Classes, Enums And Pattern Matching.