RizTech Academy logo
RizTech Academy
Data StructuresLesson 1 of 825 min

Lists: creating, indexing, slicing

So far every variable has held one thing. Real programs handle many things at once — a shopping basket, a month of temperatures, every line in a file. A list holds them in order, and it is the structure you will reach for most often.

This lesson also settles something promised back in the variables lesson: why a variable is a label on a value rather than a box. With lists, the difference stops being philosophical and starts causing bugs.

Making a list

Square brackets, items separated by commas:

fruits = ["apple", "banana", "mango"]
temperatures = [31, 28, 35, 33]
mixed = ["apple", 42, True, 3.14]
empty = []

A list can hold anything, including different types at once. In practice, a list whose items are all the same kind of thing is far easier to work with — if you find yourself storing a name, an age and a city in one list, you want a dictionary, which is two lessons away.

len() gives the count:

print(len(fruits))    # 3
print(len(empty))     # 0

Indexing

Positions start at zero, exactly like strings:

fruits = ["apple", "banana", "mango"]

print(fruits[0])     # apple
print(fruits[1])     # banana
print(fruits[-1])    # mango   — last
print(fruits[-2])    # banana  — second from last

Ask for a position that does not exist and Python stops:

print(fruits[3])
IndexError: list index out of range

Three items means valid positions 0, 1, 2. The last index is always len(list) - 1, which is the single most common off-by-one in programming. When you want the last item, use [-1] rather than calculating it.

Slicing

Same rules as strings — [start:end], end excluded:

numbers = [10, 20, 30, 40, 50]

print(numbers[1:3])     # [20, 30]
print(numbers[:3])      # [10, 20, 30]
print(numbers[2:])      # [30, 40, 50]
print(numbers[::2])     # [10, 30, 50]   — every second item
print(numbers[::-1])    # [50, 40, 30, 20, 10] — reversed

A slice always gives you a new list. That matters shortly.

Unlike indexing, slicing never raises IndexError:

print(numbers[1:100])   # [20, 30, 40, 50]
print(numbers[10:20])   # []

It quietly gives you what it can. Convenient, and occasionally it hides a bug — an empty list where you expected data may mean your indexes are wrong rather than your data being empty.

Lists can be changed

This is the fundamental difference from strings. Strings are immutable; lists are not.

fruits = ["apple", "banana", "mango"]
fruits[1] = "orange"
print(fruits)
['apple', 'orange', 'mango']

The same operation on a string raises TypeError. A list can be modified in place, and everything in this lesson follows from that.

Adding and removing

fruits = ["apple", "banana"]

fruits.append("mango")           # add one to the end
fruits.insert(1, "kiwi")         # add at a position
print(fruits)
['apple', 'kiwi', 'banana', 'mango']
fruits.remove("kiwi")            # remove by value (the first match)
last = fruits.pop()              # remove and return the last
first = fruits.pop(0)            # remove and return by position
del fruits[0]                    # remove by position, returns nothing

remove() raises ValueError if the value is not there. pop() raises IndexError on an empty list. Neither is unreasonable, but both are worth knowing before they surprise you.

The label, not the box

Here is the part that matters.

a = [1, 2, 3]
b = a

b.append(4)

print(a)    # [1, 2, 3, 4]
print(b)    # [1, 2, 3, 4]

You appended to b and a changed too.

b = a did not copy anything. It stuck a second label on the same list. There is one list in memory with two names, and modifying it through either name changes the thing both names point at.

The is operator from module 3 proves it:

print(a is b)    # True — the same object

This is not a flaw. Copying large lists on every assignment would be slow and usually unwanted. But it is the source of a genuinely nasty class of bug, because the effect shows up far from the cause — you change a list in one part of a program and something apparently unrelated breaks.

Making an actual copy

Three ways, all equivalent for a flat list:

b = a.copy()
b = a[:]
b = list(a)
a = [1, 2, 3]
b = a.copy()
b.append(4)

print(a)         # [1, 2, 3]  — untouched
print(a is b)    # False

Use .copy(). It says what it means.

The rule of thumb: if you are about to modify a list that was given to you from somewhere else, copy it first — unless changing the original is genuinely what you intend.

A copy is only one level deep

grid = [[1, 2], [3, 4]]
copied = grid.copy()

copied[0].append(99)
print(grid)
[[1, 2, 99], [3, 4]]

The outer list was copied; the inner lists were not. copied is a new list containing labels pointing at the same two inner lists.

For nested structures you need a deep copy:

import copy
truly_separate = copy.deepcopy(grid)

You will not need this often. When you do, nothing else works.

Never modify a list while looping over it

Raised in the loops lesson, and now you can see why:

numbers = [1, 2, 3, 4, 5, 6]

for number in numbers:
    if number % 2 == 0:
        numbers.remove(number)

print(numbers)
[1, 3, 5]

That happens to be right, by luck. The loop tracks a position while the list shrinks under it, so items shift left and get skipped. Change the data and it visibly breaks.

Build a new list instead:

numbers = [1, 2, 3, 4, 5, 6]
odds = []

for number in numbers:
    if number % 2 != 0:
        odds.append(number)

Or, if the name must stay the same, loop over a copy:

for number in numbers.copy():
    ...

Useful operations

numbers = [3, 1, 4, 1, 5]

print(len(numbers))          # 5
print(sum(numbers))          # 14
print(max(numbers))          # 5
print(min(numbers))          # 1
print(3 in numbers)          # True
print(numbers.count(1))      # 2
print(numbers.index(4))      # 2  — position of the first 4
print(sorted(numbers))       # [1, 1, 3, 4, 5] — a NEW sorted list
print(numbers)               # [3, 1, 4, 1, 5] — unchanged

Note sorted() returns a new list while .sort() changes the list in place and returns None — the trap from the booleans lesson.

Joining and splitting bridge lists and strings:

parts = "a,b,c".split(",")        # ['a', 'b', 'c']
joined = ", ".join(parts)         # 'a, b, c'

Practice

  1. Build a list of five cities. Print the first, the last, and the middle three using a slice.
  2. Trigger an IndexError deliberately, read it, then fix it using [-1].
  3. Start with [5, 3, 8, 1]. Append 10, insert 0 at the start, remove 8, and pop the last item into a variable. Print the list after each step.
  4. Set a = [1, 2, 3] and b = a. Append to b. Print both and explain, out loud, why a changed. Then redo it with .copy().
  5. Write a function-free "remove duplicates": loop over a list with repeats and build a new list containing each value once.
  6. Take grid = [[1, 2], [3, 4]], do copied = grid.copy(), modify an inner list, and confirm both changed. Then fix it with copy.deepcopy.
  7. Given prices = [120, 450, 89, 230], find the total, average, highest and lowest — first with a loop, then with sum(), max() and min(). Notice how much shorter the second version is.

Next: the list methods worth committing to memory, and the ones that will catch you out.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship