Skip to main content

Traits

A trait is a set of requirements that a type must implement. You can think of it as a contract: a type that conforms to a trait guarantees that it implements all of the features of the trait.

Traits are similar to Java interfaces, C++ concepts, Swift protocols, and Rust traits. If you're familiar with any of those features, Mojo traits solve the same basic problem.

You've probably already seen some traits, like Copyable and Movable, used in example code. This section describes how traits work, how to use existing traits, and how to define your own traits.

Background

In dynamically-typed languages like Python, you don't need to explicitly declare that two classes are similar. This is easiest to show by example:

🐍 Python
class Duck:
    def quack(self):
        print("Quack.")

class StealthCow:
    def quack(self):
        print("Moo!")

def make_it_quack(maybe_a_duck):
    try:
        maybe_a_duck.quack()
    except:
        print("Not a duck.")

make_it_quack(Duck())
make_it_quack(StealthCow())

The Duck and StealthCow classes aren't related in any way, but they both define a quack() method, so they work the same in the make_it_quack() function. This works because Python uses dynamic dispatch—it identifies the methods to call at runtime. So make_it_quack() doesn't care what types you're passing it, only the fact that they implement the quack() method.

In a statically-typed environment, this approach doesn't work: Mojo functions require you to specify the type of each argument. If you wanted to write this example in Mojo without traits, you'd need to write a function overload for each input type.

🔥 Mojo
@fieldwise_init
struct Duck(Copyable):
    def quack(self):
        print("Quack")

@fieldwise_init
struct StealthCow(Copyable):
    def quack(self):
        print("Moo!")

def make_it_quack(definitely_a_duck: Duck):
    definitely_a_duck.quack()

def make_it_quack(not_a_duck: StealthCow):
    not_a_duck.quack()

make_it_quack(Duck())
make_it_quack(StealthCow())
Quack
Moo!

This isn't too bad with only two types. But the more types you want to support, the less practical this approach is.

You might notice that the Mojo versions of make_it_quack() don't include the try/except statement. We don't need it because Mojo's static type checking ensures that you can only pass instances of Duck or StealthCow into the make_it_quack() function.

Using traits

Traits solve this problem by letting you define a shared set of behaviors that types can implement. Then you can write a function that depends on the trait, rather than individual types. As an example, let's update the make_it_quack() example using traits. This will involve three steps:

  1. Defining a new Quackable trait.
  2. Adding the trait to the Duck and StealthCow structs.
  3. Updating the make_it_quack() function to depend on the trait.

Defining a trait

The first step is defining a trait that requires a quack() method:

trait Quackable:
    def quack(self):
        ...

A trait looks a lot like a struct, except it's introduced by the trait keyword. Note that the quack() method signature is followed by three dots (...). This indicates it isn't implemented within the trait. In this example, quack is a required method and must be implemented by any conforming struct.

A trait can supply a default implementation, so conforming structs don't need to implement the method themselves. You can provide a full implementation or use the pass keyword. Using pass creates a no-op method that does nothing. In your conforming struct, you can choose to override this default implementation:

trait Quackable:
    def quack(self):
        pass

For more information, see Default method implementations.

A trait can also include comptime members—compile-time constant values that must be defined by conforming structs. comptime members are useful for writing traits that describe generic types. For more information, see comptime members for generics.

Adding traits to structs

Next, we need some structs that conform to the Quackable trait. Since the Duck and StealthCow structs above already implement the quack() method, all we need to do is add the Quackable trait to the traits it conforms to (in parenthesis, after the struct name).

(If you're familiar with Python, this looks just like Python's inheritance syntax.)

@fieldwise_init
struct Duck(Copyable, Quackable):
    def quack(self):
        print("Quack")

@fieldwise_init
struct StealthCow(Copyable, Quackable):
    def quack(self):
        print("Moo!")

The struct needs to implement any methods that are declared in the trait. The compiler enforces conformance: if a struct says it conforms to a trait, it must implement everything required by the trait, or the code won't compile.

Using a trait as a type bound

Finally, you can define a function that takes a Quackable like this:

def make_it_quack[DuckType: Quackable](maybe_a_duck: DuckType):
    maybe_a_duck.quack()

Or using the shorthand form:

def make_it_quack2(maybe_a_duck: Some[Quackable]):
    maybe_a_duck.quack()

This syntax may look a little unfamiliar if you haven't dealt with Mojo parameters before. What the first signature means is that maybe_a_duck is an argument of type DuckType, where DuckType is a type that must conform to the Quackable trait. Quackable is called the type bound for DuckType.

The Some[Quackable] form expresses the same idea: the type of maybe_a_duck is some concrete type that conforms to the trait Quackable.

Both forms work the same, except that the first form explicitly names the type value. This can be useful, for example, if you want to take two values of the same type:

def take_two_quackers[DuckType: Quackable](quacker1: DuckType, quacker2: DuckType):
    pass

Putting it all together

Using the function is simple enough:

make_it_quack(Duck())
make_it_quack(StealthCow())
Quack
Moo!

Note that you don't need the square brackets when you call make_it_quack(): the compiler infers the type of the argument, and ensures the type has the required trait.

One limitation of traits is that you can't add traits to existing types. For example, if you define a new Numeric trait, you can't add it to the standard library Float64 and Int types. However, the standard library already includes quite a few traits, and we'll be adding more over time.

Traits can require static methods

In addition to regular instance methods, traits can specify required static methods.

trait HasStaticMethod:
    @staticmethod
    def do_stuff(): ...

def fun_with_traits[type: HasStaticMethod]():
    type.do_stuff()

Default method implementations

Often, some or all of the structs that conform to a given trait can use the same implementation for a given required method. In this case, the trait can include a default implementation. A conforming struct can still define its own version of the method, overriding the default implementation. But if the struct doesn't define its own version, it automatically inherits the default implementation.

Defining a default implementation for a trait looks the same as writing a method for a struct:

trait DefaultQuackable:
    def quack(self):
        print("Quack")


@fieldwise_init
struct DefaultDuck(Copyable, DefaultQuackable):
    pass

When looking at the API doc for a standard library trait, you'll see methods that you must implement listed as required methods, and methods that have default implementations listed as provided methods.

The Equatable trait is a good example of the use case for default implementations. The trait includes two methods: __eq__() (corresponding to the == operator) and __ne__() (corresponding to the != operator). Every type that conforms to Equatable needs to define the __eq__() method for itself, but the trait supplies a default implementation for __ne__(). Given an __eq__() method, the definition of __ne__() is trivial for most types:

def __ne__(self, other: Self) -> Bool:
    return not self.__eq__(other)

Trait compositions

You can compose traits using the & sigil. This lets you define new traits that are simple combinations of other traits. You can use a trait composition anywhere that you'd use a single trait:

trait Flyable:
    def fly(self): ...

def quack_and_go[type: Quackable & Flyable](quacker: type):
    quacker.quack()
    quacker.fly()

@fieldwise_init
struct FlyingDuck(Copyable, Quackable, Flyable):
    def quack(self):
        print("Quack")

    def fly(self):
        print("Whoosh!")

You can also use the comptime keyword to create a shorthand name for a trait composition:

comptime DuckLike = Quackable & Flyable

struct ToyDuck(DuckLike):
    # ... implementation omitted

You can also compose traits using refinement, by defining a new, empty trait like this:

trait DuckTrait(Quackable, Flyable):
    pass

However, this is less flexible than using a trait composition and not recommended. The difference is that using the trait keyword defines a new, named trait. For a struct to conform to this trait, you need to explicitly include it in the struct's signature. On the other hand, the DuckLike comptime value represents a composition of two separate traits, Quackable and Flyable, and anything that conforms to those two traits conforms to DuckLike. For example, consider the FlyingDuck type shown above:

struct FlyingDuck(Copyable, Quackable, Flyable):
    # ... etc

Because FlyingDuck conforms to both Quackable and Flyable, it also conforms to the DuckLike trait composition. But it doesn't conform to DuckTrait, since it doesn't include DuckTrait in its list of traits.

Trait refinement

Traits refine other traits. A trait that refines another trait includes all of the requirements declared by the original trait. For example:

trait Animal:
    def make_sound(self):
        ...

# Bird refines Animal
trait Bird(Animal):
    def fly(self):
        ...

Since Bird refines Animal, a struct that conforms to the Bird trait must implement both make_sound() and fly(). And since every Bird conforms to Animal, a struct that conforms to Bird can be passed to any function that requires an Animal.

You can define a trait as a refinement of multiple traits by listing them in parentheses. This can be either a comma-separated list of traits or a trait composition. For example, you might define a NamedAnimal trait that combines the requirements of the Animal trait and a new Named trait:

trait Named:
    def get_name(self) -> String:
        ...

trait NamedAnimal(Animal, Named):
    def emit_name_and_sound(self):
        ...

Refinement is useful when you're creating a new trait that adds additional requirements. If you simply want to express the union of two or more traits, use a simple trait composition instead:

comptime NamedAnimal = Animal & Named

Traits and lifecycle methods

Traits can specify required lifecycle methods, including constructors, copy constructors and move constructors.

For example, the following code creates a MassProducible trait. A MassProducible type has a default (no-argument) constructor and can be moved. It uses two built-in traits: Defaultable, which requires a default (no-argument) constructor, and Movable, which requires the type to have a move constructor.

The factory[]() function returns a newly-constructed instance of a MassProducible type. The following example shows the definitions of the Defaultable and Movable traits in comments for reference:

# trait Defaultable
#     def __init__(out self): ...

# trait Movable
#     def __init__(out self, *, deinit take: Self): ...

comptime MassProducible = Defaultable & Movable

def factory[type: MassProducible]() -> type:
    return type()

struct Thing(MassProducible):
    var id: Int

    def __init__(out self):
        self.id = 0

    def __init__(out self, *, deinit take: Self):
        self.id = take.id

var thing = factory[Thing]()

Register passable types and the RegisterPassable trait

"Register passable" tells Mojo that a type should be passed in machine registers such as a CPU register. This means the type is always passed by value. For data types like an integer or floating-point number, this is much more efficient than storing values in stack memory.

Mojo supports two forms of register passable types.

The standard register passable type has the following capabilities and restrictions:

  • They have normal lifecycles and can implement or override lifecycle methods.
  • Every field must either be register passable or trivially register passable.
  • The type must be Movable.
  • Their self pointer is neither stable nor predictable. They can move in memory at any time so they aren't suitable for types that rely on pointer identity.
  • When used as arguments or results, they can be exposed directly to C and C++ through foreign function interfaces (FFI) and don't need to be passed by pointer.
  • They can't override the default move constructor. This guarantees moves are always side-effect-free.

You can create custom register-passable types by conforming a type to the RegisterPassable trait

Trivial register types are typically data types. They include:

  • Arithmetic types. This includes types such as Int, Int32, Bool, Float64 etc.
  • Pointers. The address value is trivial, not the data being pointed to.
  • Arrays of other trivial types. SIMD is a good example.

These types are provided by Mojo's standard library.

A trivially register passable type has the following capabilities and restrictions:

  • They do not use lifecycle methods.
  • Every field must be trivially register passable.
  • The type must be Copyable.
  • They use a special trivial (no-op) destructor. They are trivially movable (via Copyable, which refines Movable), trivially copyable, and trivially destructible.

Built-in traits

The Mojo standard library includes many traits. They're implemented by a number of standard library types, and you can also implement these on your own types. These standard library traits include:

The API reference docs linked above include usage examples for each trait. The following sections discuss a few of these traits.

The Sized trait

The Sized trait identifies types that have a measurable length, like strings and arrays.

Specifically, Sized requires a type to implement the __len__() method. This trait is used by the built-in len() function. For example, if you're writing a custom list type, you could implement this trait so your type works with len():

struct MyList(Copyable, Sized):
    var size: Int
    # ...

    def __init__(out self):
        self.size = 0

    def __len__(self) -> Int:
        return self.size

print(len(MyList()))
0

The Intable and IntableRaising traits

The Intable trait identifies a type that can be converted to Int. The IntableRaising trait describes a type can be converted to an Int, but the conversion might raise an error.

Both of these traits require the type to implement the __int__() method. For example:

@fieldwise_init
struct IntLike(Intable):
    var i: Int

    def __int__(self) -> Int:
        return self.i

value = IntLike(42)
print(Int(value) == 42)
True

The Writable trait

The Writable trait describes a type that can produce a human-readable text representation by writing to a Writer object. The print() function requires that its arguments conform to the Writable trait. This enables efficient stream-based writing by default, avoiding unnecessary intermediate String heap allocations.

The Writable trait requires a type to implement a write_to() method. This method receives a mut writer: Some[Writer] argument. A generic Writer that represents anything that can be written to, such as a String buffer, a file, or a network stream. You invoke the Writer instance's write() method to write a sequence of Writable arguments constituting the string representation of your type.

An important feature of the Writer trait is that it only accepts valid UTF-8 text. This means that when you write data to a Writer, you can only use StringSlice values (via the write_string() method) or other Writable types; you can't write arbitrary bytes. This guarantee ensures that types like String can safely implement the Writer trait without risking corruption of their internal UTF-8 data.

Writable also provides a write_repr_to() method for producing the "official" string representation of a type—the kind you would see when calling repr() or using the {!r} format specifier. If at all possible, this should look like a valid Mojo expression that could be used to recreate a struct instance with the same value. write_repr_to() has a default implementation that uses reflection, so you only need to override it if your type needs a distinct debug representation.

