Skip to content

Python lesson 7 of 9

Python Lists

Learn how Python lists store ordered, changeable collections of values, and how to create, access, modify, and iterate over them.

Published · Every example on this page was run before it was published.

So far you've stored one value per variable — a single number, a single string, a single boolean. Most real problems don't work that way. A gradebook holds dozens of scores. A shopping app holds a cart full of items. A weather station holds a reading for every hour of the day. You need a single variable that can hold many values at once, in a specific order, and Python's answer to that need is the list.

What is a List?

A list is an ordered collection of values stored under one variable name. "Ordered" means every value has a fixed position, and Python never rearranges that position on its own. "Collection" means the list can hold as many values as you want — zero, one, or thousands — and those values can be numbers, strings, booleans, or even other lists.

A good mental picture is a row of numbered lockers in a school hallway. Each locker has a position — locker 0, locker 1, locker 2, and so on — and you can look inside any locker just by knowing its number. You can also swap out what's inside a locker, add a new locker to the end of the row, or clear one out entirely, all without touching the other lockers. A Python list works the same way: each value sits at a numbered position (called an index), and you can read, change, add, or remove values by referring to that position.

Python writes a list using square brackets, with each value separated by a comma:

Python
scores = [88, 95, 72, 100, 61]

Here scores is one variable holding five values, in that exact order. This is different from writing five separate variables (score1, score2, and so on) because the list itself remembers how many values it holds and lets you work with all of them together — looping over every score, finding the highest one, or adding a new score as it comes in. Many coding problems — including the practice problem linked at the end of this lesson — hand you input as a list (sometimes called an array in other languages) and expect you to search through it, transform it, or combine values from it.

Creating and Accessing Lists

You create a list by writing values inside square brackets. An empty list is just an empty pair of brackets, and a list doesn't have to hold only one type of value:

Python
fruits = ["apple", "banana", "cherry"]
empty_list = []
mixed = [1, "two", 3.0, True]

Python numbers list positions starting at 0, not 1. So in fruits, "apple" sits at index 0, "banana" at index 1, and "cherry" at index 2. You read a value at a given position using square brackets after the variable name:

Python
fruits = ["apple", "banana", "cherry"]
print(fruits[0])
print(fruits[2])
Output
apple
cherry

Python also supports negative indexing, which counts backward from the end of the list: -1 is the last item, -2 is the second-to-last, and so on. This is convenient when you want the end of a list but don't want to compute its length yourself:

Python
fruits = ["apple", "banana", "cherry"]
print(fruits[-1])
print(fruits[-2])
Output
cherry
banana

Beyond single positions, you can pull out a whole range of values at once using a slice, written as list[start:stop]. The slice includes the index at start but stops before the index at stop — the value at stop itself is never included:

Python
numbers = [10, 20, 30, 40, 50]
print(numbers[1:3])
print(numbers[:3])
print(numbers[3:])
print(numbers[-2:])
Output
[20, 30]
[10, 20, 30]
[40, 50]
[40, 50]

Leaving out start means "from the beginning," and leaving out stop means "through the end." A slice also accepts a third number for the step size — list[::-1], for example, walks through the list backward one item at a time, which is a common shortcut for reversing a list:

Python
numbers = [10, 20, 30, 40, 50]
print(numbers[::-1])
Output
[50, 40, 30, 20, 10]

A slice always produces a new list, separate from the original — this matters later when you need an independent copy of a list rather than another name for the same one.

Modifying Lists

Unlike a string or a number, a list is mutable — you can change what it holds after it's created, without having to build a new list from scratch. Four methods handle most of the changes you'll make: append adds a value to the end, insert adds a value at a specific position, remove deletes the first matching value, and pop removes and returns a value by position.

Python
tasks = ["wash dishes", "walk dog"]
tasks.append("buy milk")
print(tasks)
Output
['wash dishes', 'walk dog', 'buy milk']

insert takes two arguments — the index to insert at, and the value to insert — and shifts everything from that position onward one slot to the right:

Python
tasks = ["wash dishes", "walk dog", "buy milk"]
tasks.insert(1, "call mom")
print(tasks)
Output
['wash dishes', 'call mom', 'walk dog', 'buy milk']

remove searches the list for the value you give it and deletes the first one it finds. It deletes by value, not by position:

Python
tasks = ["wash dishes", "call mom", "walk dog", "buy milk"]
tasks.remove("walk dog")
print(tasks)
Output
['wash dishes', 'call mom', 'buy milk']

pop is the opposite: it deletes by position and hands you back the value that was removed, which is useful when you need to both take an item out of a list and do something with it. Called with no argument, pop removes the last item:

Python
tasks = ["wash dishes", "call mom", "buy milk"]
last_task = tasks.pop()
print(last_task)
print(tasks)
Output
buy milk
['wash dishes', 'call mom']

Mutation vs. reassignment

Because lists are mutable, two variables can end up pointing at the same list in memory. When that happens, changing the list through one variable name also changes what the other variable sees — there is only ever one list, just two labels for it:

Python
original_ids = [101, 102, 103]
backup = original_ids
backup.append(104)
print(original_ids)
print(original_ids is backup)
Output
[101, 102, 103, 104]
True

Writing backup = original_ids did not create a second list — it made backup a second name for the exact same list original_ids already pointed to. Calling backup.append(104) mutated that one shared list, so the change shows up no matter which name you use to look at it. The is operator confirms this: it checks whether two variables refer to the same object, and here it's True.

If you actually want an independent copy — one where changes to the copy don't touch the original — slice the whole list with [:] (or call its .copy() method), which builds a brand-new list instead of reusing the old one:

Python
original_ids2 = [101, 102, 103]
backup2 = original_ids2[:]
backup2.append(104)
print(original_ids2)
print(original_ids2 is backup2)
Output
[101, 102, 103]
False

Common List Operations

A handful of built-in tools cover most of what you'll do with a list day to day: measuring it, ordering it, walking through it, and checking whether it contains something.

len() returns how many items a list holds — you'll use it constantly, from guarding against empty lists to writing loops that stop at the right place:

Python
nums = [5, 3, 1, 4, 2]
print(len(nums))
Output
5

Sorting a list rearranges its values from smallest to largest (or largest to smallest). The list's own .sort() method sorts it in place, meaning it changes the original list and doesn't return a new one:

Python
nums = [5, 3, 1, 4, 2]
nums.sort()
print(nums)
nums.sort(reverse=True)
print(nums)
Output
[1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]

If you want a sorted version without disturbing the original list, use the built-in sorted() function instead. It leaves its input untouched and gives you back a new, separate sorted list:

Python
other = [9, 1, 5]
result = sorted(other)
print(other)
print(result)
Output
[9, 1, 5]
[1, 5, 9]

Walking through every item in a list is done with a for loop, the same kind you've already used with range() — except now Python hands you each value directly instead of each index:

Python
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)
Output
apple
banana
cherry

Finally, the in operator tests whether a value exists anywhere in a list, and reads almost like plain English:

Python
fruits = ["apple", "banana", "cherry"]
print("apple" in fruits)
print("mango" in fruits)
Output
True
False

A Worked Example

Let's put several of these pieces together to solve a small, concrete task: given a list of numbers, find the largest value and the second-largest value.

Python
def two_largest(numbers):
    if len(numbers) < 2:
        raise ValueError("Need at least two numbers")

    if numbers[0] > numbers[1]:
        largest, second = numbers[0], numbers[1]
    else:
        largest, second = numbers[1], numbers[0]

    for value in numbers[2:]:
        if value > largest:
            largest, second = value, largest
        elif value > second:
            second = value

    return largest, second


scores = [42, 17, 89, 5, 63, 23]
top, runner_up = two_largest(scores)
print(f"Largest: {top}, second largest: {runner_up}")
print("Was 100 among the scores?", 100 in scores)
Output
Largest: 89, second largest: 63
Was 100 among the scores? False

