# How to Use DynArray Storage in GenLayer Contracts: A Complete Guide

> Master DynArray storage in GenLayer contracts for O(1) appends and persistent indexed access. Our guide explains this essential GenVM data management tool.

- Repository: [GenLayer Labs/genlayer-project-boilerplate](https://github.com/genlayerlabs/genlayer-project-boilerplate)
- Tags: how-to-guide
- Published: 2026-08-20

---

**DynArray is the deterministic, dynamic-array storage type used in GenLayer contracts to provide O(1) appends and persistent index-based access for data verified by the GenVM.**

GenLayer contracts rely on specialized collection types for deterministic on-chain storage that survives across transactions. The `DynArray` type, available in the `genlayerlabs/genlayer-project-boilerplate` repository, acts like a Python `list` but is fully compatible with the GenVM linter and runtime. Learning how to use **DynArray storage in GenLayer contracts** ensures your state remains valid, persistent, and accepted by the GenVM.

## Why Choose DynArray for GenLayer Contract Storage

The GenVM tracks every element stored in a `DynArray` and automatically persists the array’s length in the contract’s state. This guarantees that data remains available across transactions without manual serialization.

Key characteristics of `DynArray` storage include:

- **O(1) append and O(1) access by index**, making it efficient for sequential data.
- **Automatic length tracking** by the VM, which updates the stored metadata on every mutation.
- **Deterministic behavior** required for consensus, unlike native Python collections.

As documented in the repository, native Python types such as `list` and `dict` are rejected by the linter. The [`README.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/README.md) explicitly notes that invalid storage types must be replaced with approved types such as `TreeMap`, `DynArray`, or `u256` (see `README.md#L64-L66`). Similarly, [`CLAUDE.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/CLAUDE.md) lists the supported storage types and explains the linter’s restrictions (`CLAUDE.md#L72-L73`).

## Declaring a DynArray with Storage-Compatible Types

To store custom objects inside a `DynArray`, you must decorate the class with `@allow_storage` and `@dataclass`. The generic syntax `DynArray[<type>]` declares the element type for the compiler.

```python
from genlayer import *

@allow_storage
@dataclass
class Item:
    id: u256
    name: str

class MyContract(gl.Contract):
    items: DynArray[Item]          # Declaration of a dynamic array of `Item`

```

The type parameter inside `DynArray[...]` must be a storage-compatible class. Built-in types such as `u256` are also valid. Without `@allow_storage`, the GenVM linter will refuse to store the custom class in contract state.

## Appending and Reading Elements

New elements are added with `append()`, which executes in constant time. Reading by index uses standard bracket notation, and the VM performs automatic bounds checking that reverts the transaction on an out-of-range access.

```python
@gl.public.write
def add_item(self, item_id: u256, item_name: str) -> None:
    new_item = Item(id=item_id, name=item_name)
    self.items.append(new_item)    # O(1) push

@gl.public.view
def get_item(self, index: u256) -> Item:
    # Bounds checking is automatic; out‑of‑range raises a revert

    return self.items[index]

```

Because the VM does not perform implicit type coercion, you should validate inputs before appending them to the array.

## Iterating Over and Removing Elements

`DynArray` implements the Python iterator protocol, so you can safely loop over it inside view methods. The type also supports `pop()` to remove the last element and `remove_at(index)` to delete a specific index and shift subsequent elements left. Both operations update the stored length automatically.

```python
@gl.public.view
def list_names(self) -> list[str]:
    return [item.name for item in self.items]

@gl.public.write
def pop_last(self) -> None:
    self.items.pop()               # Removes the final element

```

When you need keyed lookups rather than ordered traversal, the repository’s example contracts recommend using `TreeMap` instead. The [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) file demonstrates indexed storage patterns (`contracts/football_bets.py#L22-L25`) that apply equally to `DynArray` when choosing the right collection for your data layout.

## Complete DynArray Contract Example

The following `ScoreBoard` contract, modeled after [`contracts/dynarray_example.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/dynarray_example.py), shows the full lifecycle of a `DynArray`: declaration, append, length inspection, index lookup, and removal.

```python

# contracts/dynarray_example.py

from genlayer import *

@allow_storage
@dataclass
class Score:
    player: Address
    points: u256

class ScoreBoard(gl.Contract):
    # A dynamic array that stores Score objects

    scores: DynArray[Score]

    @gl.public.write
    def record_score(self, player: str, pts: u256) -> None:
        addr = Address(player)
        self.scores.append(Score(player=addr, points=pts))

    @gl.public.view
    def total_players(self) -> u256:
        return self.scores.length          # length property is always available

    @gl.public.view
    def get_score(self, idx: u256) -> Score:
        return self.scores[idx]

    @gl.public.write
    def remove_last(self) -> None:
        self.scores.pop()

```

This example follows the repository’s pattern of separating write methods (`record_score`, `remove_last`) from view methods (`total_players`, `get_score`) to keep state mutations explicit.

## Testing DynArray Storage Operations

You can exercise the contract in direct test mode by deploying the file and calling its methods through the test fixtures provided in the boilerplate.

```python
def test_dynarray(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/dynarray_example.py")
    direct_vm.sender = direct_alice

    contract.record_score("0x1111111111111111111111111111111111111111", 10)
    contract.record_score("0x2222222222222222222222222222222222222222", 20)

    assert contract.total_players() == 2
    assert contract.get_score(0).points == 10
    contract.remove_last()
    assert contract.total_players() == 1

```

The test confirms that `append()` increases the length, bracket indexing returns the correct element, and `pop()` decrements the stored count.

## Storage Best Practices for GenLayer Contracts

Follow these rules to keep your `DynArray` usage deterministic and linter-compliant:

- **Never store native Python collections** such as `list` or `dict`. The GenVM linter rejects them at compile time, as enforced by the rules in [`CLAUDE.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/CLAUDE.md) and [`README.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/README.md).
- **Always apply `@allow_storage`** to any custom class you intend to persist inside a `DynArray`.
- **Validate all inputs** before appending. The VM does not coerce types automatically.
- **Prefer `TreeMap` for keyed lookups** and reserve `DynArray` for ordered, index-based data, aligning with the design shown in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py).

## Summary

- **DynArray** is the GenVM-approved dynamic array for persistent, deterministic contract storage.
- Declare it with `DynArray[<type>]` and decorate custom classes using `@allow_storage`.
- Use `append()` for O(1) inserts, bracket indexing for reads, and `pop()` or `remove_at()` for deletions.
- Native Python collections are forbidden; consult [`CLAUDE.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/CLAUDE.md) and [`README.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/README.md) for the approved type list.
- Write explicit tests using the boilerplate’s direct-mode fixtures to verify length and element access.

## Frequently Asked Questions

### What happens if I use a native Python list instead of DynArray in a GenLayer contract?

The GenVM linter rejects native Python collections at compile time. According to the storage rules documented in `CLAUDE.md#L72-L73` and `README.md#L64-L66`, you must replace `list` and `dict` with approved types such as `DynArray` or `TreeMap`.

### Does DynArray support arbitrary Python types?

No. The generic parameter in `DynArray[<type>]` must be a storage-compatible type, such as `u256` or a custom class decorated with `@allow_storage` and `@dataclass`. Arbitrary Python objects that lack these annotations cannot be persisted in contract state.

### How do I check the number of elements in a DynArray?

Access the `length` property on the array instance. The VM updates this value automatically on every `append()`, `pop()`, or `remove_at()` operation, so it always reflects the current stored element count.

### Is DynArray or TreeMap better for keyed lookups in GenLayer?

Use **TreeMap** when you need keyed or associative lookups. Reserve **DynArray** for ordered, index-based data, as demonstrated by the storage patterns in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py).