Lists in Python
A Python list is a mutable, ordered sequence that can hold values of any type. Unlike strings, you can change elements in place, and methods like .append(), .sort(), and .pop() modify the list directly — but assigning one list to another creates an alias, not a copy.
You already know a string is a sequence of characters you can walk through and slice apart. Now imagine a sequence that can hold anything — numbers, strings, even other lists tucked inside it — and that you can actually change after creating it. That is a list, Python's most flexible and most-used data structure.
This page covers creating and indexing lists, the mutability that sets them apart from strings, the map/filter/reduce patterns, and aliasing — the subtle trap that causes some of the most confusing bugs beginners run into. Pair it with Strings in Python for the sequence operations they share.
What you will learn
By the end you can:
- Create lists and index into them, including nested lists
- Change, add, and delete elements — something strings can never do
- Use
+,*, slicing, and core list methods - Recognize the map, filter, and reduce patterns
- Explain aliasing and why
b = ais not a copy
A list is a sequence — but way more flexible than a string
A list is an ordered collection of values. The individual values are called elements or items, and they can be any type — integers, floats, strings, even other lists. Create one with square brackets, values separated by commas:
[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']A list does not have to hold values of the same type — this is completely valid:
['spam', 2.0, 5, [10, 20]]Notice the last item is itself a list — a nested list. Totally legal, occasionally confusing to read, genuinely useful once you get used to it. An empty list is just two brackets with nothing between them:
empty = []
cheeses = ['Cheddar', 'Edam', 'Gouda']
numbers = [17, 123]
print(cheeses, numbers, empty)
# ['Cheddar', 'Edam', 'Gouda'] [17, 123] []Lists are mutable
This is the big deal about lists: unlike strings, you can change them after creating them — that property is called being mutable. You access elements exactly like strings, with square brackets starting at index 0:
cheeses = ['Cheddar', 'Edam', 'Gouda']
print(cheeses[0]) # CheddarBut here is what strings could never do — put the bracket on the left of = and directly change an element:
numbers = [17, 123]
numbers[1] = 5
print(numbers) # [17, 5]The list has actually been modified — this is called modifying something in place. List indices behave like string indices in every other way: whole-number expressions work as indices, negative indices count from the end, and an out-of-range index raises IndexError. The in operator also works the same way:
cheeses = ['Cheddar', 'Edam', 'Gouda']
'Edam' in cheeses # True
'Brie' in cheeses # FalseTraversing a list
A for loop is the most common way to walk through a list — same syntax as strings:
for cheese in cheeses:
print(cheese)That works great for reading each element. To change elements as you go, you need the index too — combine range() and len():
for i in range(len(numbers)):
numbers[i] = numbers[i] * 2range(len(numbers)) produces indices 0, 1, 2, ... up to the length minus one. On each pass, i lets you both read and write the same spot.
List operators: + and *
Just like strings, + joins and * repeats. The + operator joins two lists into a brand-new one:
a = [1, 2, 3]
b = [4, 5, 6]
c = a + b
print(c) # [1, 2, 3, 4, 5, 6]* repeats an entire list a given number of times:
[0] * 4 # [0, 0, 0, 0]
[1, 2, 3] * 3 # [1, 2, 3, 1, 2, 3, 1, 2, 3][0] * n is a fast way to build a list of n zeros.
List slices
Just like strings, you can pull out a chunk using a slice:
t = ['a', 'b', 'c', 'd', 'e', 'f']
t[1:3] # ['b', 'c']
t[:4] # ['a', 'b', 'c', 'd']
t[3:] # ['d', 'e', 'f']
t[:] # ['a', 'b', 'c', 'd', 'e', 'f'] (a full copy!)Here is something lists can do that strings genuinely cannot — use a slice on the left side of an assignment, replacing several elements at once:
t = ['a', 'b', 'c', 'd', 'e', 'f']
t[1:3] = ['x', 'y']
print(t) # ['a', 'x', 'y', 'd', 'e', 'f']t[:] is worth remembering on its own — it creates a fully independent copy of a list. Since lists are mutable, copying before you modify one is a good habit whenever you want the original left untouched (more on exactly why, below in aliasing).
List methods
.append() adds exactly one new element to the end:
t = ['a', 'b', 'c']
t.append('d')
print(t) # ['a', 'b', 'c', 'd'].extend() takes an entire other list and adds all of its elements onto the end:
t1 = ['a', 'b', 'c']
t2 = ['d', 'e']
t1.extend(t2)
print(t1) # ['a', 'b', 'c', 'd', 'e']
print(t2) # ['d', 'e'] (unchanged).sort() rearranges the elements from low to high:
t = ['d', 'c', 'e', 'b', 'a']
t.sort()
print(t) # ['a', 'b', 'c', 'd', 'e']Map, filter, and reduce
Three patterns come up constantly once you work seriously with lists. Once you recognize each by name, you will spot them everywhere in real code.
Reduce — combining every element into one value
A reduce walks through a list and combines everything into one final result:
def add_all(t):
total = 0
for x in t:
total += x
return totaltotal += x is shorthand for total = total + x. total is called an accumulator — it builds up the answer as the loop runs. This pattern is so common Python has it built in:
t = [1, 2, 3]
sum(t) # 6Map — applying a function to every element
A map transforms every element and builds a new list from the results:
def capitalize_all(t):
res = []
for s in t:
res.append(s.capitalize())
return resres starts empty, and each pass appends a transformed version of the current item.
Filter — keeping only elements that pass a test
A filter selects only the elements that pass some test:
def only_upper(t):
res = []
for s in t:
if s.isupper():
res.append(s)
return res.isupper() returns True only when every letter is uppercase. Almost any list-processing code you write ends up being some combination of map, filter, and reduce — recognizing which one you are using makes your own thinking clearer.
Deleting elements — three ways
.pop() removes by index and hands the value back:
t = ['a', 'b', 'c']
x = t.pop(1)
print(t) # ['a', 'c']
print(x) # 'b'Called with no argument, .pop() removes and returns the last element instead.
del removes by index when you do not need the value — notice it is an operator, not a method (no dot):
t = ['a', 'b', 'c']
del t[1]
print(t) # ['a', 'c']
t = ['a', 'b', 'c', 'd', 'e', 'f']
del t[1:5]
print(t) # ['a', 'f'].remove() removes by value, not index — it finds the first match and deletes it, returning None:
t = ['a', 'b', 'c']
t.remove('b')
print(t) # ['a', 'c']Rule of thumb: use .pop() when you need the removed value back, del when you know the index and do not care about the value, and .remove() when you know the value but not the index.
Converting between lists and strings
A string is a sequence of characters; a list is a sequence of values — close cousins, not the same thing. list() breaks a string into a list of characters:
s = 'spam'
t = list(s)
print(t) # ['s', 'p', 'a', 'm'].split() breaks a string into a list of words at whitespace:
s = 'pining for the fjords'
t = s.split()
print(t) # ['pining', 'for', 'the', 'fjords']
s = 'spam-spam-spam'
s.split('-') # ['spam', 'spam', 'spam'] (custom delimiter).join() reverses .split() — call it on the delimiter, passing the list as the argument:
t = ['pining', 'for', 'the', 'fjords']
delimiter = ' '
delimiter.join(t) # 'pining for the fjords'
''.join(['a', 'b', 'c']) # 'abc'Objects, values, and identity
Consider two variables holding the same string. Do they point to the same object, or two separate ones that happen to match? Python quietly optimizes strings, so they end up pointing to the exact same object:
a = 'banana'
b = 'banana'
a is b # True — literally the same objectLists are different — Python deliberately creates a brand-new object every time:
a = [1, 2, 3]
b = [1, 2, 3]
a is b # False — different objects, same valuesTwo lists with the same elements are equivalent but not identical. == checks equivalence (same value); is checks identity (literally the same object). This distinction matters enormously once you start modifying lists.
Aliasing — the hidden trap
Assigning one list variable to another makes both names point to the same underlying object:
a = [1, 2, 3]
b = a
b is a # TrueThis is called aliasing — one object, two names. Because lists are mutable, changing the list through either name changes what you see through the other:
b[0] = 17
print(a) # [17, 2, 3]You only touched b, but a changed too — because a and b were never two separate lists, just two names for the same one. This is not a bug; it is how Python is designed to work. But it remains one of the most common sources of confusing bugs for beginners and experienced programmers alike. For a genuinely independent copy, use b = a[:].
Strings never run into this — since they are immutable, there is nothing that could get accidentally modified through an alias.
Passing lists to functions
When you pass a list into a function, the function receives a reference to that same list — not a fresh copy. Changes made inside are visible outside once the function returns:
def delete_head(t):
del t[0]
letters = ['a', 'b', 'c']
delete_head(letters)
print(letters) # ['b', 'c']Inside the function, t is simply an alias for letters. But there is a genuine trap: some operations modify a list in place, while others build and return a brand-new list — and mixing these up breaks code silently:
t1 = [1, 2]
t2 = t1.append(3)
print(t1) # [1, 2, 3]
print(t2) # None — append always returns None
t3 = t1 + [4]
print(t3) # [1, 2, 3, 4] — a separate, new listHere is a classic mistake — a function that looks like it removes the first element but does nothing to the caller's list:
def bad_delete_head(t):
t = t[1:] # WRONG — builds a new list, reassigns only the LOCAL tt[1:] builds an entirely new list, and the assignment only redirects the local variable — it never touches the original outside the function. The correct approach, when you want a modified copy and want the original untouched, is to return the new list instead:
def tail(t):
return t[1:]
letters = ['a', 'b', 'c']
rest = tail(letters)
print(rest) # ['b', 'c']
print(letters) # ['a', 'b', 'c'] (untouched)Common mistakes
- Writing
t = t.sort()— void methods returnNone, so this throws the list away - Writing
t.append([x])when you meantt.append(x)— the first nests a list instead of addingx - Writing
t = t + xinstead oft = t + [x]—+needs a list on both sides - Assuming
b = amakes an independent copy — it creates an alias; useb = a[:]for a real copy - Reassigning a function parameter (
t = t[1:]) expecting it to change the caller's list
Why this matters
Lists are everywhere in real Python programs — collecting results, processing data, passing structured information between functions. Understanding mutability and aliasing early prevents the exact class of bug where "my list changed and I have no idea why" — usually because two names were pointing at the same object all along.
Common questions
What is the difference between a list and a string in Python?
A string is an immutable sequence of characters. A list is a mutable, ordered sequence that can hold any type of value, including other lists, and can be changed after creation.
Why does t = t.sort() delete my list in Python?
sort() is a void method — it rearranges the list in place and returns None. Assigning the result back to t replaces your list with None. Just call t.sort() on its own line, without reassigning.
What is aliasing in Python lists?
Aliasing happens when two variables point to the same list object, e.g. b = a. Because lists are mutable, changing the list through either name affects both. Use b = a[:] to make an independent copy instead.
What is the difference between append() and extend() in Python?
append() adds its argument as a single new element to the end of the list. extend() takes another list and adds all of its elements individually onto the end.
How do you remove an item from a list in Python?
Use pop(index) to remove by index and get the value back, del list[index] to remove by index without needing the value, or remove(value) to remove the first matching value.
What are map, filter, and reduce in Python?
Map transforms every element of a list into a new list. Filter keeps only elements that pass a test. Reduce combines every element into a single result, like summing a list.
Explore
Related topics
Keep going — these sit next to this concept in a real learning path.
Strings in Python
The sequence operations lists and strings share — and where they differ.
Data Types in Python
Where list fits among Python's mutable, non-primitive types.
Operators and Operands in Python
How + and * behave differently on lists versus numbers.
The input() Function in Python
Combine input() with .split() to read multiple values at once.
Dictionaries in Python
Key-value pairs, the histogram pattern, and memoization.
Tuples in Python
Immutability, unpacking, *args, zip(), and DSU sorting.
Sets in Python
Uniqueness, fast membership checks, and set math.
Python course hub
All free Python explainers and the path into Agentic practice.
The for Loop in Python
Looping over ranges, strings, and lists with for.