Skip to main content

Generics

Type generics let you write code once and use it across many types without duplicating logic. You don't need separate implementations or type checks for each case. Mojo generates specialized versions for each type you use.

Most languages stop there, and their generics only parameterize over types. Mojo also supports value generics. Its parameter system accepts both types and compile-time values using the same [] syntax.

Mojo distinguishes compile-time parameters from runtime arguments in its syntax. Parameters go in square brackets [] and resolve at compile time. Arguments go in parentheses () and resolve at runtime.

You see this distinction at every definition and call site, so you always know what the compiler specializes and what gets passed at runtime.

# T is a type parameter, threshold is a value parameter
# Both are compile-time. values is a runtime argument.
def count_above[
    T: Comparable & Copyable, threshold: T
](values: List[T]) -> Int:
    var count = 0
    for v in values:
        if v > threshold:
            count += 1
    return count

Many generics use traits to constrain which types work with the code. A trait defines what a type must do, and generic code declares which traits it requires.

The compiler enforces these requirements and generates specialized code for each concrete type at the call site.

The value of generics

Generics eliminate duplicated logic. You write one implementation that adapts at compile time to the types and values you use, including types from libraries you didn't write.

Type generics let one function, method, or struct work across many types. You declare requirements once using trait bounds, and the compiler enforces them. You avoid near-identical implementations and fix bugs in one place.

Value generics let you parameterize over compile-time constants such as sizes, thresholds, or feature flags. The compiler can specialize code paths, remove dead branches, and optimize for specific values without runtime cost.

Together, they let you express intent once and apply it broadly without sacrificing correctness, clarity, or performance.

Type generics

Type generics let you write code that works across many types. You define behavior once, and the compiler specializes it for each concrete type at the call site.

Setting generic parameter bounds

Bounds define what a type must do. You express them as traits that restrict which types your generic code accepts.

Always constrain generic parameters with trait bounds. Traits define the operations and guarantees your code relies on. They also expose associated types (compile-time aliases) that your implementation can use. With these constraints in place, the compiler can check correctness and report missing requirements early.

  • The most permissive bound is AnyType. It places no behavioral requirements on a type and serves as the least restrictive option.

  • ImplicitlyDestructible is a common baseline for types with lifetimes. It marks types that need cleanup when they go out of scope. Generic code that stores or owns values often requires it.

Without bounds, generic code can't do meaningful work. The compiler has no guarantees about what operations are valid.

Examples on this page use trait composition (such as Equatable & Copyable) to declare the exact capabilities an algorithm needs. Each trait defines required API elements that concrete types must provide at the call site.

Basic generics: compare two lists

Consider comparing two lists to test whether they contain the same values in the same order.

You could write a separate implementation for each element type. Or you can write one generic function that works for any list whose elements support the operations you need.

This concrete version only works with integers:

def all_equal_int(ref lhs: List[Int], ref rhs: List[Int]) -> Bool:
    if len(lhs) != len(rhs): return False

    for left, right in zip(lhs, rhs):
        if left != right:
            return False
    return True

The generic version doesn't care about the element type. It only requires the capabilities the algorithm uses: elements must support equality comparison and be copyable.

With those requirements, this updated function walks both lists element by element, just like the integer version:

def all_equal[
    T: Equatable & Copyable
](ref lhs: List[T], ref rhs: List[T]) -> Bool:
    if len(lhs) != len(rhs): return False

    for left, right in zip(lhs, rhs):
        if left != right:
            return False
    return True

Both implementations follow the same logic: check lengths, return False on the first mismatch, and return True if no differences are found.

How this works

The generic pieces in this example make a function reusable across many types. The first sign is the type parameter T. You declare type parameters in square brackets before the function arguments. Here, T represents the element type for both lists.

When you call all_equal(), the compiler infers T from the call site:

print("Int (Expect True):\t",
    all_equal([1, 2, 3], [1, 2, 3])) # True
print("Int (Expect False):\t",
    all_equal([1, 2, 3], [4, 5, 6])) # False
print("String (Expect True):\t",
    all_equal(["hello", "world"], ["hello", "world"])) # True
print("String (Expect False):\t",
    all_equal(["hello", "world"], ["goodbye", "world"])) # False

The compiler generates a concrete, type-specific version of all_equal() for each type you use. In this example, it creates one for Int and one for String.

Type requirements

Equatable is required by this algorithm. Copyable is often needed for loops and container elements. The compiler enforces both, so you don't have to guess.

Keep requirements minimal:

T: Equatable & Copyable

The ampersand (&) composes traits. It means both requirements apply.

Use the fewest bounds your code needs. That keeps your function usable with more types. For example, if you remove Equatable from all_equal(), the code fails to compile because the compiler can't guarantee that != exists for all T.

When your code uses an operation not covered by the bounds, the compiler reports an error. The type is underspecified. Required behavior isn't guaranteed by the current bounds.

Fix this by adding the trait that provides the missing behavior. Use trait composition to extend the bounds.

Adding bounds restricts which types you accept, but expands what your code can do. Each trait adds guaranteed operations, which lets the compiler check correctness and reason about lifetimes and effects.

In practice, generic errors mean your bounds don't include the behavior your code uses. Add the missing trait.

Generic parameter types

In this example, both parameters are Lists that use the same element type:

lhs: List[T], rhs: List[T]

Using T for both arguments ensures your loop compares like-with-like and prevents type mismatches.

The previous example showed lists containing elements of type T. Generic types can also be used without embedding them into container types, like you see here:

def my_generic_fn[T: AnyType](value: T):

This function accepts any type, because its only limit is AnyType. AnyType is the root of the trait hierarchy. It's the baseline that all types extend. Using AnyType means the value has no guaranteed destructor or lifetime management. Outside of reflection (type introspection), this function is effectively useless.

Generic types

So far, you've seen generics applied to methods and functions. Mojo also lets you define generic types. These use compile-time parameters to define both their fields and their methods.

Generic types let you package a reusable shape: stored fields plus supported operations. Instead of coding PairInt, PairString, and so on, you write one Pair[T] and let the compiler generate specialized versions for each concrete T you use.

comptime ComparableValue = Equatable & ImplicitlyCopyable

@fieldwise_init
struct Pair[T: ComparableValue](ComparableValue):
    var left: Self.T
    var right: Self.T

    def __eq__(self, other: Pair[Self.T]) -> Bool:
        return self.left == other.left and
               self.right == other.right

Like generic functions, generic types use placeholder parameters. The difference is that a generic type uses those parameters in its storage as well as in its methods. Here, Pair stores two values of the same element type and implements equality by comparing its fields.

Traits are what make generic types practical. Those bounds describe the requirements the type definition depends on. In this example, Pair needs to compare values, so T must be equatable. It also needs to assign and copy values in common operations, so it applies a trait composition bound to T:

comptime ComparableValue = Equatable & ImplicitlyCopyable

The Pair definition applies this bound in two places: to the type itself (in parentheses) and to the type parameter (in square brackets), which is used to define its fields.

struct Pair[T: ComparableValue](ComparableValue):
  • For the parentheses after the type, you can say the struct "conforms to" or "implements" this bound. This is a promise. The Pair type is saying, "I am a ComparableValue."
  • For the square brackets, the trait bound restricts which values callers can pass. This is a requirement: "Any value used with Pair must have type T, and T must be a ComparableValue."

This makes Pair[T] usable anywhere a ComparableValue is required (because of the parentheses) and ensures all T values conform (because of the square brackets).

So far, these examples use type parameters. Generic types can also take non-type parameters, which you'll see next.

Mixing type and value parameters

Generics don't limit what parameters you can use. Add whatever you need to define the behavior.

struct ExampleStruct:
    def example[
        T: Writable & Copyable,   # type parameter
        count: Int,               # value parameter
    ](
        self,
        data: String,             # argument
        init_value: T             # generic argument
    ) -> String:

By convention, Mojo uses lower_snake_case for compile-time value parameters. This visually distinguishes them from type parameters.

Downcasting safely: conforms_to + trait_downcast()

Sometimes a generic bound is broader than what you need. You may want operations from a specific trait that the bound doesn't guarantee. Use a guarded downcast with conforms_to() and trait_downcast().

Downcast only when the underlying type implements the target trait. Check conformance first. Then downcast and use the value through that trait.

This pattern keeps behavior explicit and safe:

