Skip to main content

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):
    body

Traits 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:

    pass

Mojo 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:

ElementSyntaxRole
Required method... bodyConforming types must implement
Provided methodCode bodyInherited unless overridden
Comptime member - associated typecomptime Name: TraitConforming types provide a concrete type
Comptime member - required valuecomptime name: TypeConforming types provide a value
Comptime member - constantcomptime name = valueShared 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

ElementNamingNotes
Trait namePascalCaseCapabilities gained by conformance: Equatable, Copyable, PathLike.
Instance methodlower_snake_case()Method's action: write_to(), update()
Static methodlower_snake_case()Method's action: get_type_name, get_element_bitwidth
comptime membersTrait compositions are PascalCase. Values are lower_snake_caseDescribes use: KeyElement, element_bitwidth
Associated typePascalCaseDescribes 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 method

Provided 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 method

Both 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 method

Provided 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.
  • pass is a no-op that counts as a provided implementation body. It's only valid when the method returns None.

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 ImplicitlyDestructible

An 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: constant

Outside 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 initializer

Comptime 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)                 # 42

Comptime 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: hello

A 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 where clause 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
    ...                # parameters

Traits can't declare or use fields:

trait Unsupported:
    var x: Int  # Error: traits do not support 'var' fields

Traits don't support where clauses on methods:

trait Unsupported:
    def maybe(self) -> Int where Self: Sized:
        ...
    # Error: 'where' clauses on trait methods are not supported

Conformance 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 implemented

Missing 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 specified

Type 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 Sized

Provided 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 manually

Was this page helpful?