Does Python Index Start At 0? | Zero-Based Indexing Explained

Yes, Python sequences use zero-based indexing, so the first item sits at position 0 and the last item sits at len(sequence) – 1.

If you’ve ever typed my_list[1] expecting the first item, you’ve met Python’s indexing rule the hard way. Python starts counting positions at 0 for the built-in sequence types you use every day: strings, lists, tuples, ranges, and many more. Once you lock that in, a lot of “weird” behavior turns into clean, predictable patterns.

This article shows what “index 0” means in real code, how indexing differs from slicing, how negative indexes work, and where beginners usually trip up. You’ll also get a practical mental model for reading code that uses offsets, loops, and slices.

What An Index Means In Python

An index is a position number you use to fetch a single item from a sequence. In Python, indexing uses square brackets: seq[i]. If i is 0, you’re asking for the first item. If i is 1, you’re asking for the second item.

That “position” is an offset from the start. Offset 0 means “zero steps from the start.” Offset 3 means “three steps from the start.” This offset view is the simplest way to stay calm when you read unfamiliar code.

Zero-Based Indexing In Plain English

Think of a sequence as a row of boxes. The first box has offset 0 because you don’t move at all to reach it. The next box is one move away, so it has offset 1. Keep going and you get 0, 1, 2, 3… all the way up to the final offset.

That final offset is always one less than the count of items. If a list has 5 items, the last index is 4. This “count vs. last index” mismatch causes most off-by-one bugs.

Which Things Use Indexing

Any object that behaves like a sequence or implements item access can be indexed. Common ones include:

  • Strings: "Python"[0] gives "P"
  • Lists: [10, 20, 30][0] gives 10
  • Tuples: (1, 2, 3)[2] gives 3
  • Bytes and bytearray
  • Ranges: range(10)[0] gives 0

Dictionaries also use [], but that’s mapping lookup, not positional indexing.

Python Indexing From Zero: Rules And Exceptions

For the built-in sequence types, the first valid index is 0, and the last valid index is len(seq) - 1. That pattern is consistent across strings, lists, tuples, and ranges. The core docs describe sequences and their shared behavior in the standard library reference. Sequence Types — list, tuple, range is a solid place to cross-check details.

There are a few edge cases that look like “exceptions,” yet they still follow the same logic:

  • Empty sequences: they have no valid index at all, so any seq[0] raises an error.
  • Custom classes: they can define their own meaning for [], yet most follow the usual convention to match reader expectations.
  • Some libraries: a library might offer 1-based labels or IDs, but that’s a layer on top of Python, not the language rule.

Why Python Uses 0-Based Indexing

One reason: 0-based indexing matches the idea of an offset. If you treat an index as “how far from the start,” 0 is the only sensible start value.

Another reason is history. Many earlier languages and systems treated positions as offsets into contiguous storage. Python kept that convention, then built a consistent sequence model around it. You don’t need to write low-level code to benefit from the mental model, though. Just keep thinking “offset,” not “rank.”

Offsets Make Slices And Length Math Cleaner

When indexes represent offsets, many common calculations stay simple:

  • The distance between two positions a and b is b - a.
  • A slice that starts at a and stops at b has b - a items.
  • The last index is len(seq) - 1, so the “stop” value for a full slice is len(seq).

These patterns show up all over Python code, from simple list work to text parsing and binary data handling.

Indexing Versus Slicing

Indexing returns one item. Slicing returns a new sequence (or a view-like object, depending on the type) that contains a run of items. Slicing uses the form seq[start:stop:step].

Here’s the detail that surprises people: the stop value is excluded. So seq[0:3] returns items at indexes 0, 1, and 2.

Reading Slices Without Guessing

To read a slice, translate it into a sentence: “Start at start, keep taking items, stop right before stop.” If start is missing, it defaults to 0. If stop is missing, it defaults to the length of the sequence.

The official tutorial section on data structures links out to sequence behavior and is a helpful refresher when you want canonical wording. Tuples And Sequences covers shared sequence ideas in context.

Negative Indexes: Counting From The End

Negative indexes let you count backward from the end. seq[-1] is the last item, seq[-2] is the second-to-last, and so on. This works because Python maps a negative index -k to len(seq) - k.

One subtle point: -0 equals 0, so there’s no “negative zero” index. That’s why negative indexing starts at -1.

