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:
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:
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:
fruits = ["apple", "banana", "cherry"]
print(fruits[0])
print(fruits[2])apple
cherryPython 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:
fruits = ["apple", "banana", "cherry"]
print(fruits[-1])
print(fruits[-2])cherry
bananaBeyond 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:
numbers = [10, 20, 30, 40, 50]
print(numbers[1:3])
print(numbers[:3])
print(numbers[3:])
print(numbers[-2:])[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:
numbers = [10, 20, 30, 40, 50]
print(numbers[::-1])[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.
tasks = ["wash dishes", "walk dog"]
tasks.append("buy milk")
print(tasks)['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:
tasks = ["wash dishes", "walk dog", "buy milk"]
tasks.insert(1, "call mom")
print(tasks)['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:
tasks = ["wash dishes", "call mom", "walk dog", "buy milk"]
tasks.remove("walk dog")
print(tasks)['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:
tasks = ["wash dishes", "call mom", "buy milk"]
last_task = tasks.pop()
print(last_task)
print(tasks)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:
original_ids = [101, 102, 103]
backup = original_ids
backup.append(104)
print(original_ids)
print(original_ids is backup)[101, 102, 103, 104]
TrueWriting 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:
original_ids2 = [101, 102, 103]
backup2 = original_ids2[:]
backup2.append(104)
print(original_ids2)
print(original_ids2 is backup2)[101, 102, 103]
FalseCommon 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:
nums = [5, 3, 1, 4, 2]
print(len(nums))5Sorting 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:
nums = [5, 3, 1, 4, 2]
nums.sort()
print(nums)
nums.sort(reverse=True)
print(nums)[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:
other = [9, 1, 5]
result = sorted(other)
print(other)
print(result)[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:
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)apple
banana
cherryFinally, the in operator tests whether a value exists anywhere in a list, and reads almost like plain
English:
fruits = ["apple", "banana", "cherry"]
print("apple" in fruits)
print("mango" in fruits)True
FalseA 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.
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)Largest: 89, second largest: 63
Was 100 among the scores? FalseHere'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:
list_a = [1, 2, 3]
list_b = list_a
list_b.append(4)
print(list_a)[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[:]):
list_c = [1, 2, 3]
list_d = list_c.copy()
list_d.append(4)
print(list_c)
print(list_d)[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:
colors = ["red", "green", "blue"]
print(colors[len(colors)])IndexError: list index out of rangeThe 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:
colors = ["red", "green", "blue"]
print(colors[-1])blueRemoving 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:
values = [1, 2, 4, 5, 7]
for v in values:
if v % 2 == 0:
values.remove(v)
print(values)[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:
values2 = [1, 2, 4, 5, 7]
for v in values2[:]:
if v % 2 == 0:
values2.remove(v)
print(values2)[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:
nums = [10, 20, 30]
nums.remove(1)ValueError: list.remove(x): x not in listnums.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.