Here's what each part is doing. len(numbers) < 2 uses len() as a guard clause — if the list has fewer than two values, there's no "second largest" to find, so the function refuses to continue rather than silently producing a wrong answer. The next block looks only at numbers[0] and numbers[1] (plain indexing) to set up an initial guess for largest and second, putting the bigger of the two first. From there, numbers[2:] is a slice that produces every remaining value — everything from index 2 onward — so the for loop only has to examine values it hasn't already accounted for.

Inside the loop, each new value is compared against the current largest. If it beats largest, it becomes the new leader and the old leader slides down into second — that's what largest, second = value, largest does in one step. If it doesn't beat largest but still beats second, it only replaces second. Any value that beats neither is simply ignored, and the loop moves on. By the time the loop finishes, largest and second hold the top two values from the entire list, even though only numbers[0] and numbers[1] were ever looked at directly by name — everything else was reached through the loop and the slice. The last line demonstrates in: checking whether 100 turned up anywhere in scores without needing to write a search loop yourself.

Common Mistakes

Assuming = makes a copy of a list. Because lists are mutable, assigning one list variable to another doesn't duplicate the list — it just gives the same list a second name. Changes made through either name affect both:

Python
list_a = [1, 2, 3]
list_b = list_a
list_b.append(4)
print(list_a)
Output
[1, 2, 3, 4]

list_a changed even though the code only ever appended to list_b, because they were never two separate lists to begin with. To get an independent copy, use .copy() or a full slice (list_a[:]):

Python
list_c = [1, 2, 3]
list_d = list_c.copy()
list_d.append(4)
print(list_c)
print(list_d)
Output
[1, 2, 3]
[1, 2, 3, 4]

Indexing one position past the end of the list. A list of length n has valid indices 0 through n - 1 — the index equal to the length itself doesn't exist, since counting starts at 0:

Python
colors = ["red", "green", "blue"]
print(colors[len(colors)])
Output
IndexError: list index out of range

The fix is to use len(colors) - 1 for the last position, or better, use colors[-1], which always means "the last item" regardless of how long the list is and can't be off by one:

Python
colors = ["red", "green", "blue"]
print(colors[-1])
Output
blue

Removing items from a list while looping over it. A for loop tracks its position by index as it goes. When you remove an item mid-loop, every later item shifts one position to the left to fill the gap — but the loop's position counter still advances by one, so it ends up skipping the item that just shifted into the spot it already passed:

Python
values = [1, 2, 4, 5, 7]
for v in values:
    if v % 2 == 0:
        values.remove(v)
print(values)
Output
[1, 4, 5, 7]

The 4 should have been removed along with the 2, but it survives — after 2 is removed, 4 slides into the position the loop had just checked, and the loop moves past it without ever looking at it again. The fix is to loop over a separate copy of the list (a full slice works well here) while removing from the original, so the list you're modifying is never the same list you're stepping through:

Python
values2 = [1, 2, 4, 5, 7]
for v in values2[:]:
    if v % 2 == 0:
        values2.remove(v)
print(values2)
Output
[1, 5, 7]

Confusing remove() with pop(). remove() takes a value and deletes the first item that matches it; pop() takes an index and deletes whatever sits at that position. Passing an index to remove() by mistake searches for that number as a value instead:

Python
nums = [10, 20, 30]
nums.remove(1)
Output
ValueError: list.remove(x): x not in list

nums.remove(1) fails because the value 1 isn't in the list — [10, 20, 30] has no 1 in it, even though 1 looks like it could mean "index 1." To delete the item at position 1 (the 20), use nums.pop(1) instead.

Next Steps

Lists are the foundation for a huge share of coding problems, because so many problems boil down to "do something with a sequence of values." The Two Number Sum practice problem is a direct next step from here — it hands you a list of integers and asks you to find the pair that adds up to a target, which means indexing, looping, and membership checks all come into play. Once you've read through this lesson, head to the Python playground to experiment with the examples above on your own list values before attempting the problem.

Write it yourself

Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.

When you want a harder one

Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.