Section 01 · From scratch
What is an array in Solidity?
An array is a single variable that stores many values of the same type, laid out in order. Each value sits at a numbered position called an index, starting at zero. If you have ever used a list in Python or an array in JavaScript, the mental model is the same — only the rules around growth and gas are stricter.
Quick answer
What is an array? A Solidity array is an ordered, zero indexed collection of values that all share one type. The type is part of the declaration — uint256[] holds only uint256 values, address[] holds only addresses. You read a value by its index and, on dynamic storage arrays, you can append with .push() or remove the last element with .pop().
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HelloArray {
// Three numbers, fixed length, declared with initial values.
uint256[3] public initialScores = [uint256(10), 20, 30];
// An empty list of addresses that can grow over time.
address[] public players;
}The array initialScores always holds three numbers. You can change a value at a position, but you cannot add a fourth slot or remove one. The array players starts empty. Every call to players.push(someAddress) appends a new entry and the array grows by one.
Section 02 · Declaring arrays
Five ways to declare an array
Arrays show up in five common shapes. Knowing each one lets you read most production contracts comfortably.
// 1. Fixed length, declared with initial values.
uint256[3] public scores = [uint256(10), 20, 30];
// 2. Fixed length, default zeroed values.
bool[5] public flags; // [false, false, false, false, false]
// 3. Dynamic, starts empty, grows with push.
address[] public players;
// 4. Dynamic array of structs.
struct Bid { address bidder; uint256 amount; }
Bid[] public bids;
// 5. Two-dimensional array (array of arrays).
uint256[][] public matrix; // each row can have its own lengthTwo small surprises catch new Solidity developers. The first is the cast in [uint256(10), 20, 30]: without it the literal [10, 20, 30] is interpreted as uint8[3], which is not assignable to uint256[3]. The second is the difference between uint256[3][] and uint256[][3] — Solidity reads the dimensions in the opposite order from C, so the inner type is the one that comes after the variable name when reading left to right.
Section 03 · The full API
Indexing, length, push, pop, delete
The Solidity array API is intentionally small. Five operations cover almost every real contract.
| Operation | Syntax | Works on | Note |
|---|---|---|---|
| Read by index | a[i] | all arrays | reverts if i >= length |
| Write by index | a[i] = x | fixed and dynamic, mutable | must be in range |
| Length | a.length | all arrays | free read, never throws |
| Append | a.push(x) | dynamic storage only | grows the array by one |
| Remove last | a.pop() | dynamic storage only | refunds some gas |
| Reset slot | delete a[i] | all mutable arrays | does not shrink, only zeroes |
uint256[] public list;
list.push(7); // list = [7]
list.push(11); // list = [7, 11]
list.push(13); // list = [7, 11, 13]
uint256 first = list[0]; // 7
uint256 n = list.length; // 3
list.pop(); // list = [7, 11], length = 2
delete list[0]; // list = [0, 11], length is STILL 2What delete really does
delete sets the slot to the default value of its type — zero for numbers, false for bools, address(0) for addresses. It does not shift later elements down and it does not reduce length. To genuinely remove an item, use the swap and pop pattern below.
// Swap-and-pop: remove an element in O(1) without leaving a hole.
function removeAt(uint256 i) external {
require(i < list.length, "out of bounds");
list[i] = list[list.length - 1]; // overwrite slot with last
list.pop(); // shrink by one
}Swap and pop trades order for speed. After the call the element that used to be last is now sitting where you removed something — the array is no longer in insertion order. If order genuinely matters (rare in smart contracts), you have to shift every later element down by one, which costs a lot of gas and is only safe for small arrays.
Section 04 · Data locations
storage, memory, and calldata
Arrays look the same in source code, but where they live decides what you can do with them and how much gas the call costs.
// storage — declared at contract level, persists across transactions
address[] public allHolders;
// memory — fixed-size scratch space, cleared at end of the call
function topThree() external pure returns (uint256[] memory) {
uint256[] memory out = new uint256[](3);
out[0] = 1; out[1] = 2; out[2] = 3;
return out;
}
// calldata — read-only input, cheapest of the three
function airdrop(address[] calldata recipients, uint256 amount) external {
for (uint256 i = 0; i < recipients.length; i++) {
// recipients[i] = address(0); ← compile error, calldata is read-only
// do something with recipients[i]
}
}The rule of thumb that survives most code review feedback is to declare external function inputs as calldata whenever the function does not need to modify them. Use memory for working arrays inside a function. Reach for storage when you truly need state that lives between transactions.
Section 05 · Iterating safely
Loops and the unbounded loop bug
Loops over arrays are fine when the length is bounded by the contract. They are dangerous when the length depends on user input.
// Cache .length in a local — saves an SLOAD on every iteration.
uint256[] public values;
function sum() external view returns (uint256 total) {
uint256 len = values.length;
for (uint256 i = 0; i < len; ++i) {
total += values[i];
}
}The pattern above is safe because values.length is controlled by the contract — only the contract decides when to push, so the array cannot grow without limit. The same loop becomes dangerous the moment users can call push directly.
// DANGEROUS — anyone can join, refundAll loops over everyone
address[] public participants;
function join() external { participants.push(msg.sender); }
function refundAll() external onlyOwner {
for (uint256 i = 0; i < participants.length; i++) {
payable(participants[i]).transfer(1 ether);
}
// attacker registers 10 000 throwaway addresses → refundAll runs out of
// gas → refunds revert forever → contract is effectively bricked.
}Pull over push
The fix that scales is to give each user a way to claim their own funds — store balances in a mapping and expose a withdraw() function. The contract never iterates the list, so growth in participants never breaks the contract. This is OpenZeppelin's PullPayment pattern and the standard for distributing DeFi rewards.
Section 06 · Worked example
A complete contract using arrays
A small but realistic contract that tracks a list of student scores. It demonstrates declaring, pushing, indexing, iterating, swap and pop removal, and exposing a getter for the full list.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ScoreBook {
address public immutable owner;
// Each score is one entry in this array.
uint256[] private scores;
// Event emitted each time a score is added.
event ScoreAdded(uint256 indexed index, uint256 value);
event ScoreRemoved(uint256 indexed index);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
// 1. Append a score to the end.
function addScore(uint256 value) external onlyOwner {
scores.push(value);
emit ScoreAdded(scores.length - 1, value);
}
// 2. Read a score by index.
function getScore(uint256 i) external view returns (uint256) {
require(i < scores.length, "out of bounds");
return scores[i];
}
// 3. Read how many scores are stored.
function totalScores() external view returns (uint256) {
return scores.length;
}
// 4. Remove a score in O(1) by swapping with the last element.
function removeScore(uint256 i) external onlyOwner {
require(i < scores.length, "out of bounds");
scores[i] = scores[scores.length - 1];
scores.pop();
emit ScoreRemoved(i);
}
// 5. Compute the average. Loop is bounded — only the owner can grow it.
function averageScore() external view returns (uint256) {
uint256 len = scores.length;
require(len > 0, "no scores yet");
uint256 total;
for (uint256 i = 0; i < len; ++i) {
total += scores[i];
}
return total / len;
}
// 6. Return a memory copy of every score for off-chain consumers.
function allScores() external view returns (uint256[] memory) {
return scores;
}
}Walk through it once and the entire array surface area is on the page. addScore uses push. getScore uses indexed read with a bounds check. removeScore uses swap and pop. averageScore caches the length and iterates. allScores returns the entire storage array as a uint256[] memory, which the EVM copies into the return data for the caller.
Section 07 · Real world use cases
Where arrays show up in production
Arrays are not always the right tool, but four patterns show up over and over in audited code.
Whitelist for a sale
A small list of approved addresses kept on chain. Cheap to check membership when the list is short, and it survives across transactions because it lives in storage.
Token holder snapshot
When a project plans an airdrop, it sometimes records the holder set at a specific block in an address[] for later reference. Off chain indexers usually take over once the list grows.
Governance proposal queue
A DAO appends each proposal as a new struct in a Proposal[] array. The index becomes the proposal id and the array grows as governance activity accumulates.
Merkle proof input
An airdrop verifier accepts bytes32[] calldata proof so the user can claim their share. The array stays in calldata, which makes it the cheapest possible input shape.
Section 08 · Common pitfalls
Five mistakes beginners make
Forgetting that delete does not shrink
delete a[i] resets the slot to zero, but the array still has the same length. If you forget this, your loops keep visiting the empty slot and your tally is wrong.
Iterating user controlled lengths
Any loop whose bound depends on an array users can grow is a denial of service risk. Cap the number of iterations, paginate, or switch to a pull pattern.
Using storage where memory is enough
Reading a storage slot is hundreds of gas per access. If you only need a value once inside a function, copy it into a local memory variable first.
Returning huge arrays from view functions
A view that returns a 10 000-entry array works in tests and reverts in production wallets that cap eth_call response size. Paginate the return value instead.
Confusing array of arrays dimension order
uint256[3][] is a dynamic list of length three rows. uint256[][3] is a fixed three row matrix where each row is dynamic. Beginners flip them constantly.
Section 09 · Decision guide
When NOT to use an array
Arrays earn their keep when order matters or you genuinely need to enumerate the contents on chain. The moment neither holds, a mapping is almost always the better choice.
| You need… | Best fit | Why |
|---|---|---|
| Lookup by known key | mapping | O(1) read, no iteration cost |
| Ordered list with insertion order | array | indices give you the order |
| Membership check on a small list | array | fine up to ~50 entries |
| Membership check on a large list | mapping(address => bool) | scales without iteration cost |
| Enumerate every entry on chain | array | mappings have no iteration |
| Both lookup and enumeration | array + mapping | OpenZeppelin EnumerableSet |
A surprising amount of production code uses both at the same time — an array of keys for occasional iteration, plus a mapping from key to struct for fast lookup. OpenZeppelin packages this combination as EnumerableSet, which is worth reading once even if you never use it directly.
Section 11 · FAQ
Frequently asked questions
What is an array in Solidity in simple words?
An array is a single variable that holds many values of the same type, stored in order. Each value lives at a numbered position called an index. Indexes start at zero, so the first value is at position zero, the second at position one, and so on.
What is the difference between fixed and dynamic arrays?
A fixed array bakes its size into the type — uint256[5] always holds exactly five values. A dynamic array writes empty brackets — uint256[] starts empty and can grow at runtime with .push(). Fixed arrays save a small amount of gas because the layout is known at compile time, but they cannot grow.
How do I add an element to a Solidity array?
On a dynamic storage array, call .push(value). The array length grows by one and the new value lives at the new last index. Fixed length arrays cannot be appended to — you can only overwrite an existing slot with a[i] = value.
How do I remove an element from a Solidity array?
On a dynamic storage array, .pop() removes the last element. To remove an element from any other position without leaving a hole, copy the last element into the slot you are removing and then call .pop(). This pattern is called swap and pop and runs in constant time but does not preserve order.
What does delete a[i] do in Solidity?
It resets the value at index i to the default value of the array's type — zero for numbers, false for bools, address(0) for addresses. It does not move later elements down and it does not change the array length. The slot still exists; it just holds the type's zero value.
When should I use storage, memory, or calldata for an array?
Use storage for state variables that must persist across transactions. Use memory for arrays you create inside a function and only need until the call ends. Use calldata for external function inputs you do not need to modify — it is the cheapest of the three because the data stays in the call's input region.