append vs extend in Python: the list-growing mixup
Python append vs extend: append adds one item, extend adds each item from an iterable. Extending with a string splits it into characters.

append adds one item to the end of a list. extend adds each item from an iterable. The difference only becomes visible when the thing you are adding is itself a collection:
a = [1, 2]
a.append([3, 4])
# [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4])
# [1, 2, 3, 4]
append treated the list as a single value and nested it. extend unpacked it and added the contents. Both mutated the list in place, and both returned None.
Every result on this page was run in Python 3.13 through Pyodide 0.28.3.
Which one you want
The question to ask is what you are adding.
Adding one thing? append. It does not care what the thing is, and it always increases the length by exactly one.
Adding several things that are already in a collection? extend. The length increases by the number of items in that collection.
lst = [1, 2]
lst.append("hi") # [1, 2, 'hi'] length +1
lst = [1, 2]
lst.extend("hi") # [1, 2, 'h', 'i'] length +2
That second line is the one to remember, and it has its own section below.
The string trap
Strings are iterable, and their items are characters. So extending a list with a string splits it:
c = [1, 2]
c.extend("hi")
# [1, 2, 'h', 'i']
Nothing errored, nothing warned, and the list now has two single-character strings instead of the word you meant to add. If you ever find stray letters in a list, this is almost certainly why.
Adding a whole string is append, because a string is one item:
e = [1, 2]
e.append("hi")
# [1, 2, 'hi']
The same logic applies to any iterable. Extending with a dictionary adds its keys, not its pairs, for the same reason looping a dictionary gives you keys.
What extend accepts
Any iterable, not just lists:
g = [1, 2]
g.extend((5, 6)) # a tuple -> [1, 2, 5, 6]
g.extend({7}) # a set -> [1, 2, 5, 6, 7]
Give it something that is not iterable and it says so plainly:
[1].extend(5)
# TypeError: 'int' object is not iterable
That error is the mirror of the string trap. extend needs a collection; append needs a value. When you get this TypeError, you wanted append.
Both return None
Neither method hands the list back:
result = [1].append(9) # None
result = [1].extend([9]) # None
This is a deliberate Python convention: a method that changes an object in place returns nothing, so you cannot mistake it for one that produces a new object.
It is also the most common way this pair goes wrong:
nums = nums.append(4) # nums is now None
The list was modified correctly, and then the variable was overwritten with None, so everything after this line fails with a NoneType error. The fix is to call the method and not assign:
nums.append(4)
The += shortcut
+= on a list behaves like extend, not like append:
f = [1, 2]
f += [3, 4]
# [1, 2, 3, 4]
This surprises people who expect += to add one item. It also has the same string behaviour, so f += "hi" adds two characters.
Worth knowing that += mutates the list in place, while f = f + [3, 4] builds a new list and rebinds the name. For a list you are the only holder of, the difference does not show. When the list is shared, += changes what everyone sees and + does not.
The nested-list trap that follows from this
Once you know append nests, the obvious way to build a grid is to make the rows first. There is a shortcut for that which is broken in a way you will not see until you write to it:
grid = [[]] * 3
grid[0].append("x")
print(grid)
# [['x'], ['x'], ['x']]
One append, three changes. [[]] * 3 does not create three empty lists. It creates one empty list and puts three references to it in the outer list, so every row is the same object.
The comprehension form creates a new list each time:
grid = [[] for _ in range(3)]
grid[0].append("x")
print(grid)
# [['x'], [], []]
Multiplication is perfectly safe for immutable values, which is why the trap stays hidden:
row = [0] * 3 # [0, 0, 0], entirely fine
Numbers cannot be modified in place, so sharing them cannot be observed. The rule: * n is safe for immutable items and wrong for mutable ones. Use a comprehension whenever the repeated item is a list, dict or set.
Adding one item into the middle
Neither method puts an item anywhere but the end. For that there is insert:
lst = [1, 3]
lst.insert(1, 2) # [1, 2, 3]
The first argument is the position the new item should occupy. insert(0, x) puts it at the front, which is worth knowing is slow on a long list for the same reason pop(0) is: everything after it shifts. Building the list in the right order, or reversing at the end, is usually better than repeated insert(0, ...).
Building lists without either
Both methods have their place, and a loop that only appends is often a comprehension in disguise:
result = []
for n in nums:
result.append(n * 2)
result = [n * 2 for n in nums]
The second is shorter, does not need the empty list, and cannot suffer the None bug above. Reach for append in a loop when the logic is too involved for one line, and for a comprehension when it is not.
For flattening a list of lists, the extend version reads well:
flat = []
for row in rows:
flat.extend(row)
Choosing between them in real code
Three situations where the choice is not obvious from the method names.
Collecting results from a function that returns a list. If parse(line) returns a list of records, append gives you a list of lists and extend gives you a flat list of records. Which you want depends on whether you need to know which line each record came from. That is a real design decision, not a mistake, and the methods let you express either.
Adding a tuple. A tuple is iterable, so extend unpacks it into separate items and append keeps it as one:
points = []
points.append((1, 2)) # [(1, 2)] one coordinate
points.extend((1, 2)) # [1, 2] two numbers
For a list of coordinates you almost always want append. This is the same trap as the string case, and it is easier to fall into because a tuple looks like a single value.
Combining lists you do not own. a.extend(b) modifies a in place, which is wrong if a came from a caller who did not expect it to change. c = a + b builds a new list and leaves both alone. When in doubt about ownership, + is the safer expression even though it allocates.
Three common mistakes
Assigning the result. nums = nums.append(4) sets nums to None. Both methods return nothing; call them and do not assign.
Reaching for extend with a single item. lst.extend(5) raises TypeError: 'int' object is not iterable, and lst.extend("hi") does something worse by succeeding and adding two characters. If you are adding one thing, it is append, whatever that thing is.
Using append in a loop to flatten. This produces a list of lists:
flat = []
for row in rows:
flat.append(row) # wrong, keeps the rows nested
extend is the fix, or a comprehension that reads better than either:
flat = [item for row in rows for item in row]
The double for reads left to right in the order you would say it: for each row, for each item in that row.
Which is faster
For adding many items, extend beats a loop of append calls, because it is one method call that grows the list once rather than one call per item with repeated capacity checks.
lst.extend(other) # preferred
for x in other: lst.append(x) # same result, more work
The difference is small on short lists and real on long ones. More importantly the extend version says what it means in one line.
Where append is unavoidable is when each item needs computing or filtering as you go, and even then a comprehension usually expresses it better. The rule of thumb: if the loop body is a single append of an expression, it wants to be a comprehension.
Quick reference
lst.append(x) # add x as ONE item
lst.extend(xs) # add each item of xs
lst += xs # same as extend
lst.insert(i, x) # add x at position i
lst.append("hi") # ['hi']
lst.extend("hi") # ['h', 'i']
All of them return None and change the list in place.
Want to grow, trim and reshape lists somewhere the errors are immediate? Start with the Python track, or read list.pop() for the method that takes items back out.
More from the blog

How to merge two arrays in JavaScript
To merge arrays in JavaScript, spread and concat both work. push(...arr) throws a RangeError at 200,000 items, which is why the choice matters.
Read more
How to loop through an object in JavaScript
To loop through an object in JavaScript, for...in walks inherited properties too. And integer-like keys come out first, whatever order you wrote.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.