Here is a simple example of a type that implements Writable with a custom write_repr_to():

@fieldwise_init
struct Dog(Copyable, Writable):
    var name: String
    var age: Int

    # Allows the type to be written into any `Writer`
    def write_to(self, mut writer: Some[Writer]):
        t"Dog({self.name}, {self.age})".write_to(writer)

    # Official representation when calling `repr`
    def write_repr_to(self, mut writer: Some[Writer]):
        t"Dog(name={self.name}, age={self.age})".write_to(writer)

dog = Dog("Rex", 5)
print(repr(dog))
print(dog)
Dog(name=Rex, age=5)
Dog(Rex, 5)

Lifetime management traits

Mojo provides two core traits for managing value lifetimes:

  • AnyType: The base trait that all types extend. Types conforming only to AnyType may require explicit destruction.

  • ImplicitlyDestructible: Types that can be automatically destroyed by calling __del__() when their lifetime ends.

For detailed information about value destruction, explicit destruction with the @explicit_destroy decorator, and when to use each approach, see Death of a value.

Generic structs with traits

You can also use traits when defining a generic container. A generic container is a container (for example, an array or hashmap) that can hold different data types. In a dynamic language like Python it's easy to add different types of items to a container. But in a statically-typed environment, the compiler needs to be able to identify the types at compile time. For example, if the container needs to copy a value, the compiler needs to verify that the type can be copied.

The List type is an example of a generic container. A single List can only hold a single type of data. The list elements must conform to the Copyable trait:

struct List[T: Copyable]:

For example, you can create a list of integer values like this:

var list: List[Int]
list = [1, 2, 3, 4]
for i in range(len(list)):
    print(list[i], end=" ")
1 2 3 4

You can use traits to define requirements for elements that are stored in a container. For example, List requires elements that can be moved and copied. To store a struct in a List, the struct needs to conform to the Copyable trait, which requires a copy constructor and a move constructor.

Building generic containers is an advanced topic. For an introduction, see the section on parameterized structs.

comptime members for generics

In addition to methods, a trait can include comptime members, which must be defined by any conforming struct. For example:

trait Repeater:
    comptime count: Int

An implementing struct must define a concrete constant value for the comptime member, using any compile-time parameter value. For example, it can use a literal constant or a compile-time expression, including one that uses the struct's parameters.

struct Doublespeak(Repeater):
    comptime count: Int = 2

struct Multispeak[verbosity: Int](Repeater):
    comptime count: Int = Self.verbosity * 2 + 1

The Doublespeak struct has a constant value for count, but the Multispeak struct lets the user set the value using a parameter:

repeater = Multispeak[12]()

Note that the field is named count, and the Multispeak parameter is named verbosity. Parameters and comptime members are in the same namespace, so the parameter can't have the same name as the comptime member.

comptime members are most useful for writing traits for generic types. For example, imagine that you want to write a trait that describes a generic stack data structure that stores elements that conform to the Copyable trait.

By adding the element type as a comptime member on the trait, you can specify generic methods on the trait:

trait Stacklike:
    comptime EltType: Copyable

    def push(mut self, var item: Self.EltType):
        ...

    def pop(mut self) -> Self.EltType:
        ...

The following struct implements the Stacklike trait using a List as the underlying storage:

struct MyStack[type: Copyable & ImplicitlyDestructible](Stacklike):
    """A simple Stack built using a List."""
    comptime EltType = Self.type
    comptime list_type = List[Self.EltType]

    var list: Self.list_type

    def __init__(out self):
        self.list = Self.list_type()

    def push(mut self, var item: Self.EltType):
        self.list.append(item^)

    def pop(mut self) -> Self.EltType:
        return self.list.pop()

    def dump[
        WritableEltType: Writable & Copyable
    ](self: MyStack[WritableEltType]):
        print("[", end="")
        for item in self.list:
            print(item, end=", ")
        print("]")

The MyStack type adds a dump() method that prints the contents of the stack. Because a struct that conforms to Copyable is not necessarily printable, MyStack uses conditional conformance to define a dump() method that works as long as the element type is writable.

The following code exercises this new trait by defining a generic method, add_to_stack() that adds an item to any Stacklike type.

def add_to_stack[S: Stacklike](mut stack: S, var item: S.EltType) raises:
    stack.push(item^)

def main() raises:
    s = MyStack[Int]()
    add_to_stack(s, 12)
    add_to_stack(s, 33)
    s.dump()             # [12, 33, ]
    print(s.pop())       # 33

Was this page helpful?