Common Index Mistakes And How To Fix Them

Most indexing bugs aren’t about memorizing rules. They come from mixing up “item number” with “offset,” or mixing up included and excluded bounds. These fixes keep you out of trouble.

Mixing Up Position And Count

  • Bug: “The list has 5 items, so the last index is 5.”
  • Fix: Last index is len(seq) - 1, so 4 in a 5-item list.

Using The Stop Index Like It’s Included

  • Bug: Expecting seq[0:3] to include index 3.
  • Fix: Stop is excluded. Use seq[0:4] if you want 0 through 3.

Indexing Past The End

Indexing a missing position raises IndexError. Slicing is more forgiving: seq[0:999] just stops at the end. This difference is handy when you want “give me up to N items” without extra checks.

Looping With The Wrong Range

A classic bug is writing range(len(seq) + 1) and then indexing with that value. The last iteration tries seq[len(seq)], which is always out of bounds. If you need indexes, use range(len(seq)). If you need items, loop over the items directly.

Indexing Patterns You’ll See In Real Code

Once you know indexes start at 0, you can read common patterns quickly. These show up in scripts, web apps, data work, and automation.

Use enumerate When You Need Both Index And Value

enumerate(seq) yields pairs of (index, value), starting at 0 by default. You can pass a different start value if you want human-friendly numbering for output, like line numbers in a report.

  • Default: indexes 0, 1, 2…
  • Human numbering:enumerate(lines, start=1) prints 1-based line numbers, while the underlying sequence rule stays the same.

Use Slices For Chunks

Chunking a list is often done with slices that step forward by a fixed amount. The excluded stop makes chunk sizes predictable when the data length isn’t a perfect multiple of the chunk size.

Use -1 For “Last Item” Only When You Know It Exists

seq[-1] is clean, yet it still fails on an empty sequence. If emptiness is possible, guard first with a simple truthy check like if seq:.

Table Of Indexing And Slicing Behavior Across Types

The same indexing ideas appear across different built-in types, yet the returned values differ. This table helps you predict what you get back.

Type Indexing With [0] Slicing With [1:4]
list First element New list of elements 1–3
tuple First element New tuple of elements 1–3
str First character (1-length string) Substring of characters 1–3
bytes Integer 0–255 New bytes object
bytearray Integer 0–255 New bytearray
range First generated number New range object
array (array module) First element New array
memoryview One element view or integer, by format New memoryview slice

How To Think About Indexes When You Write Your Own Code

The best way to avoid indexing confusion is to write code that makes the intent obvious. A few habits help a lot.

Name Variables After What They Hold

Use names like start, stop, i, last_index, or count in a way that matches the math. If a variable is a count, call it count. If it is a last index, call it last_index.

Prefer Direct Iteration When You Don’t Need Indexes

Instead of looping over range(len(seq)), loop over the values. It’s shorter, easier to read, and it avoids boundary mistakes.

Convert Between Human Numbering And Python Indexes Deliberately

User interfaces, spreadsheets, and some specs number items starting at 1. When you accept a number from a user, convert it once at the boundary:

  • Human “item 1” becomes Python index 0
  • Human “item n” becomes Python index n - 1

Doing this conversion in one place keeps the rest of your code consistent.

Table Of Off-By-One Traps And Quick Fixes

These are the small mistakes that steal time during debugging. The fixes are simple when you spot the pattern.

Situation What Goes Wrong Safer Pattern
Looping over indexes Using range(len(seq)+1) Use range(len(seq)) or iterate values
Included upper bound Expecting stop index included Use stop = last_index + 1
Last item access Using seq[len(seq)] Use seq[-1] after emptiness check
Dropping last element Using seq[:len(seq)-1] by habit Use seq[:-1] when non-empty
Chunk sizes Miscounting items per slice Use seq[i:i+n] for n items
Human input Using 1-based input directly Convert once: idx = user_n - 1
Reversed loops Off-by-one in reverse ranges Use range(len(seq)-1, -1, -1)

Quick Self-Checks When Something Feels Off

If your output looks shifted by one position, run these checks:

  • Print len(seq) and the index you’re using side by side.
  • Confirm whether a value is a count or a last index.
  • For slices, rewrite it as “take stop - start items.”
  • For negative indexes, translate -k into len(seq) - k.

These quick checks tend to reveal the mistake in seconds, not minutes.

References & Sources