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]gives10 - Tuples:
(1, 2, 3)[2]gives3 - Bytes and bytearray
- Ranges:
range(10)[0]gives0
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
aandbisb - a. - A slice that starts at
aand stops atbhasb - aitems. - The last index is
len(seq) - 1, so the “stop” value for a full slice islen(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 - startitems.” - For negative indexes, translate
-kintolen(seq) - k.
These quick checks tend to reveal the mistake in seconds, not minutes.
References & Sources
- Python Documentation.“Sequence Types — list, tuple, range.”Defines sequence behavior, including indexing and slicing behavior across built-in types.
- Python Documentation.“Tuples And Sequences.”Explains shared properties of sequence types and points to the standard sequence reference.
