Why Programmers Count From Zero
The mathematical, hardware, and cognitive reasons behind zero-based indexing
The Question That Confuses Everyone
If you've ever tried to explain arrays to a non-programmer, you've hit this wall. "The first element is at index zero." Their eyes narrow. "Why not one?" And you realize you're not entirely sure of the answer yourself.
Zero-based indexing — the convention that the first element of a sequence is numbered 0, not 1 — is one of computing's most fundamental and least intuitive choices. It shows up in C, Java, Python, JavaScript, Rust, Go, and nearly every mainstream language. It's not a law of nature. It's a design decision. But it's a design decision with surprisingly deep roots.
Dijkstra's Argument
The most famous defense of zero-indexing comes from Edsger W. Dijkstra, one of the towering figures of computer science. In a 1982 manuscript titled "Why numbering should start at zero" (EWD 831), Dijkstra considered four ways to denote a subsequence of natural numbers, say 2, 3, ..., 12:
2 <= i < 131 < i <= 122 <= i <= 121 < i < 13
He argued that convention (1) — inclusive lower bound, exclusive upper bound — was superior. It makes the difference between the bounds equal to the length of the sequence. It makes adjacent ranges share a boundary cleanly: [0, 5) followed by [5, 10) covers [0, 10) without overlap or gap. And when you use this convention, starting from zero falls out naturally.
"Should the smallest natural number be 0 or 1? Dijkstra's answer was unambiguous: when the upper bound is exclusive, starting from 0 gives the most elegant notation."
This is not merely aesthetic. When ranges compose cleanly, entire classes of off-by-one errors disappear. The range(10) in Python produces [0, 1, 2, ..., 9] — exactly 10 elements, and the length equals the argument. That's Dijkstra's convention in action.
The Hardware Story
But Dijkstra was writing in 1982, and zero-indexing predates his argument by decades. The real origin is hardware.
In the earliest computers, memory was addressed directly. An array wasn't an abstract data structure — it was a block of contiguous memory locations. If your array started at memory address 1000, and each element was 4 bytes wide, then the address of element i was:
address = base + (i * element_size)
If i starts at 0, the first element is at base + 0 — which is just base. No addition needed. If i started at 1, you'd need to subtract 1 every time: base + (i - 1) * element_size. On machines where every instruction mattered, that extra subtraction was a real cost.
The PDP-11, the machine on which C was born, made this pattern especially natural. Dennis Ritchie and Ken Thompson at Bell Labs designed C's array semantics to map directly to PDP-11 addressing modes. In C, a[i] is literally defined as *(a + i) — pointer arithmetic with zero as the natural starting offset.
BCPL and B: The Ancestors
C inherited zero-indexing from its predecessor B, which inherited it from BCPL (Basic Combined Programming Language, 1967). Martin Richards designed BCPL with a simple memory model: a variable holding an array was a pointer to the first word, and subscripting added an offset. Zero was the first offset. The convention wasn't debated — it was simply the obvious mapping from language to hardware.
When Thompson wrote B for the PDP-7 at Bell Labs in 1969, he kept this convention. When Ritchie evolved B into C in 1972, he kept it again. And when C conquered the world, zero-indexing conquered with it.
The Mathematical Perspective
There's also a purely mathematical case. In modular arithmetic, which is fundamental to computing, zero is the identity element for addition and the natural starting point for cyclic groups. When you compute i % n, the results range from 0 to n-1. Hash tables, circular buffers, and ring data structures all exploit this. If arrays started at 1, every modular operation would need an adjustment.
Consider a circular buffer of size n. The next position after i is (i + 1) % n. This gives you positions 0, 1, 2, ..., n-1, 0, 1, 2, ... — clean and simple. With 1-based indexing, it would be (i % n) + 1, adding a constant source of bugs.
The Dissenters
Not everyone agreed. Fortran, the oldest surviving programming language (1957), uses 1-based indexing by default. So does MATLAB, R, Julia, and Lua. These languages serve communities — scientists, statisticians, mathematicians — where counting from 1 is the universal convention in their domain.
Lua's creator, Roberto Ierusalimschy, has said that 1-based indexing was a deliberate choice to serve Lua's original audience of non-programmer engineers at Petrobras, the Brazilian oil company. For people who think in terms of "the first item, the second item," 1-based indexing is simply more natural.
Pascal went further, allowing arrays to start at any index the programmer chose. You could declare an array indexed from -5 to 5 if you wanted. This flexibility sounds appealing but turns out to create its own problems — interoperability between different arrays becomes harder when their bounds are arbitrary.
The Cognitive Cost
Research in software engineering has consistently shown that off-by-one errors are among the most common bugs. Zero-indexing contributes to this for beginners, but proponents argue it reduces these errors for experienced programmers by making range calculations cleaner.
The key insight is that zero-indexing optimizes for composition at the expense of intuition. Slicing, striding, and modular operations all work more cleanly. But explaining to a new programmer why array[0] is the "first" element will never feel natural.
The Mathematical Argument
The mathematical case for zero-based indexing goes deeper than convenience. In modular arithmetic, which is fundamental to computing, zero is the identity element for addition. Array indices are fundamentally offsets from a base address, and the first element has zero offset. This is not arbitrary. It is a consequence of how memory addressing works.
Dijkstra's argument can be formalized. Consider a sequence of N elements. There are four ways to describe the range: [0, N), (−1, N), [0, N−1], (−1, N−1]. The half-open interval [0, N) is the most natural because the difference between the bounds equals the number of elements (N − 0 = N), and the lower bound equals zero, which is the smallest natural number. Any other convention either uses negative numbers (which introduce unnecessary complexity) or requires adding or subtracting one to compute the count (which creates off-by-one errors).
The practical consequences are pervasive. In a zero-indexed array of N elements, the valid indices are 0 through N−1. The index of the last element is length − 1. The number of elements between index i and index j (inclusive) is j − i + 1. These formulas are simple but require careful attention. The off-by-one error, or "fencepost error," is one of the most common bugs in programming precisely because the relationship between element count and index boundaries requires one mental adjustment.
Language-by-Language Comparison
The choice of zero-based or one-based indexing varies by language and reveals design priorities:
Zero-based: C, C++, Java, JavaScript, Python, Rust, Go, Swift. These languages prioritize consistency with hardware addressing (C), compatibility with C (most others), or mathematical elegance (Python, Rust).
One-based: Fortran, MATLAB, R, Lua, Julia. These languages prioritize human intuition: the first element is element 1, the second is element 2, and so on. Fortran's choice reflects its mathematical heritage; MATLAB and R follow Fortran's convention because they serve mathematicians and statisticians who think in one-based terms.
Configurable: Some languages allow the programmer to choose. Pascal permits arrays with arbitrary lower bounds. Visual Basic historically used one-based arrays by default but allowed zero-based with an Option Base directive. Ada allows any discrete type as an index. This flexibility eliminates the debate but creates inconsistency within codebases.
Special cases: APL uses one-based indexing by default but allows the user to set the index origin to 0 or 1 (the ⎕IO system variable). This was a pragmatic choice for a language used by both mathematicians (who prefer 1) and computer scientists (who prefer 0).
The Human Cost of Zero-Based Indexing
The off-by-one error is sometimes called the "hardest problem in computer science" (tongue-in-cheek, but with a grain of truth). Zero-based indexing contributes to several common bug patterns:
Buffer overflows: Accessing array[length] instead of array[length − 1] reads or writes memory beyond the array's bounds. In C, this is undefined behavior and a major source of security vulnerabilities. Modern languages (Python, Java, Rust) perform bounds checking and raise exceptions rather than allowing out-of-bounds access.
Loop boundary errors: Writing for (int i = 0; i <= n; i++) instead of for (int i = 0; i < n; i++) iterates n+1 times instead of n. The half-open interval convention (start inclusive, end exclusive) is the standard idiom in zero-based languages, but it requires learning a convention that does not match everyday counting.
Pagination bugs: When displaying items 1-10 of 100, the underlying array indices are 0-9. The conversion between user-facing one-based numbering and internal zero-based indices is a common source of bugs in user interfaces, APIs, and database queries.
Despite these costs, zero-based indexing has won in mainstream programming. The C language's influence, combined with the genuine mathematical elegance of zero-based ranges, has established it as the default convention. Languages that choose one-based indexing (R, MATLAB, Julia) do so consciously and for specific audiences who prefer it.
Key Takeaways
- Zero-based indexing originated from hardware addressing conventions where array access is calculated as base_address + (index * element_size), making zero the natural first index.
- Dijkstra's formal argument showed that half-open intervals [0, N) produce the cleanest mathematics for sequence manipulation, avoiding both negative numbers and off-by-one adjustments.
- The convention varies by language: systems and general-purpose languages use zero-based indexing, while mathematical and statistical languages often use one-based indexing.
- Off-by-one errors (buffer overflows, loop boundary mistakes, pagination bugs) are a significant human cost of zero-based indexing, contributing to both bugs and security vulnerabilities.
- Zero-based indexing is a convention rooted in practical engineering, not an inherent truth about counting. Both conventions are internally consistent; the industry chose zero primarily because C chose zero, and C chose zero because of PDP-11 addressing.
The Cultural Dimension
The zero vs. one debate has become a cultural shibboleth in programming. Experienced programmers who have internalized zero-based indexing often view one-based indexing as a sign of inexperience or naivety. This is unfair. One-based indexing is a legitimate design choice made by languages (Fortran, MATLAB, R, Julia) used by highly skilled scientists and mathematicians who have different intuitions about sequences than systems programmers.
The cultural dimension extends to interview questions and coding challenges. Candidates who make off-by-one errors in whiteboard interviews are penalized, even though these errors are among the most common bugs in production code written by experienced developers. The convention that demands zero-based fluency is a cultural norm, not a measure of fundamental programming ability.
Donald Knuth, one of the most respected computer scientists in history, uses one-based indexing in "The Art of Computer Programming" for mathematical notation but zero-based indexing when discussing implementation. This dual usage reflects the pragmatic reality: the "right" convention depends on whether you are thinking about the mathematics of a problem or its implementation in a specific language.
Zero-Based Indexing in Everyday Code
The practical impact of zero-based indexing shows up in common patterns that every programmer encounters:
# Getting the last element
last = array[len(array) - 1] # zero-based
# Python simplifies this with negative indexing
last = array[-1]
# Iterating with index
for i in range(len(array)): # 0 to n-1
print(f"Item {i+1}: {array[i]}") # +1 for display
# Converting between 0-based and 1-based (common in APIs)
page_number = 3 # 1-based (user-facing)
page_index = page_number - 1 # 0-based (internal)
These conversions between zero-based internal representation and one-based user-facing display are ubiquitous in software. Database result sets, pagination, line numbers in editors, and floor numbers in elevators all require converting between the convention the user expects and the convention the system uses.
Historical Context: Before Zero Was Standard
Before C standardized zero-based indexing, the computing world was fragmented. COBOL used one-based indexing because its designers wanted the language to be readable by business managers. BASIC, designed for beginners, used one-based indexing because it matched how people naturally count. Even FORTRAN's default array indexing started at 1, though FORTRAN allowed programmers to specify any lower bound.
The transition to zero-based indexing happened gradually through C's influence. When C became the lingua franca of systems programming in the 1970s and 1980s, its conventions spread to every language that borrowed its syntax. Java, JavaScript, C++, C#, Python, Ruby, and dozens of other languages adopted zero-based indexing because their designers had grown up programming in C or C-influenced languages. The convention propagated through cultural transmission, not through independent derivation. Each new language chose zero-based indexing because the existing programmer population expected it, not because the designers independently concluded it was superior.
This path dependence means that zero-based indexing is, in a meaningful sense, an accident of history. If FORTRAN rather than C had become the dominant systems language, one-based indexing might be the universal standard today. The mathematical arguments for zero-based indexing are real, but they are post-hoc justifications for a convention that was originally driven by PDP-11 hardware addressing, not by theoretical elegance. Both conventions are internally consistent and can be made to work well. The industry chose zero because Dennis Ritchie chose zero, and Dennis Ritchie chose zero because it was natural for the hardware he was targeting.
Why Zero Indexing Creates Real Bugs
The practical cost of zero-based indexing is measured in bugs. Off-by-one errors are so common they have their own name, and they cause real damage in production systems. The Mars Climate Orbiter was lost partly due to a unit conversion error, a cousin of the off-by-one bug. Buffer overflow attacks, which have been the basis of some of the most damaging security exploits in computing history (the Morris Worm, Code Red, Slammer), exploit the boundary between valid and invalid array indices.
Python mitigates some of these issues with negative indexing (array[-1] returns the last element) and slice syntax (array[2:5] returns elements at indices 2, 3, and 4). These features, which are absent in C and Java, reduce the mental overhead of working with zero-based indices. Rust takes a different approach: array access is bounds-checked at runtime, and out-of-bounds access causes a panic rather than undefined behavior. Each language has found its own way to reduce the human cost of zero-based indexing while maintaining the convention.
The database world has its own perspective. SQL uses one-based indexing for its string functions (SUBSTRING starts at position 1, not 0) and for row numbering (ROW_NUMBER() starts at 1). This creates a constant translation layer between application code (zero-based) and database queries (one-based). ORM libraries and data access layers must handle this conversion, adding another source of potential off-by-one errors.
A Convention, Not a Truth
It's worth remembering that zero-indexing is a convention, not a mathematical necessity. Different conventions make different operations easier. The reason zero-indexing dominates is ultimately historical: C won, and C used zero-indexing because it mapped cleanly to the hardware of 1972.
But within that historical accident lies genuine elegance. The interplay between pointer arithmetic, modular operations, and half-open intervals creates a consistent system where zero is the natural origin. It's not the only possible system. But it's a good one.
And the next time someone asks you why programmers count from zero, you can tell them: it's because of a PDP-11 at Bell Labs, a Dutch computer scientist's manuscript, and fifty years of accumulated convention. Simple as that.