Mojo trait declarations
A trait defines requirements that a conforming type must satisfy including methods, associated types, and constants. Traits are similar to protocols in Swift, interfaces in Java, and traits in Rust. When a type conforms to a trait, the compiler checks every requirement and rejects the code if anything is missing.
Trait declarations
Traits are declared with the trait keyword followed by the trait name and
an optional refinement list. The body contains the trait's requirements,
both required and provided:
trait Name:
body
trait Name(ParentA, ParentB):
bodyTraits must be declared at the top level of a file. They can't be nested inside structs, other traits, or functions:
struct Outer:
trait Inner: # Error: nested trait not supported here
...Marker traits
Empty traits are called marker traits and signal that a type has a specific property or capability without refining other traits or declaring requirements:
trait AnyType:
passMojo marker traits include: AnyType, TrivialRegisterPassable,
RegisterPassable, and ImplicitlyCopyable. They tell the compiler
about a type's properties, supporting compile-time optimizations.
Trait body elements
A trait body can contain these elements:
| Element | Syntax | Role |
|---|---|---|
| Required method | ... body | Conforming types must implement |
| Provided method | Code body | Inherited unless overridden |
| Comptime member - associated type | comptime Name: Trait | Conforming types provide a concrete type |
| Comptime member - required value | comptime name: Type | Conforming types provide a value |
| Comptime member - constant | comptime name = value | Shared across all conforming types |
Trait and member names
Trait names must be valid identifiers. By convention, they
describe a capability: Writable, Hashable, Copyable.
Naming conventions
| Element | Naming | Notes |
|---|---|---|
| Trait name | PascalCase | Capabilities gained by conformance: Equatable, Copyable, PathLike. |
| Instance method | lower_snake_case() | Method's action: write_to(), update() |
| Static method | lower_snake_case() | Method's action: get_type_name, get_element_bitwidth |
comptime members | Trait compositions are PascalCase. Values are lower_snake_case | Describes use: KeyElement, element_bitwidth |
| Associated type | PascalCase | Describes use: Element, Iterator |
Methods
Traits define both required and provided methods, and both instance and
static members. Instance methods take self as the first parameter. Static
methods require the @staticmethod decorator.
Required methods
An ellipsis (...) marks a required method. Conforming types must provide
an implementation:
trait RequiredMethods:
def required_method(self):
...
@staticmethod
def required_static_method():
...
@fieldwise_init
struct SampleStruct(RequiredMethods):
def required_method(self):
print("Required method")
@staticmethod
def required_static_method():
print("Required static method")
def main():
var s = SampleStruct()
s.required_method() # Required method
SampleStruct.required_static_method() # Required static methodProvided methods
A method with a body other than ... provides a default implementation.
Conforming types automatically receive that behavior but can also override
it:
trait ProvidedMethods:
def provided_method(self):
print("Provided method")
@staticmethod
def provided_static_method():
print("Provided static method")
@fieldwise_init
struct SampleStruct(ProvidedMethods):
def provided_method(self):
print("Overridden provided method")
def main():
var sample = SampleStruct()
sample.provided_method() # Overridden provided method
SampleStruct.provided_static_method() # Provided static methodBoth required and provided methods can return values:
trait Describable:
def provided_describe(self) -> String:
return "no description" # Type can override this implementation
def required_describe(self) -> String:
... # Type must provide an implementation for this methodProvided behavior can't use implementation details from any specific conforming type, as a trait has no knowledge of a type's capabilities beyond those declared in the trait and refinement list. It must work across all of them.
pass vs ...
pass and ... mean distinct things in trait bodies:
...marks a required method stub.passis a no-op that counts as a provided implementation body. It's only valid when the method returnsNone.
If a method declares a return type but uses pass as its body,
the compiler will encourage you to replace it with ...:
trait Unsupported:
def __compute__(self) -> Int:
pass
# Error: trait method has results but default
# implementation returns no value; did you mean '...'?Comptime members - associated types
An associated type is a comptime member that declares a related
subordinate type that conforming types must specify. For example, an
associated type might be the Element type for a container or
collection trait, or the Key and Value types for a map.
The associated type is declared as a comptime member without an
initializer. Only traits can use this declaration form. Conforming structs
provide the concrete value that satisfies any constraints declared in the
trait.
In the following example, Self refers to the conforming type, so
Self.Associated refers to the conforming type's value for the associated
type Associated:
trait Boxable:
comptime Associated: Writable & Copyable & ImplicitlyDestructible
def unbox(self) -> Self.Associated:
...
@fieldwise_init
struct ConcreteBox(Boxable):
comptime Associated = String
var value: Self.Associated
def unbox(self) -> Self.Associated:
return self.value.copy()
def main():
var box = ConcreteBox(value="Hello")
var unboxed = box.unbox() # Known to be Copyable
print(unboxed) # Known to be Writable
_ = unboxed^ # Known to be ImplicitlyDestructibleAn associated type can also be assigned from call sites with a generic parameter. This lets the trait work across a family of types:
comptime Base = Copyable & ImplicitlyDestructible & Writable
@fieldwise_init
struct Box[T: Base](Boxable):
comptime Associated = Self.T
var value: Self.Associated
def unbox(self) -> Self.Associated:
return self.value.copy()A comptime without an assignment, type, or traits is an error:
trait Unsupported:
comptime X
# Error: expected '=' after comptime declaration
trait Supported:
comptime X: Copyable # OK: associated type
comptime y: Int # OK: required value
comptime z = 42 # OK: constantOutside of traits, a comptime member without an initializer is an error:
struct Unsupported:
comptime X: Int
comptime Y: Copyable
# Error: only traits may contain a comptime member
# without an initializerComptime members - constants and required values
A trait can declare comptime constants (shared value) and required
assignments (conforming types must provide a value).
Constants
This trait provides a usable bitwidth for conforming types based on
the Element associated type and a named trait composition:
from std.sys.info import bit_width_of
trait Test:
comptime Element: RegisterPassable
comptime element_bitwidth = bit_width_of[Self.Element]()
comptime KeyElement = Copyable & ImplicitlyDestructible & Writable
@fieldwise_init
struct SampleStruct[T: KeyElement](Test):
comptime Element = Int64
var x: Self.T
def show_element_bitwidth(self):
print(Self.element_bitwidth)
def main():
var s = SampleStruct[Int64](x=42)
s.show_element_bitwidth() # 64
print(s.x) # 42Comptime members: required values
A required value is a comptime member without an initializer. Conforming
types must provide a value for it. This is useful when the trait needs a
compile-time constant that varies across conforming types. For example, a
Measurable trait might require a unit string and an always_positive
boolean to validate measurements.
In this example, the trait requires requires conforming types to provide a
unit string, an always_positive boolean, and a get_value() method.
The validate() function uses those requirements to check that the value
is positive when always_positive is True:
trait Measurable:
comptime unit: StaticString # Required
comptime always_positive: Bool # Required
def get_value(self) -> Float64: ... # Required method
def validate[T: Measurable](measurement: T) raises:
comptime if T.always_positive:
if Float64(measurement.get_value()) < 0.0:
raise Error(t"{T.unit} cannot be negative")
@fieldwise_init
struct Pascals(Measurable):
comptime unit: StaticString = "Pa"
comptime always_positive: Bool = True
var value: Float64
def get_value(self) -> Float64:
return self.value
def main() raises:
validate(Pascals(value=101325.0)) # Validation succeeds
# validate(Pascals(value=-101325.0)) # Validation fails
# run-time error:
# "Unhandled exception caught during execution: Pa cannot be negative"A similar conformance for DegreesCentigrade would set the unit to
"°C" and always_positive to False. A value of -10.0 would pass
validation for DegreesCentigrade and fail for Pascals.
Trait refinement
When a trait refines another, its bound implies the parent's bound:
trait Printable:
def to_string(self) -> String:
...
trait PrettyPrintable(Printable):
def to_pretty_string(self) -> String:
...
@fieldwise_init
struct Box[T: Copyable & Writable](PrettyPrintable):
var value: Self.T
def to_string(self) -> String:
return String(t"Box({self.value})")
def to_pretty_string(self) -> String:
return String(t"Box with value: {self.value}")
def render[T: PrettyPrintable](item: T):
# to_string() available: PrettyPrintable refines Printable
print(item.to_string(), "-", item.to_pretty_string())
def main():
render(Box(1)) # Box(1) - Box with value: 1
render(Box("hello")) # Box(hello) - Box with value: helloA function requiring T: PrettyPrintable can call any method
from Printable without naming it in the bound.
Bound resolution order
When the compiler resolves a trait bound:
- The parameter's declared traits are checked first.
- Parent traits are included transitively.
- If the bound uses
&, all composed traits must be satisfied. - If a
whereclause is present, its constraints are checked after parameter-level bounds.
If any check fails, the compiler reports which trait the type doesn't conform to.
A child trait can override a parent method by declaring a method with the same signature. The parent's version is replaced in the child's requirements.
Trait restrictions
Traits don't support parameter lists:
trait Unsupported[T]: # Error: trait declarations do not support
... # parametersTraits can't declare or use fields:
trait Unsupported:
var x: Int # Error: traits do not support 'var' fieldsTraits don't support where clauses on methods:
trait Unsupported:
def maybe(self) -> Int where Self: Sized:
...
# Error: 'where' clauses on trait methods are not supportedConformance checks
The compiler checks every requirement and errors on unmet ones.
Missing methods
trait Sized:
def __len__(self) -> Int: ...
@fieldwise_init
struct SizedStruct[T: Copyable](Sized):
var backing_store: List[Self.T]
# Error: 'SizedStruct[T]' does not implement all requirements
# for 'Sized'
# Note: required function '__len__' is not implementedMissing required members
trait Container:
comptime Element: Copyable
@fieldwise_init
struct Bag(Container):
var data: Int
# Error: 'Bag' does not implement all requirements for
# 'Container'
# Note: required member 'Element' is not specifiedType mismatch on associated types
trait Taggable:
comptime Tag: Sized
@fieldwise_init
struct Widget(Taggable):
comptime Tag = Int
# Note: comptime member 'Tag' type Int does not conform
# to trait's required type SizedProvided method conflicts
When two traits in a struct's conformance list produce conflicting provided methods for the same method, the struct must implement it manually:
trait Greeter:
def greet(self):
print("Hello from Greeter")
trait Welcomer:
def greet(self):
print("Welcome from Welcomer")
@fieldwise_init
struct Host(Greeter, Welcomer):
pass
# Error: trait method requirement greet has conflicting
# default implementations in Greeter and Welcomer; you
# must implement it manuallyWas this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!