def process[T: AnyType](value: T):
    comptime if conforms_to(T, Writable & ImplicitlyCopyable):
        var w = trait_downcast[Writable & ImplicitlyCopyable](value)
        print(w)
    else:
        print("<not writable>")
        # Or fail at compile time with `comptime assert`

Key points:

  • conforms_to(T, Writable & ImplicitlyCopyable) checks whether T satisfies the trait composition.
  • trait_downcast() rebinds the value to a trait-constrained view at compile time.

Only call trait_downcast() inside the guarded branch. Handle the fallback in the else branch. If you always need the trait, add it as a bound (for example, T: Writable) instead of downcasting.

Use this pattern when you can't express the requirement as a bound. It's common in reflection-style code that handles values based on their traits. See reflection.

Type generics and explicit destruction

Explicitly destroyed types don't always work with generic code. The issue isn't generics, it's lifetime management. Explicit destruction gives you control over teardown. You can define destructors that take arguments, follow different paths, or raise errors.

Generic code that copies or moves values can't see or honor that logic. Once a value is copied or transferred, you lose control over how (or whether) it's cleaned up, and the code won't compile.

Watch for these cases:

  • Generic code that manages lifetimes
  • Generic code that copies or transfers values

These show up most often in containers, collections, and iterators. They copy values or take ownership and decide when destruction happens.

Safe cases:

  • Generic operations that don't affect lifetimes

Think comparisons, predicates, and other pure operations.

Rule of thumb: if your generic code needs to own, copy, or control when a value dies, avoid explicitly destroyed types. Add an ImplicitlyDestructible bound to keep things working.

Value generics

Value generics parameterize code over compile-time constants instead of types. You declare them in [] alongside type parameters, but they bind to values.

When to use value generics

Use value generics when a value shapes structure or behavior and is known at compile time. Common cases:

  • Fixed sizes: buffer lengths, array dimensions, matrix shapes
  • Thresholds and limits: capacity caps, retry counts, precision levels
  • Feature selection: algorithm variants, debug flags, mode switches
  • Numeric configuration: SIMD widths, stride lengths, unroll factors

The compiler specializes code for each distinct value. It can remove dead branches (comptime if), unroll loops (comptime for), replace inline constants, and optimize aggressively with no runtime cost.

Basic example

This function creates a fixed-size list initialized with a default value:

comptime MyCollectionElement = ImplicitlyCopyable & ImplicitlyDestructible

def make_filled[T: MyCollectionElement, size: Int](
    splat_value: T
) -> List[T]:
    var result = List[T](capacity=size)
    for _ in range(size):
        result.append(splat_value)
    return result^

The size parameter resolves at compile time. Each call site with a different value gets its own specialized version:

var three_zeros = make_filled[Int, 3](0)
var five_hellos = make_filled[String, 5]("hello")
print(three_zeros)  # [0, 0, 0]
print(five_hellos)  # [hello, hello, hello, hello, hello]

Value parameters vs runtime arguments

Put values known at compile time in []. Put values only known at runtime in ().

Ask yourself: does the caller know this value when writing the code? If yes, use a parameter. If it depends on input, files, or runtime state, use an argument.

# size is compile-time: the compiler specializes
def fixed[size: Int]():
    var buf = InlineArray[Int, size](fill=0)

# size is runtime: no specialization
def dynamic(size: Int):
    var buf = List[Int](capacity=size)

When generics get more complex

The examples on this page cover the most common cases. As you build larger APIs, you'll run into a few additional patterns:

  • Associated (dependent) types: Some traits declare associated types. For example, collection and iteration traits use an associated element type (often Element) to describe the values they own. Associated types can also constrain behavior—for example, requiring elements to be hashable or implicitly destructible.

  • Generic methods in non-generic types: A type doesn't have to be generic to use generics. Individual methods can declare their own type parameters when only part of the API needs them.

  • Multiple type parameters: Some algorithms use more than one type parameter. Traits describe how those types relate, not just what each can do. Common examples include dictionaries (keys and values), caches (keys and values), result types (values and errors), and algorithms that operate across multiple types.

  • Compile-time computation: Value parameters enable comptime if, comptime for, and compile-time arithmetic. This shows up in SIMD code, fixed-size containers, and performance-critical loops where the compiler generates different code paths for different values.

These topics build on the same core ideas: type parameters, bounds, and specialization. You can apply bounds to functions, methods, types, or comptime aliases to keep requirements close to where they matter.

You won't need these patterns for simple cases, but they matter as generic code grows.

Conditional trait conformance

Conditional trait conformance uses checks before allowing a type to adopt a trait. If the condition is satisfied, the type conforms. It must fulfill the trait's requirements by providing required methods and associated types, and it gains any default implementation provided by the trait. The type can then be used anywhere the trait is required, such as when passing it to a function that expects a conforming type.

When the condition isn't satisfied, Mojo skips the conformance. You can't use the fully concrete type in contexts that require the trait.

Example: derived conformance

In the following declaration, Mojo conforms Wrapper to Writable when its parameter, T, is also Writable:

comptime BaseTraits = Copyable & ImplicitlyDestructible
@fieldwise_init
struct Wrapper[T: BaseTraits](
    Writable where conforms_to(T, Writable)
):
    var value: Self.T

When conforming to Writable, Wrapper doesn't need to implement any methods to enable printing. The trait provides a default implementation of the write_to() method.

Now consider a type that isn't Writable, that you might add into a Wrapper:

@fieldwise_init
struct NotWritable(BaseTraits):
    var data: Int

When instantiated with an Int or String (both Writable), the Wrapper gains Writable conformance. With NotWritable, you can build a struct, but you can't print it:

var w_int = Wrapper[Int](42)  # Int is Writable
print(w_int)  # Wrapper[Int](value=42)


var w_str = Wrapper[String]("Hello")  # String is Writable
print(w_str)  # Wrapper[String](value=Hello)


# OK: only `Writable` conformance is unavailable
var w_not_writable = Wrapper[NotWritable](NotWritable(10))
# print(w_not_writable)  # Compile-time error:
# invalid call to 'print': could not convert element of 'values' with
# type 'Wrapper[NotWritable]' to expected type 'Writable'

Since NotWritable doesn't conform to Writable, the conditional conformance check fails and Wrapper[NotWritable] doesn't adopt the Writable conformance. In contrast, Int and String are Writable and their wrappers gain the conformance. This allows their instances to print by using the write_to() method provided by Writable.

This pattern is standard for single-type containers like Optional[T], Box[T], Lazy[T], and List[T]. It says: "This type can do X if its inner type can do X."

Example: parts conformance

Conditional conformance has another standard pattern for types with multiple distinct components: Result[T, E], Pair[L, R], Dict[K, V], and similar. It says: "This type can do X if each of its parts can do X":

comptime BaseTraits = Copyable & ImplicitlyDestructible

@fieldwise_init
struct Pair[L: BaseTraits, R: BaseTraits](
    Hashable where conforms_to(L, Hashable) and conforms_to(R, Hashable)
):
    var left: Self.L
    var right: Self.R


@fieldwise_init
struct NotHashable(BaseTraits):
    var data: Int

In this example, Pair uses two distinct type parameters. When both are Hashable, the concrete Pair type becomes Hashable, gaining the hash() method from the trait:

var pair = Pair[Int, String](left=1, right="one")
var hash = hash(pair)
print(hash)  # Prints the hash of the pair

# OK: only hashing is unavailable
var pair2 = Pair[Int, NotHashable](left=1, right=NotHashable(10)) # Constructible
# var hash2 = hash(pair2) # Compile time error

Example: conditional method access

When a type adopts a trait, it must satisfy the trait's required methods. The same conditions that determine whether a type can conditionally conform to a trait are usually the same conditions used to gate required method implementations.

You express these method tests with where clauses. If true, the method is available. If not, the method can't be used for that specialized type. The following example walks you through this process and how it works.

You can use Boolable instances in if statements. You make your Wrapper testable by conforming to the Boolable trait. This trait requires a new method, __bool__():

@fieldwise_init
struct Wrapper[T: BaseTraits](
    Writable where conforms_to(T, Writable),
    Boolable where conforms_to(T, Boolable), # New conditional conformance
    ):
    var value: Self.T

    # New method
    def __bool__(self) -> Bool where conforms_to(Self.T, Boolable):
        return trait_downcast[Boolable](self.value).__bool__()

