Python Lists: Create, Slice, Mutate, Iterate
A complete beginner's guide to Python lists — creation, indexing, slicing, mutation, the essential methods, iteration patterns, and a first look at list comprehensions.
What you'll learn
- ✓Every way to create a Python list
- ✓How indexing and slicing work on lists
- ✓The list methods you will use constantly
- ✓How to iterate over lists cleanly
- ✓A first look at list comprehensions
- ✓Why "copying" a list matters and how to do it safely
Prerequisites
- •Familiarity with Python data types — see Data Types
A list is an ordered, mutable collection of values. It is the most-used container type in Python, and once you are comfortable with lists, large parts of the standard library and most third-party data tools become much easier.
Creating a list
The most common way to create a list is with square brackets:
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
empty = []
A list can hold values of any type, including a mix:
mixed = [1, "two", 3.0, True, None]
Mixing types is technically legal but rare in good code — most lists hold values of the same kind.
You can also create a list from any iterable using the list() constructor:
chars = list("hello") # ['h', 'e', 'l', 'l', 'o']
nums = list(range(5)) # [0, 1, 2, 3, 4]
Indexing and length
Lists use zero-based indexing, just like strings, and support negative indices that count from the end:
fruits = ["apple", "banana", "cherry"]
print(fruits[0]) # apple
print(fruits[2]) # cherry
print(fruits[-1]) # cherry
print(len(fruits)) # 3
An index outside the valid range raises IndexError.
Lists are mutable
Unlike strings and tuples, lists can be modified in place. You can reassign individual elements:
fruits = ["apple", "banana", "cherry"]
fruits[1] = "blueberry"
print(fruits) # ['apple', 'blueberry', 'cherry']
This mutability is exactly why lists are the workhorse container — almost any data-processing task involves transforming a list as you go.
Slicing
The slicing syntax you learned for strings works identically on lists:
nums = [10, 20, 30, 40, 50]
print(nums[1:4]) # [20, 30, 40]
print(nums[:3]) # [10, 20, 30]
print(nums[2:]) # [30, 40, 50]
print(nums[::-1]) # [50, 40, 30, 20, 10] — reversed
print(nums[::2]) # [10, 30, 50] — every second element
A slice always returns a new list — the original is unchanged.
Adding and removing items
The everyday mutation methods you’ll use most often:
fruits = ["apple", "banana"]
fruits.append("cherry") # add to the end
print(fruits) # ['apple', 'banana', 'cherry']
fruits.insert(1, "blueberry") # insert at index 1
print(fruits) # ['apple', 'blueberry', 'banana', 'cherry']
fruits.remove("banana") # remove first matching value
print(fruits) # ['apple', 'blueberry', 'cherry']
last = fruits.pop() # remove & return last element
print(last, fruits) # cherry ['apple', 'blueberry']
first = fruits.pop(0) # remove & return element at index 0
print(first, fruits) # apple ['blueberry']
fruits.clear() # remove everything
print(fruits) # []
append and pop together turn a list into a stack. pop(0) removes from the front (less efficient — for queue-like patterns prefer collections.deque).
Concatenation and extension
Two ways to combine lists:
a = [1, 2, 3]
b = [4, 5, 6]
c = a + b # creates a new list
print(c) # [1, 2, 3, 4, 5, 6]
a.extend(b) # adds elements of b to a in place
print(a) # [1, 2, 3, 4, 5, 6]
extend is preferable inside loops because it doesn’t allocate a new list each time.
Searching and counting
nums = [10, 20, 30, 20, 40]
print(nums.index(20)) # 1 — index of first match (raises if absent)
print(nums.count(20)) # 2
print(20 in nums) # True
print(99 in nums) # False
Sorting and reversing
nums = [3, 1, 4, 1, 5, 9, 2, 6]
nums.sort()
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True)
print(nums) # [9, 6, 5, 4, 3, 2, 1, 1]
nums.reverse()
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
sort() modifies the list in place and returns None. If you need a sorted copy without modifying the original, use the built-in sorted():
nums = [3, 1, 2]
new = sorted(nums)
print(nums, new) # [3, 1, 2] [1, 2, 3]
sort() accepts a key argument to sort by something other than the value itself:
words = ["pear", "apricot", "fig"]
words.sort(key=len)
print(words) # ['fig', 'pear', 'apricot']
Iterating over a list
The most direct way to iterate is for ... in:
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
When you need both the index and the value, use enumerate():
for i, fruit in enumerate(fruits):
print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry
To iterate two lists in parallel, use zip():
names = ["Alice", "Bob", "Carol"]
ages = [30, 25, 35]
for name, age in zip(names, ages):
print(f"{name} is {age}")
These three patterns — for, enumerate, and zip — handle the overwhelming majority of list iteration in real code.
Try it yourself. Build a list of the first ten square numbers (1, 4, 9, ..., 100) using a for loop and the append method. Then print each one with its index using enumerate.
List comprehensions
A list comprehension is a compact way to build a list from another iterable. The form is [expression for item in iterable].
# Traditional
squares = []
for n in range(1, 6):
squares.append(n * n)
# Comprehension — exactly the same result
squares = [n * n for n in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]
You can add an if filter:
evens = [n for n in range(10) if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
Comprehensions are a hallmark of idiomatic Python. They are not just shorter — they are also typically faster than the equivalent loop. We will return to them in depth later in the series.
Copying a list
Assigning a list to another variable does not copy it. Both names point to the same underlying object:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] — surprise!
When you actually want a copy, use one of these:
b = a.copy() # method (Python 3.3+)
b = list(a) # constructor
b = a[:] # full slice
All three produce a shallow copy — a new outer list, but the inner objects (if any) are still shared. For nested lists, use copy.deepcopy() from the standard library:
import copy
b = copy.deepcopy(a)
This distinction trips up nearly every beginner. Spend a minute experimenting with it in the REPL until it clicks.
Try it yourself. Create a list of three names. Assign it to a second variable using =, modify the second variable, and observe what happens to the first. Then repeat using .copy() and confirm the original stays intact.
A small worked example
Putting it all together — read a list of scores, compute the average, find the highest, and list the names of everyone who passed.
names = ["Alice", "Bob", "Carol", "Dave", "Eve"]
scores = [82, 47, 91, 56, 73]
# Average
average = sum(scores) / len(scores)
# Highest
top = max(scores)
top_name = names[scores.index(top)]
# Passers (>= 60)
passers = [name for name, score in zip(names, scores) if score >= 60]
print(f"Average: {average:.1f}")
print(f"Top scorer: {top_name} with {top}")
print(f"Passers: {passers}")
Every concept in this post appears in those eight lines. This is the standard texture of real Python.
Recap
You now know:
- Lists are ordered, mutable collections created with
[]orlist() - Indexing and slicing work the same way as on strings
append,insert,remove,pop,extend,clearare the everyday mutation methodssort,sorted, andreversehandle ordering —sortmutates,sortedreturns a new listfor,enumerate, andzipcover virtually all iteration patterns- List comprehensions are the idiomatic way to build a list from another iterable
- Assigning a list does not copy it — use
.copy(),list(...), or[:]
Next steps
The next post is the natural counterpart to this one: tuples — Python’s immutable cousin of the list. We’ll cover when to choose a tuple over a list, the surprising single-element gotcha, and tuple packing and unpacking.
→ Next: Python Tuples and When to Use Them
Questions or feedback? Email codeloomdevv@gmail.com.