Array indexes start at 0 in most programming languages because the index is an offset: it counts how many elements come before the one you want, so the first element is 0 steps from the start. The convention comes from BCPL and C, where a[i] literally means “the address of a plus i elements”, and it survived because it makes ranges and loops simpler. Languages such as Lua, MATLAB, R, Julia and Fortran count from 1 instead.
Below, each part of that answer is shown with code I ran: the memory addresses of a C array, the machine code a compiler generates for 0-based and 1-based access, Dijkstra’s argument about ranges, and what happens in five languages when you get the index wrong.
What Zero-Based Indexing Means
In a zero-based (or 0-indexed) array, the first element has index 0, the second index 1, and the last index length − 1. In a one-based (1-indexed) array, the first element has index 1 and the last has index length.
langs = ["C", "Java", "Python", "JavaScript"]
print(langs[0]) # first element
print(langs[len(langs) - 1]) # last element
print(langs[-1]) # Python shortcut for the last element
for i in range(len(langs)): # range(4) is 0, 1, 2, 3
print(i, langs[i])
print(langs[0:2], langs[2:4]) # half-open slices split cleanly
try:
langs[4]
except IndexError as e:
print("IndexError:", e)C
JavaScript
JavaScript
0 C
1 Java
2 Python
3 JavaScript
['C', 'Java'] ['Python', 'JavaScript']
IndexError: list index out of rangeThe easiest way to think about it is Edsger Dijkstra’s phrasing: an element’s index equals the number of elements before it. “C” has nothing before it, so its index is 0.
Reason 1: The Index Is an Offset From the Start
An array is stored as one block of memory. To find element i, the program takes the address of the first element and moves forward i × element size bytes. This C program prints where each element lives:
#include <stdio.h>
int main(void) {
int a[4] = {10, 20, 30, 40};
for (int i = 0; i < 4; i++) {
printf("a[%d] = %d at %p (base + %zu bytes)\n",
i, a[i], (void *)&a[i], (size_t)((char *)&a[i] - (char *)a));
}
printf("*(a + 2) = %d\n", *(a + 2)); /* what a[2] means */
printf("2[a] = %d\n", 2[a]); /* legal, because a[i] is *(a + i) */
return 0;
}a[0] = 10 at 0x16f156670 (base + 0 bytes)
a[1] = 20 at 0x16f156674 (base + 4 bytes)
a[2] = 30 at 0x16f156678 (base + 8 bytes)
a[3] = 40 at 0x16f15667c (base + 12 bytes)
*(a + 2) = 30
2[a] = 30Each int is 4 bytes, so element i sits exactly 4 × i bytes after the start, and element 0 sits at the start itself. The C standard defines a[i] as *(a + i): “go i elements past a and read what is there”. The odd-looking 2[a] compiles and prints 30 for the same reason, because *(2 + a) is the same as *(a + 2). With that definition, starting at 1 would mean the first element is at a + 1, one element past the start, which makes no sense.
Reason 2: History, From BCPL to C to Everything Else
Zero-based arrays didn’t start with C. In 2013, programmer Mike Hoye asked Martin Richards, who designed BCPL in the 1960s, why BCPL arrays start at 0. Richards explained that BCPL was a typeless language close to machine code: if v points to a block of words, v!i means the word at v + i, so “it is entirely natural for the first element of the array to have subscript zero”. He added that C copied this approach, using * and [ ] where BCPL used !.
C then shaped C++, Java, C#, JavaScript, Go, Rust and Python, which all kept 0-based arrays. Today, most programmers learn arrays in a 0-based language, so the convention reinforces itself.
Is 0-based indexing faster?
This is the most common explanation, and today it is mostly a myth. Starting at 1 means computing a + (i − 1), but compilers fold that −1 into the instruction. I compiled the same lookup both ways with Apple clang 21 at -O2:
int get_zero_based(const int *a, long i) { return a[i]; }
int get_one_based(const int *a, long i) { return a[i - 1]; }| Zero-based a[i] | One-based a[i − 1] | |
|---|---|---|
| x86-64 | movl (%rdi,%rsi,4), %eax | movl -4(%rdi,%rsi,4), %eax |
| ARM64 (Apple M2) | ldr w0, [x0, x1, lsl #2] | add x8, x0, x1, lsl #2ldur w0, [x8, #-4] |
On x86-64, both versions are a single instruction; the −4 byte offset is free. On ARM64, the one-based version needs one extra add. That cost is real but tiny, and loops often hide it completely. It mattered more on the hardware of the 1960s and 1970s, when BCPL and C were designed, than it does now.
Reason 3: Half-Open Ranges Are Simpler
The strongest modern argument comes from Edsger Dijkstra’s note “Why numbering should start at zero” (EWD831, August 1982). He compared four ways to write a range of integers and concluded that the best is lower ≤ i < upper, which includes the lower bound and excludes the upper one, because:
- The length of the range is simply
upper − lower. - Two adjacent ranges share a bound:
0 ≤ i < 2and2 ≤ i < 4. - An empty range is natural:
0 ≤ i < 0.
With that convention, a sequence of N elements gets the clean range 0 ≤ i < N if you start at 0, but the awkward 1 ≤ i < N + 1 if you start at 1. You can see the benefit in the Python example above: range(len(langs)) covers every index, and langs[0:2] and langs[2:4] split the list into two halves with no overlap and no gap. Dijkstra also noted that the Mesa language at Xerox PARC supported all four conventions, and its programmers found the other three “a constant source of clumsiness and mistakes”.
Which Languages Start Arrays at 1?
| Starts at 0 | Starts at 1 | You choose the bounds |
|---|---|---|
| C, C++, C#, Java, JavaScript, TypeScript, Python, Go, Rust, Swift, Kotlin, PHP, Ruby | Lua, MATLAB, R, Julia, Fortran (by default), COBOL, Smalltalk, Mathematica | Pascal, Ada, Fortran (declared bounds such as a(0:9)) |
One-based languages are mostly those built for mathematics and science, where matrices and vectors have always been numbered from 1. Lua shows how they behave:
local langs = {"C", "Java", "Python", "JavaScript"}
print(langs[1], langs[#langs]) -- Lua tables start at 1
print(langs[0]) -- nil: there is no element 0C JavaScript
nilWatch out for 1-based indexing outside “1-based languages”, too. In SQL, string positions start at 1, even when you call SQL from Python or JavaScript. In SQLite 3.54:
SELECT substr('scientyfic', 1, 3), substr('scientyfic', 0, 3), instr('scientyfic', 's');
-- sci | sc | 1Passing 0, as you would in Python, silently returns one character fewer (sc) instead of raising an error.
Off-by-One Errors: The Price of Zero-Based Indexing
Because the last index is length − 1, not length, it is easy to go one step too far. Two quick examples:
- An array with 15 elements, indexed from 0, has its last element at index 14.
- For
p = [12, 7, 9, 4, 20, 6], the last index is 5 andp[5]is 6.
What happens when you do go too far depends on the language:
| Language | Reading one past the end | What I saw |
|---|---|---|
| Python | Raises an exception | IndexError: list index out of range |
| Java 21 | Raises an exception | ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 |
| JavaScript | Returns undefined, no error | undefined |
| Lua | Returns nil, no error | nil |
| C | Undefined behaviour: reads whatever is in memory | a[4] = -88325760 (garbage) |
The C case is the dangerous one. This loop uses <= instead of <:
#include <stdio.h>
int main(void) {
int a[4] = {10, 20, 30, 40};
for (int i = 0; i <= 4; i++) /* off-by-one: should be i < 4 */
printf("a[%d] = %d\n", i, a[i]);
return 0;
}It compiled without a warning (with -Wall) and printed a garbage fifth value. Compiling with AddressSanitizer (-fsanitize=address) caught it immediately as a stack-buffer-overflow. Bugs like this are the source of many security vulnerabilities, which is one reason newer languages check bounds at runtime.
The habits that prevent off-by-one errors are simple: loop with i < length, not i <= length; prefer for item in items or for...of loops that don’t use indexes at all; and use built-ins for the last element (items[-1] in Python, items.at(-1) in JavaScript).
Zero-Based vs One-Based: Pros and Cons
| Zero-based | One-based | |
|---|---|---|
| Matches memory offsets | Yes: index = offset | Needs a −1, usually free after compilation |
| Ranges | 0 ≤ i < N, length = N | 1 ≤ i ≤ N, closed range |
| Modular arithmetic (wrap-around) | i % N works directly | Needs (i − 1) % N + 1 |
| Matches everyday counting | No: the “first” item is item 0 | Yes |
| Matches maths notation | No | Yes: matrices and vectors start at 1 |
| Typical languages | C family, Python, JavaScript, Go, Rust | Lua, MATLAB, R, Julia, Fortran |
Neither is wrong. Zero-based indexing fits how memory and ranges work; one-based indexing fits how people and mathematicians count. What matters is knowing which one your language uses, especially when you move between, say, Python and SQL or R. If you are learning about arrays from the start, see arrays in C.
Conclusion
Arrays start at 0 because the index is an offset from the start of the array: a[i] means “move i elements past the beginning”, so the first element is at offset 0. The idea came from BCPL, C made it standard, and Dijkstra’s half-open ranges (0 ≤ i < N) explain why it remains convenient. The speed argument is mostly historical: a modern compiler makes 1-based access cost nothing or one extra instruction. The practical lesson is to remember that the last index is length − 1, and to let your language’s loops and bounds checks protect you from off-by-one errors.
FAQs
Why do array indexes start at 0 instead of 1?
Because the index is an offset: it counts how many elements come before the one you want. In C, a[i] means the element i positions after the start, so the first element is a[0]. C inherited this from BCPL, and most later languages copied C.
Does Python start at 0 or 1?
Python starts at 0 for lists, tuples and strings. The first element is items[0] and the last is items[len(items) – 1], or simply items[-1].
Does a string index start at 0?
In most languages, yes: Python, JavaScript, Java and C all start string indexes at 0. SQL is the common exception: SUBSTRING and substr positions start at 1.
What is a 1-indexed array?
An array whose first element has index 1 and whose last element has index equal to its length. Lua, MATLAB, R, Julia and Fortran use 1-based indexing by default.
If an array has 15 elements, what is the index of the last element?
With 0-based indexing, the last index is 15 – 1 = 14. With 1-based indexing, it is 15.
Is 0-based indexing faster than 1-based?
Barely, if at all, on modern hardware. Compilers fold the minus one into the memory instruction: on x86-64 both versions compile to one instruction, and on ARM64 the 1-based version needs one extra add.