In this example, the condition on __bool__() is the same as the one used for the Boolable conditional conformance. The where clause gates the method. If the condition is true, the method is available. If not, it isn't.

Since the method and the conformance use the same condition, they stay aligned. You won't end up in a situation where the wrapped type conforms but the method isn't available, or where the method exists without the corresponding conformance.

You see this in the following examples:

var w_str = Wrapper[String]("Hello")
if w_str:  # Chooses the non-empty branch
    print(t"Non-empty string \"{w_str.value}\" is truthy")
else:
    print(t"Empty string \"{w_str.value}\" is falsy")

var w_empty_str = Wrapper[String]("")
if w_empty_str:  # Chooses the empty branch
    print(t"Non-empty string \"{w_empty_str.value}\" is truthy")
else:
    print(t"Empty string \"{w_empty_str.value}\" is falsy")

The NotWritable type from earlier in this section has no traits beyond the base Copyable and ImplicitlyDestructible. Therefore it isn't Boolable and the condition on the __bool__() method will fail:

@fieldwise_init
struct NotWritable(BaseTraits):
    var data: Int

var w_not_writable = Wrapper[NotWritable](NotWritable(10))

# The instance can't be used as a boolean value due to the
# conditional method access for `__bool__()`.

# Compiler error: the method condition is false
if w_not_writable:
    print(t"NotWritable with data {w_not_writable.value.data} is truthy")
else:
    print(t"NotWritable with data {w_not_writable.value.data} is falsy")

This condition on your method isn't conditional trait conformance. It's conditional method access. In this example, the method and the conformance use the same condition, so they stay aligned.

That said, where clauses on methods and functions are useful even when you're not working with trait conformance. For example, you might gate a method so it only works with non-empty lists, real numbers, or with values that fall within the index bounds of a collection.

Conditional trait composition

Mojo supports flexible condition composition, as shown in these examples:

  • Unconditional: No special clauses.

    struct Foo(Copyable, ImplicitlyDestructible):
  • Simple condition: as shown in Wrapper.

    struct Wrapper[T: BaseTraits](
        Writable where conforms_to(T, Writable)
    ):
  • Hybrid: Mixes unconditional and conditional traits.

    struct Foo[T: AnyType](
        Copyable, Writable where conforms_to(T, Writable)
    )
  • Multiple aligned conditions: as shown in Pair.

    struct Pair[L: BaseTraits, R: BaseTraits](
        Hashable where conforms_to(L, Hashable) and conforms_to(R, Hashable)
    ):
  • Multiple independent conditions:

    struct Foo[T: AnyType](
      Writable where conforms_to(T, Writable),
      Hashable where conforms_to(T, Hashable),
    )

Conditional conformance with value parameters

Conditional conformance also works with value parameters. You can gate both conformance and methods on compile-time value conditions.

This is useful when a type's capabilities depend on a numeric parameter. For example, a fixed-capacity wrapper might only be Writable when it has capacity and its elements are writable. The Sized conformance is unconditional.

struct SizedListWrapper[capacity: Int, T: Copyable](
    Sized, Writable where conforms_to(T, Writable) and capacity > 0
):
    var data: List[Self.T]

    def __init__(out self, value: Self.T):
        self.data = List[Self.T](capacity=Self.capacity)
        for _ in range(Self.capacity):
            self.data.append(value.copy())

    def __len__(self) -> Int:
        return len(self.data)

    def write_to(self, mut writer: Some[Writer]):
        writer.write(repr(self.data))

You can gate methods on value conditions:

def first(self) -> Self.T where Self.capacity > 0:
    return self.data[0].copy()

When capacity is one or more, the type conforms to Writable and the first() method is available. When it is zero or less, neither is usable:

var s = SizedListWrapper[5, Int](42)
print(s)          # List[Int]([Int(42), Int(42), Int(42), Int(42), Int(42)])
print(s.first())  # 42

# var s = SizedListWrapper[0, Int](42)
# print(s)          # Error: Writable not satisfied
# print(s.first())  # Error: constraint is false

Value conditions follow the same rules as type conditions. You can combine them with and and mix them with conforms_to checks in the same where clause.

Was this page helpful?