◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Pine Script Arrays: A Practical Guide to push, get, Loops, and Pitfalls
Pine Script

Pine Script Arrays: A Practical Guide to push, get, Loops, and Pitfalls

A working guide to arrays in Pine Script v6: creating them with array.new<type>(), the core push/get/set/size API, safe looping with for...in, and the pitfalls that actually bite — above all the var-versus-rebuilt-each-bar mistake that silently corrupts historical calculations, plus out-of-bounds indexes and reference semantics. Every recommended example compiles under v6 and passes our deterministic validator. Educational material only, not financial advice; arrays are a data structure, and nothing built with them is implied to be profitable — trading carries a real risk of loss.

How do arrays work in Pine Script, and when should you use them?

*A Pine Script array is a dynamically sized, one-dimensional collection of values of a single type, created with `array.new<type>()` and manipulated through the `array. namespace — array.push(), array.get(), array.set(), array.size().** Unlike a series, which gives you one value per bar with history reachable via the []` operator, an array is a container that exists within the script's execution and can hold any number of values on a single bar. You reach for arrays when the built-in series model is not enough: custom rolling windows with non-standard aggregation (a rolling median, a percentile), collecting swing points or levels whose count is unknown in advance, sorting, or carrying a working set of values across bars.

The execution model is the part that makes or breaks correctness. Pine runs your entire script once per bar, left to right across history. Any variable declared without var is re-initialised on every one of those runs. So myArr = array.new<float>() creates a brand-new empty array on every single bar, while var myArr = array.new<float>() creates it once, on the first bar, and lets its contents persist and grow as the script walks forward. Choosing wrongly does not produce an error — it produces plausible-looking numbers that are quietly wrong across all of history, which is why an entire section below is devoted to it.

Type discipline is strict: an array<float> holds floats and array.push(prices, "abc") will not compile. Available element types include float, int, bool, string, color, line, label, box, and user-defined types. Indexing is zero-based: the first element is array.get(a, 0), the last is array.get(a, array.size(a) - 1) — and negative indexes are a runtime error, not Python-style shorthand.

The core API: creating, filling, reading, and updating

The functions you will use constantly, in one compilable reference script:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Array API tour", overlay = false)

// Create: empty, or pre-sized with an initial value.
var prices = array.new<float>()          // empty, grows as you push
var flags  = array.new<bool>(5, false)   // 5 elements, all false

// Fill: push appends to the end; unshift prepends at index 0.
array.push(prices, close)

// Cap the container so it does not grow without bound.
if array.size(prices) > 50
    array.shift(prices)                  // drop the oldest (index 0)

// Read and update by index (zero-based).
first = array.size(prices) > 0 ? array.get(prices, 0) : na
last  = array.size(prices) > 0 ? array.get(prices, array.size(prices) - 1) : na

// Built-in aggregation — no manual loop needed for the common cases.
plot(array.avg(prices),    "Mean of window")
plot(array.median(prices), "Median of window")
plot(array.max(prices),    "Max of window")
plot(last, "Newest element", display = display.none)
plot(first, "Oldest element", display = display.none)

A few things worth reading twice. The push-then-shift pair is the standard rolling window idiom: append the newest value, and once the array exceeds the window length, drop the oldest. array.pop() removes from the end instead — using pop where you meant shift silently turns your rolling window into a "first N bars of the chart" window, a bug that looks fine on recent bars and is wrong everywhere else.

The aggregation family — array.avg(), array.sum(), array.min(), array.max(), array.median(), array.stdev(), array.sort(), array.percentile_nearest_rank() — covers most reasons people write manual loops, and the built-ins are both faster and harder to get wrong. Reach for a loop only when no built-in expresses your logic.

The var trap: rebuilt-each-bar arrays silently corrupt your history

This is the mistake the whole article exists for, because it produces no error and no visual glitch — just numbers that are wrong in a way you will not notice until something downstream depends on them.

// ❌ WRONG — no `var`: the array is recreated EMPTY on every bar,
// so after the push it always contains exactly one element (today's close).
// The "rolling median of 21 closes" is silently just... the close itself.
// closes = array.new<float>()
// array.push(closes, close)
// if array.size(closes) > 21
//     array.shift(closes)          // never true — size is always 1
// plot(array.median(closes))       // plots close, labelled as a median

