For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
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 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 & ImplicitlyCopyable & ImplicitlyDeletable, 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.
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.
Type constraints
Constraints define what a type must do. You express them as traits or trait compositions. You must always constrain generic parameters — without a fixed set of required features, the compiler has no guarantees about what operations are valid.
-
The most permissive constraint is
AnyType. It places no behavioral requirements on a type. -
ImplicitlyDeletableis a common baseline for types with lifetimes. Generic code that stores or owns values often requires it.
Constraints make your code sound: every operation you use is guaranteed to exist for any type that satisfies them.
Naming conventions
Mojo follows naming conventions used in languages like Rust and C++.
Type parameter names use PascalCase — short (T, E) or descriptive
(ErrorType, Element). By convention, T, U, V are general
types; K/V for key-value pairs; E for errors; H for hashers.
Value parameter names use lower_snake_case and should be descriptive
(capacity, hasher, tile_x).
Basic example: 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:
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.
The type parameter T is declared in square brackets before the function
arguments. It 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 — one for Int and one for
String in this example.
Choosing constraints
Keep requirements minimal:
T: Equatable & Copyable
The ampersand (&) composes traits. Use the fewest constraints your code
needs — that keeps your function usable with more types. If you remove
Equatable from all_equal(), the code won't compile because the
compiler can't guarantee that != exists for all T.
When your code uses an operation not covered by its constraints, the compiler reports an error — the type is underspecified. Fix this by adding the trait that provides the missing behavior. In practice, generic errors mean your constraints don't include the behavior your code uses.
Adding constraints 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.
Generic parameter types
In the all_equal() example, both parameters use the same element type:
lhs: List[T], rhs: List[T]
Using T for both ensures your loop compares like with like and prevents
type mismatches. You can also use a generic type directly without
embedding it in a container:
def my_generic_fn[T: AnyType](value: T):
This function accepts any type because its only limit is AnyType — the
root of the trait hierarchy that all types conform to. Using AnyType
means the value has no guaranteed destructor or lifetime management.
Outside of reflection, this
function can't do anything meaningful with value.
Printing under-specified generic values
A common issue with AnyType is that the compiler can't print values of
unspecified types. Because it can't determine whether T conforms to
Writable, it can't generate the code needed to print it:
def function[Ts: AnyType](*args: Ts):
for arg in args:
print(arg) # Will error.
# The compiler can't verify `Writable` conformance
def main():
function(1, 2, 3)
Work around this by testing the generic type parameter for Writable
conformance and downcasting to expose the Writable trait:
def represent[T: AnyType](v: T) -> String:
comptime if conforms_to(T, Writable):
return String(trait_downcast[Writable](v))
else:
return String(t"{reflect[T].name()}")
def function[Ts: AnyType](*args: Ts):
for arg in args:
print(represent(arg))
@fieldwise_init
struct SomeStruct:
var x: Int
def main():
function(1, 2, 3) # prints each integer
function(SomeStruct(2), SomeStruct(3)) # each "(module-name).SomeStruct"
Writable items print as if explicitly converted to String.
Non-Writable items use their type name, prefixed by the module name
(the file name without the extension).
Read more about safe downcasting on this page.
Generic types
Generics aren't limited to functions. You can define generic types that 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 writing 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 & ImplicitlyDeletable
@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 — but 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.
Pair needs to compare values, so T must be equatable. It also needs
to copy and clean up values in common operations, so it applies a trait
composition on T:
comptime ComparableValue = Equatable & ImplicitlyCopyable & ImplicitlyDeletable
The Pair definition applies this conformance in two places:
struct Pair[T: ComparableValue](ComparableValue):
- Square brackets — a requirement on callers: any value used with
Pairmust have typeT, andTmust be aComparableValue. - Parentheses — a promise from
Pairitself: "I am aComparableValue."
This makes Pair[T] usable anywhere a ComparableValue is required,
and ensures all T values conform.
Mixing type and value parameters
Generic types can take non-type parameters too. 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.
Simplified conformance syntax with Some
You can replace explicit type parameters and conformances with concise
Some[Trait(s)] and SomeTypeList[Trait(s)] syntax. These forms
support both a single trait and trait compositions, and let you express
conformance where you use the type instead of declaring a parameter in
one place and using it in another.
For example:
def my_generic_fn[T: Trait(s)](value: T):
becomes:
def my_generic_fn(value: Some[Trait(s)]):
You can use Some with any trait or trait composition, under any
argument convention, wherever you've been using type parameters. If the
compiler can't infer a concrete type, it errors and asks you to use
explicit type parameters instead.
Arguments
Some places the trait requirement directly on the argument, eliminating the
explicit type parameter:
# Before
def foo[T: Intable, //](x: T) -> Int:
return x.__int__()
# After
def foo(x: Some[Intable]) -> Int:
return x.__int__()
Function types
Some works with function types too, moving closure parameter conformance
onto the argument:
# Before
def sync_parallelize[
FuncType: def(Int) -> None,
](func: FuncType):
...
# After
def sync_parallelize(func: Some[def(Int) -> None]):
...
Variadics
Use *SomeTypeList to conform a variadic parameter pack instead of a
single type parameter:
# Before
def show[*Ts: Writable](*pack: *Ts):
...
# After
def show(*pack: *SomeTypeList[Writable]):
...
Operator overloads
Operator overloads are a natural fit for Some — the trait that enables
the operator lives directly on the declaration:
# Before
def __getitem__[I: Indexer, //](self, idx: I) -> ref[self.x] Self.T:
...
# After
def __getitem__(self, idx: Some[Indexer]) -> ref[self.x] Self.T:
...
Where Some won't work
Some can't replace type parameters in all cases. The compiler can't
infer a concrete type for struct fields, and will report an error
indicating the type isn't concrete and asking you to use [] to bind
missing parameters:
@fieldwise_init
struct Struct(Writable):
# Error: a `Some` struct field has no concrete type to infer
var x: Some[Copyable & ImplicitlyDeletable & Writable]
def main():
var s = Struct(1)
print(s)
Move the conformances back to a generic type parameter to fix this:
@fieldwise_init
struct Struct[T: Copyable & ImplicitlyDeletable & Writable](Writable):
var x: Self.T
def main():
var s = Struct(1)
print(s) # Struct[Int](x=1)
Downcasting safely: conforms_to + trait_downcast()
Sometimes a generic conformance is broader than what you need. Use a
guarded downcast with conforms_to() and trait_downcast() to access
operations from a specific trait the conformance doesn't guarantee.
Check conformance first, then downcast and use the value through that trait:
def process[T: AnyType](value: T):
comptime if conforms_to(
T, Writable & ImplicitlyCopyable & ImplicitlyDeletable
):
var w = trait_downcast[
Writable & ImplicitlyCopyable & ImplicitlyDeletable
](value)
print(w)
else:
print("<not writable>")
# Or fail at compile time with `comptime assert`
conforms_to(T, Trait)checks whetherTsatisfies 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
constraint (for example, T: Writable) instead of downcasting.
Use this pattern when you can't express the requirement as a constraint. It's common in reflection-style code that handles values based on their traits. See reflection.
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 & ImplicitlyDeletable
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)
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 that copy values or take ownership and decide when destruction happens.
Safe cases are generic operations that don't affect lifetimes, such as comparisons and predicates.
If your generic code needs to own, copy, or control when a value dies,
avoid explicitly destroyed types. Add an ImplicitlyDeletable constraint
to keep things working.
Conditional trait conformance
Conditional trait conformance uses checks before allowing a type to adopt a trait. When the condition is satisfied, the type conforms — it must fulfill the trait's requirements and gains any default implementation the trait provides. When the condition isn't satisfied, Mojo skips the conformance.
Example: derived conformance
In the following declaration, Mojo conforms Wrapper to Writable when
its parameter T is also Writable:
comptime BaseTraits = Copyable & ImplicitlyDeletable & Writable
@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 — the trait provides a default implementation of write_to().
Now consider a type that isn't Writable:
@fieldwise_init
struct NotWritable(BaseTraits):
var data: Int
When instantiated with Int or String (both Writable), Wrapper
gains Writable conformance. With NotWritable, you can build the
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:
# Wrapper[NotWritable] doesn't conform to 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
For types with multiple distinct components, the pattern extends
naturally. Result[T, E], Pair[L, R], Dict[K, V], and similar
types say: "This type can do X if each of its parts can do X":
comptime BaseTraits = Copyable & ImplicitlyDeletable
@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
When both L and R 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))
# 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.
You gate those method implementations with where clauses — the same
conditions that control whether the conformance applies.
You can make Wrapper testable in if statements by conforming to
Boolable, which requires __bool__():
@fieldwise_init
struct Wrapper[T: BaseTraits](
Writable where conforms_to(T, Writable),
Boolable where conforms_to(T, Boolable),
):
var value: Self.T
def __bool__(self) -> Bool where conforms_to(Self.T, Boolable):
return trait_downcast[Boolable](self.value).__bool__()
The condition on __bool__() matches the one used for the Boolable
conditional conformance. Since the method and the conformance use the
same condition, they stay aligned — you won't end up with a conformance
but no method, or a method without the corresponding conformance.
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")
Because NotWritable isn't Boolable, the condition on __bool__()
fails and the method isn't available:
@fieldwise_init
struct NotWritable(BaseTraits):
var data: Int
var w_not_writable = Wrapper[NotWritable](NotWritable(10))
# Compile-time 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")
where clauses on methods are useful beyond trait conformance too. You
can gate a method so it only works with non-empty lists, real numbers, or
values that fall within a collection's index bounds.
Conditional trait composition
Mojo supports flexible condition composition:
-
Unconditional — no special clauses:
struct Foo(Copyable, ImplicitlyDeletable): -
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:
comptime ElementTraits = Writable & Copyable & ImplicitlyDeletable
struct SizedListWrapper[capacity: Int, T: ElementTraits](
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
first() is available. When it's zero or less, neither is usable:
var s = SizedListWrapper[5, Int](42)
print(s) # List of 42s
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.