append vs extend vs +=.
One item. Or many.
Three ways to add to a list. They look similar. They do very different things. append adds one item — even if that item is a list, it becomes a nested element. extend adds each item of an iterable individually. += is equivalent to extend — it's the same operation with different syntax. This page covers the difference, the string trap, the None return, and when each is the right tool.
Short answer: append(x) adds x as one element. extend(iterable) adds each item of iterable. += iterable is the same as extend. All three mutate in place and return None. All three are efficient for building lists. Use append for one item, extend or += for a collection. Watch out for extend('abc') — it adds each character, not the string.
✅ The three in action
Same list, three operations.
nums = [1, 2, 3]
# append — add ONE item
nums.append(4)
print(nums) # [1, 2, 3, 4]
# append a list — adds the list as ONE element
nums = [1, 2, 3]
nums.append([4, 5])
print(nums) # [1, 2, 3, [4, 5]]
# extend — add each item of an iterable
nums = [1, 2, 3]
nums.extend([4, 5])
print(nums) # [1, 2, 3, 4, 5]
# extend with a tuple
nums = [1, 2, 3]
nums.extend((4, 5))
print(nums) # [1, 2, 3, 4, 5]
# extend with a string — adds each character
nums = [1, 2, 3]
nums.extend('abc')
print(nums) # [1, 2, 3, 'a', 'b', 'c']
# += is the same as extend
nums = [1, 2, 3]
nums += [4, 5]
print(nums) # [1, 2, 3, 4, 5]
# None return
nums = [1]
result = nums.append(2)
print(result) # None
print(nums) # [1, 2]
# vs + — creates a NEW list
nums = [1, 2, 3]
new_nums = nums + [4]
print(nums) # [1, 2, 3] — unchanged
print(new_nums) # [1, 2, 3, 4]
The difference:
[1, 2, 3].append([4, 5])
→ [1, 2, 3, [4, 5]]
→ 4 items: 1, 2, 3, and the list [4, 5]
→ len is 4
[1, 2, 3].extend([4, 5])
→ [1, 2, 3, 4, 5]
→ 5 items: 1, 2, 3, 4, 5
→ len is 5
The key insight:
append adds ONE thing — even if that thing is
a list, it's one thing.
extend adds each thing — if you give it a list,
each item in the list is added individually.
The mental model:
append is like dropping a box into a shelf.
The box might contain many items, but it's
one box on the shelf.
extend is like emptying a box onto the shelf.
Each item from the box goes onto the shelf
individually.
Where the confusion comes from:
In JavaScript, array.push(arr) appends the
array as one element (like Python's append).
In Python, some people expect extend-like
behavior from append because they're used
to "adding items."
The naming is precise:
append → "add to the end" (as one element)
extend → "extend the list with each item"
When to use each:
✅ append — when adding ONE item:
- Adding a single value: nums.append(4)
- Adding a computed value: results.append(compute())
- Building a list one by one in a loop:
for x in data: results.append(transform(x))
- When the item is a container (list, dict,
tuple) and you want it as one element:
rows.append(row) # row is one row
✅ extend — when adding MANY items:
- Concatenating two lists: list1.extend(list2)
- Adding a range: list.extend(range(5))
- Adding a sequence of items: list.extend(items)
- Adding characters: chars.extend("hello")
✅ += — same as extend, different syntax:
- list1 += list2
- Readability: `+=` is shorter, `extend` is
clearer about intent
- Performance: identical (both in-place)
- Prefer extend for clarity, += for brevity
❌ + — creates a NEW list (not the same thing):
- new_list = list1 + list2 # new list
- list1 + list2 doesn't modify list1
- Use for: getting a new list from two
- Don't use for: building a list in a loop
(creates N intermediate lists, O(n²))
The common bug:
# Expected: add each item
results = []
for x in data:
results.append(x * 2) # ✅ correct
# Wrong:
results = []
for x in data:
results += [x * 2] # ✅ also works
# But:
results = []
for x in data:
results = results + [x * 2] # ❌ O(n²)!
The last one creates a new list every iteration.
For 10,000 items, that's 50 million operations.
For 10 items, it's fine. For 10,000, it's slow.
Fix: use append or +=.
append is O(1) amortized (list over-allocates).
+= is also O(1) amortized (calls extend).
+ is O(n) per iteration (creates new list).
✅ The None return
Why append returns None.
# ❌ The classic bug — expecting a return value nums = [1, 2, 3] result = nums.append(4) print(result) # None — not [1, 2, 3, 4]! # Why: append mutates in place and returns None. # This is by design — Python's convention. # ✅ Correct — call append separately nums = [1, 2, 3] nums.append(4) print(nums) # [1, 2, 3, 4] # ✅ Or use + for a new list nums = [1, 2, 3] result = nums + [4] print(result) # [1, 2, 3, 4] # Same for extend nums = [1, 2, 3] result = nums.extend([4, 5]) print(result) # None # Same for += nums = [1, 2, 3] # result = nums += [4] # SyntaxError — += is a statement # Why the None return? # 1. Consistency # All in-place methods return None: # list.append, list.extend, list.sort, # list.reverse, list.insert, list.remove, # dict.update, dict.clear, set.add # The None means "I modified the object, # I didn't create a new one." # 2. Prevent the bug # If append returned the list, you might write: # result = original.append(x) # expecting a new list, but you'd get the # same list — modified. The None forces you # to think about mutation vs new list. # 3. Idiomatic Python # In-place mutation is a common pattern in # Python. Returning None signals "look at # the original." # The antipattern: # result = [] # result.append(x).append(y) # AttributeError: NoneType # # None has no append # # Fix: chain on the list itself, not the return: # result = [] # result.append(x) # result.append(y) # Or: # result.extend([x, y]) # What DOES return? # list.copy() → new list # list + [x] → new list # list[:] → new list # sorted(list) → new list # list.copy() → new list # # What DOESN'T? # list.append() → None # list.extend() → None # list.sort() → None # list.reverse() → None
Why None is a feature, not a bug:
The design pattern:
In-place mutators return None:
→ append, extend, insert, remove, pop,
clear, sort, reverse
→ dict.update, dict.clear, dict.pop
→ set.add, set.remove, set.update
New-object creators return the new object:
→ list.copy(), list + [x], list[:]
→ sorted(list), reversed(list) (iterator)
→ copy.copy(obj), copy.deepcopy(obj)
→ {**d1, **d2}, d1 | d2 (Python 3.9+)
The convention:
None from a method means "look at the receiver,
not the return value."
A return value means "look at the return value,
the receiver is unchanged."
This is how Python prevents the classic bug:
# In JavaScript:
const arr = [1, 2, 3];
const result = arr.push(4); // returns length (4)
// arr is [1, 2, 3, 4]
// result is 4
# In Python:
nums = [1, 2, 3]
result = nums.append(4) # returns None
# nums is [1, 2, 3, 4]
# result is None — you can't accidentally
# use the return as if it were the list
The None return forces a decision:
❌ result = nums.append(4)
# result is None. Probably a bug.
# What did you mean?
✅ nums.append(4)
# Modifies nums. No confusion.
✅ result = nums + [4]
# Creates a new list. nums is unchanged.
The choice is explicit.
No hidden semantics.
Compare to dict.update:
d = {'a': 1}
result = d.update({'b': 2}) # None
# d is {'a': 1, 'b': 2}
# result is None
# Or use | (Python 3.9+):
d = {'a': 1}
result = d | {'b': 2} # {'a': 1, 'b': 2}
# d is {'a': 1}, result is new dict
Same pattern: in-place → None, new → return.
Chaining:
Since in-place methods return None, you can't
chain them:
❌ nums.append(1).append(2) # AttributeError
✅ nums.append(1)
nums.append(2)
✅ nums.extend([1, 2])
For chaining, use fluent APIs:
Some libraries return self from mutators
(e.g., pandas DataFrames, some ORMs).
Python's built-in list doesn't — and that's
intentional. The standard library favors
clarity over chaining.
The test:
If you're unsure whether a method returns
a new object or None, check:
→ Does it mutate the receiver? → returns None
→ Does it create a new object? → returns it
In-place: append, extend, sort, reverse,
update, add, clear, remove, insert
Return: copy, sorted, reversed, slice,
+, |, |=
The one exception: += on lists.
nums += [4, 5] returns None (it's a statement,
not an expression). But it mutates.
This matches the pattern: mutation → None.
You can't write `result = nums += [4]`
because += is a statement in Python.
The bigger picture:
Python's standard library follows a "clarity
over cleverness" principle. The None return
forces you to be explicit about whether you
want mutation or a new object.
Some developers find this annoying.
Some find it protective.
The alternative (returning self, like
pandas) enables fluent chains but makes it
easier to write `x = y.append(z)` and expect
a new list.
Python chose the safer option.
✅ Building lists efficiently
append vs + in loops.
# ✅ Building a list — append is O(1) amortized
results = []
for x in range(10000):
results.append(x * x)
# ✅ extend is also O(k) for k items
results = []
for chunk in chunks:
results.extend(chunk)
# ✅ List comprehension — idiomatic, fastest
results = [x * x for x in range(10000)]
# ❌ + in a loop is O(n²)
results = []
for x in range(10000):
results = results + [x * x]
# Each iteration creates a new list
# Total: 1 + 2 + 3 + ... + 10000 = 50M operations
# Why += is fine (unlike +)
results = []
for x in range(10000):
results += [x * x]
# += mutates in place (same as extend)
# O(1) amortized per iteration
# The comparison:
# | Method | Per iteration | Total for n |
# |----------------|---------------|--------------|
# | append | O(1) amortized| O(n) |
# | extend(chunk) | O(k) | O(n) |
# | += [item] | O(1) amortized| O(n) |
# | = + [item] | O(n) | O(n²) |
# | comprehension | — | O(n) fastest |
# When to use which:
# ✅ List comprehension — when you have a transform
results = [f(x) for x in data]
# ✅ Generator + extend — when you have a generator
results.extend(gen)
# ✅ Append — when you need conditional logic
results = []
for x in data:
if should_include(x):
results.append(transform(x))
# ✅ Extend — when you're accumulating collections
all_items = []
for group in groups:
all_items.extend(group)
# ❌ results = results + [x] in a loop — never
# The comprehension is faster because it's
# optimized in C. For 10,000 items:
# append in loop: ~1ms
# comprehension: ~0.5ms
# The difference is small, but comprehensions
# also read more clearly.
# When comprehensions don't fit:
# 1. Multiple conditions:
results = []
for x in data:
if x > 0:
if x % 2 == 0:
results.append(x * 2)
# 2. Side effects:
results = []
for x in data:
results.append(x)
log(f'Processed {x}')
# 3. Early exit:
results = []
for x in data:
if x < 0:
break
results.append(x)
Why append is O(1) amortized:
CPython's list uses over-allocation.
When the list grows beyond capacity:
→ Allocates more space (typically ~1.125x)
→ Copies existing items
→ Capacity doubles-ish
Most appends fit in the pre-allocated space
→ O(1) per append
→ Occasionally a resize copies → O(n)
→ Amortized over many appends → O(1)
For 10,000 appends:
~13 resizes (2^13 > 10,000)
Total resize work: ~20,000 element copies
Average per append: ~2 element copies
→ Still O(1) amortized
Why + is O(n):
list1 + list2 creates a new list.
→ Allocates for len(list1) + len(list2)
→ Copies all items from list1
→ Copies all items from list2
→ O(n1 + n2) per call
In a loop of n iterations:
Iteration 1: + 1 item → copy 1 item
Iteration 2: + 1 item → copy 2 items
Iteration 3: + 1 item → copy 3 items
...
Iteration n: + 1 item → copy n items
Total: 1 + 2 + 3 + ... + n = n(n+1)/2 = O(n²)
For n = 10,000: ~50,000,000 element copies.
At ~1ns per copy: ~50ms.
For n = 100,000: ~5,000,000,000 copies.
~5 seconds.
The O(n²) becomes unusable at scale.
The fix is one character:
❌ result = result + [x] → O(n) per iteration
✅ result += [x] → O(1) per iteration
✅ result.append(x) → O(1) per iteration
The += is in-place; the + creates a new list.
Same difference as list += vs list = list +.
Why comprehension is faster than append:
[f(x) for x in data]
→ Compiles to a specialized bytecode
→ The compiler knows the result will be a list
→ Pre-allocates based on the iterable's length
(if the iterable has __len__)
→ No method call overhead per append
vs.
result = []
for x in data:
result.append(f(x))
→ LOAD_METHOD, CALL_METHOD for each append
→ Dynamic type check on the result (is it a list?)
→ The list starts empty, grows via resizing
For small n, the difference is microseconds.
For large n, comprehension is ~2x faster.
Both are O(n) — the difference is constants.
When to use what:
For 90% of list-building:
→ Use a comprehension
→ Clearest, fastest, most idiomatic
When the logic is complex:
→ Use append in a loop
→ Multiple conditions, side effects, breaks
When accumulating collections:
→ Use extend
→ Flattening, merging, chunking
Never:
→ result = result + [x] in a loop
→ This is the O(n²) bug that shows up at scale
The one case for +:
Creating a new list from two lists:
combined = list1 + list2
✅ Clear intent
✅ O(n) once
✅ Both inputs unchanged
Even here, you could use:
combined = list(list1)
combined.extend(list2)
Both work. The + is shorter. For one-time
combination, use +. For building in a loop,
use append/extend.
The rule:
+ for one-time concatenation.
append/extend for accumulating in a loop.
Comprehension for transforming data.
Choose based on context, not on avoiding one.
Don't use extend on a string unless you want characters. list.extend('hello') adds 'h', 'e', 'l', 'l', 'o' — five items, not one. Same for any string argument. If you want to add a string as a single element, use append: list.append('hello'). The same trap applies to tuples, sets, and any iterable — extend iterates them. This is one of the most common bugs when someone means "add this string to the list" and accidentally uses extend. The fix is one word: use append.
⚠️ Common mistakes
The append/extend mistakes that cost real time.
result = list.append(x)Returns None. Fix: list.append(x); result = list.
extend when you mean appendAdds each item individually. Fix: append for one item.
append when you mean extendAdds the list as one nested element. Fix: extend for a collection.
extend('string')Adds each character. Fix: append('string') for the whole string.
list = list + [x] in a loopO(n²) — creates a new list each iteration. Fix: append or +=.
.append()Returns None, so chaining fails. Fix: separate statements.
+= on a tupleCreates a new tuple (tuples aren't mutable). Fix: use a list for in-place ops.
extend on a dictIterates keys, not items. Fix: extend(d.items()) or append(d).
❓ Frequently asked
What is the difference between append and extend?
append adds its argument as a single element to the end of the list. If the argument is a list, that list becomes one element (nested). extend adds each element of its argument individually. Example: [1, 2].append([3, 4]) → [1, 2, [3, 4]]. [1, 2].extend([3, 4]) → [1, 2, 3, 4]. Rule: append for one item, extend for a collection.
What does += do on a list?
list += other is equivalent to list.extend(other) — it adds each element of the iterable to the end. The difference from extend is subtle: += requires the right side to be an iterable, while extend accepts any iterable too. Both mutate in place and return None. The common confusion: list += list is extend, not append.
Does append return a new list?
No — append, extend, and += all mutate the original list in place and return None. This is a common mistake: result = my_list.append(x) gives result = None, not the appended list. The fix is to call append separately: my_list.append(x); result = my_list. Or use list concatenation to create a new list: result = my_list + [x].
Can I use append to add multiple items at once?
Not directly — append takes exactly one argument. To add multiple items, use extend: my_list.extend([a, b, c]). Or use a loop: for x in [a, b, c]: my_list.append(x). Or use +=: my_list += [a, b, c]. The only case where append adds multiple items is when you append a list — but that adds the list as one nested element, not its items individually.
What happens if I use extend with a string?
extend iterates the string character by character. So [1, 2].extend('abc') gives [1, 2, 'a', 'b', 'c'] — three items added. To add the string as one element, use append: [1, 2].append('abc') gives [1, 2, 'abc'].
What about list + [x] vs append?
list + [x] creates a NEW list — it does not modify the original. append mutates in place. Use + when you want a new list (functional style). Use append when you want to modify the existing list. For a single append, either works. For a loop of appends, use append — the copy-on-each-iteration from + is O(n²).
🔗 Continue learning
Next in this series:
a[start:stop:step] — indices point between elements, and that fixes 90% of off-by-one bugs