Because array.median() of a one-element array is that element, the plot tracks price perfectly and looks like a fast-reacting median. Feed it into a signal and every historical calculation built on it is corrupted — confidently, invisibly. The fix is one keyword:

//@version=6
// Educational only — validate before trading; not financial advice.
indicator("Rolling median via array", overlay = true)
len = input.int(21, "Window length", minval = 1)

var closes = array.new<float>()      // created ONCE, persists across bars
array.push(closes, close)
if array.size(closes) > len
    array.shift(closes)

plot(array.median(closes), "Rolling median", linewidth = 2)

The inverse mistake exists too: using var on an array you intended to rebuild fresh each bar — say, a per-bar scratch list of conditions — and forgetting to array.clear() it first. Then the array accumulates every bar's entries forever, aggregations drift, and on long histories you eventually hit runtime limits. The discipline is to decide explicitly, for every array: persistent accumulator (var, with a size cap) or per-bar scratch space (no var, or var plus array.clear() at the top of the bar). A quick self-test: plot array.size() — a persistent window should ramp up then plateau at the cap; scratch space should stay constant. Anything else is the bug above.

Looping over arrays: why for...in beats index arithmetic

Pine gives you two loop forms for arrays, and one of them removes an entire class of bugs. The index-arithmetic form is the one most people write first:

// Index form — correct only because of the explicit size guard.
sumAbove = 0.0
if array.size(prices) > 0
    for i = 0 to array.size(prices) - 1
        v = array.get(prices, i)
        sumAbove += v > close ? v : 0.0

The guard is load-bearing: when the array is empty, array.size(prices) - 1 is -1, and the loop bounds 0 to -1 are exactly the sort of edge you do not want to reason about at review time — on top of which any off-by-one in the bound is an immediate out-of-bounds runtime error that halts the script on the offending bar. The for...in form makes the whole problem structural:

// for...in form — no indexes, no bounds, empty arrays are simply zero iterations.
sumAbove2 = 0.0
for v in prices
    sumAbove2 += v > close ? v : 0.0

// Need the index too? Destructure it:
for [i, v] in prices
    // i is the index, v the value
    na

An empty array yields zero iterations — no guard, no -1, nothing to get wrong. Our validator flags manual 0 to size-1 loops for exactly this reason; for...in is the recommended default, with the indexed destructuring form for [i, v] covering the cases where position matters.

One hard rule regardless of form: do not insert into or remove from an array while iterating over it. Mutating the collection mid-loop shifts elements under the iterator and produces skipped or double-processed values. Collect the indexes or values to change into a second array during the loop, then apply the mutations afterwards. It is one extra line and it removes an entire category of order-dependent bugs.

Remaining pitfalls: out-of-bounds, negative indexes, na, and reference semantics

Four smaller traps round out the list, each cheap to avoid once named.

Out-of-bounds and negative indexes are runtime errors, not conveniences. array.get(a, array.size(a)) is out of bounds by one; array.get(a, -1) is not "last element" as it would be in Python — it halts the script. For the endpoints, use the purpose-built accessors array.first(a) and array.last(a), and remember even those error on an empty array, so a array.size(a) > 0 check still guards them.

`na` propagates through aggregations. Push close on a symbol with sparse data, or push the result of a calculation that is na during its warm-up bars, and your window quietly contains na values; array.avg() and friends handle them by ignoring, but your own loops will not unless you check na(v) explicitly. Decide at the push site: either skip na values (if not na(x)) or push a sentinel you handle downstream. Do not discover the policy inside a loop three functions away.

Arrays are references, not values. b = a does not copy the array — both names now point at the same object, and array.push(b, x) changes what a sees. This is also true when passing arrays into functions: the function mutates the caller's array. When you need an independent snapshot, array.copy(a) is the explicit, intentional way to get one. Reference semantics are a feature — they make arrays cheap to pass around — but only if you know you opted into them.

Know when not to use an array at all. If what you need is "the value N bars ago", the series history operator close[n] already does that, with less code and less to corrupt. Arrays earn their complexity for non-standard aggregations and variable-length collections — not as a reimplementation of what series already give you. And as always: these are correctness tools. Nothing about a well-built array makes the logic it feeds profitable; this guide is educational only, and trading carries a real risk of loss.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro