Python list indexing and slicing are the two operations you will perform on every list you ever write. Indexing grabs one item; slicing carves out a stretch of items and hands you a brand-new list, with rules you can predict once you know them.
How do you create a Python list?
The literal form is square brackets with comma-separated values inside. [] is the empty list, and it is falsy, which matters as soon as you start checking whether you have any results.
languages = ["Python", "JavaScript", "Rust"]
scores = [92, 87, 95]
mixed = ["Ada", 36, True, None]
empty = []
print(languages)
print(scores)
print(mixed)
print(empty, bool(empty))
There is a second constructor, list(), and it has one famous trap. list("abc") does not give you a list containing the string "abc"; it gives you three separate characters, because the constructor iterates whatever you hand it. If you want the string itself, wrap it in a list: ["abc"].
print(list("abc"))
print(["abc"])
print(list(range(5)))
print(list(range(2, 10, 2)))
list(range(...)) is the bridge between the numeric range object and a real list you can index, slice and append to. Use the range object directly in a loop when you do not need a list, and convert it when you do.
How does list indexing work?
Positions start at zero, which means the first element is at index 0 and the last is at len(list) - 1. Python also counts backwards from the end, so -1 is the last element, -2 the one before it, and so on. That negative indexing is unique to Python and saves a lot of arithmetic.
items = ["first", "second", "third", "fourth"]
print(items[0])
print(items[-1])
print(items[-2])
print(len(items))
try:
print(items[99])
except IndexError as exc:
print("IndexError:", exc)
An out-of-range index raises IndexError: list index out of range rather than returning something empty. That is a good default, because a silent None would let a bug travel far from its cause. If you genuinely want a fallback, reach for dict.get on a mapping or an explicit bounds check on a list.
One more indexing habit matters: negative indices wrap around repeatedly, so items[-99] is still an IndexError rather than silently counting from the end. Python checks the bound properly.
What is a slice, and why is the stop value exclusive?
A slice is written with a colon: items[start:stop]. It takes the elements from index start up to but not including index stop. The stop value is exclusive because the length of the result is then simply stop - start, and because it lets adjacent slices tile perfectly without an off-by-one at the seam.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:5])
print(numbers[:3])
print(numbers[7:])
print(numbers[:])
Omitting a bound means "from the start" or "to the end", and numbers[:] returns a shallow copy of the whole list. The step, when you add a second colon, walks the list rather than cutting it: numbers[::2] takes every second element, numbers[1::2] takes every second element starting from index one.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::3])
print(numbers[1::3])
print(numbers[::-1])
print(numbers[-3:])
A negative step reverses the list. numbers[::-1] is the idiomatic way to reverse a list without touching the original, and it is far more readable than numbers.reverse() when you want a reversed copy rather than a reversed list.
What can you do with a slice?
Three things, and the third is the one that surprises people. First, a slice always returns a new list, so slicing gives you a shallow copy you can modify without touching the original. Second, slices never raise IndexError; a range that runs off the end is simply trimmed. Third, you can assign to a slice, and Python replaces the target range with whatever you give it.
original = [1, 2, 3, 4, 5]
snapshot = original[1:4]
snapshot.append(99)
print("original:", original)
print("snapshot:", snapshot)
beyond = original[10:20]
print("out of range slice:", beyond)
editable = [1, 2, 3, 4, 5]
editable[1:3] = [20, 30]
print("after assignment:", editable)
editable[1:3] = [21, 31, 41]
print("longer replacement:", editable)
Assignment to a slice can change the length of the list: replacing two elements with three makes the list longer, because Python deletes the covered range and inserts what you supplied in its place. That makes slices a compact way to splice data, though in everyday code the list methods read better.
Because a slice copy is shallow, the elements inside it are still the same objects. Slicing a list of lists gives you a new outer list whose inner lists are shared with the original. If you mutate an inner list after slicing, the original changes too. That is the same shallow-copy rule you will meet in the copying lesson later in this module, and it is the reason copy.deepcopy exists.
One slice operation has a length rule worth knowing: assigning to an extended slice, the one with a step, requires the replacement to have exactly the same length as the range it covers. editable[::2] = [0, 0, 0] on a six-element list works; a different count raises ValueError.
How do you check whether a list contains something?
The in operator answers membership directly: "python" in tags evaluates to True or False. It works on any container, and on strings it checks for a substring rather than an exact element.
tags = ["python", "web", "data"]
print("python" in tags)
print("rust" in tags)
print("pyt" in "python")
for index, value in enumerate(tags, start=1):
print(index, value)
enumerate is the standard way to walk a list when you need the position as well as the value. It yields pairs of index and item, and the start argument shifts the numbering when you want it to begin at one instead of zero.
Two related searches are worth knowing as well. list.index(value) returns the position of the first match and raises ValueError if the value is absent, while list.count(value) returns how many times it appears. Use in when you only need a yes-or-no answer; it short-circuits on the first match instead of scanning the whole list.
tags = ["python", "web", "data", "python"]
print(tags.index("web"))
print(tags.count("python"))
try:
tags.index("rust")
except ValueError as exc:
print("ValueError:", exc)
Note that index gives you the first match only. When you need every match, loop over the list and collect the indices, or reach for enumerate together with a filter.
Quick reference
| Expression | Result on [0, 1, 2, 3, 4] |
|---|---|
lst[0] | first element |
lst[-1] | last element |
lst[1:3] | [1, 2] — stop is excluded |
lst[:2] | [0, 1] |
lst[3:] | [3, 4] |
lst[::2] | [0, 2, 4] |
lst[::-1] | [4, 3, 2, 1, 0] |
lst[99:120] | [] — never an error |
lst[99] | raises IndexError |
Try it yourself: make a list of ten file names such as "report-01.txt" through "report-10.txt", then slice it four ways — the first three items, every item whose index is even, the last two, and the whole list reversed. Assign back into the list twice: once replacing a single element with three, and once replacing an extended slice with a same-length list. Print len() after each assignment so you can see which one changed the length, and finish with a membership check for a name that is not in the list.