Coding tips

Python append vs extend vs insert: Differences, + vs +=, Strings, Nested Lists and Speed Compared (Tested Examples)

append() adds one item to the end of a Python list, extend() adds every item from an iterable, and insert() adds one item at a position. Here is a tested comparison in Python 3.13: strings, negative indexes, + vs +=, the None return value, nested lists and speed.

By 9 min read 0 views
Cover image for Python append vs extend vs insert: Differences, + vs +=, Strings, Nested Lists and Speed Compared (Tested Examples)

append(x) adds x as a single item at the end of a list, extend(iterable) adds each item of an iterable at the end, and insert(i, x) adds one item before index i. So [1, 2].append([3, 4]) gives [1, 2, [3, 4]], while extend([3, 4]) gives [1, 2, 3, 4]. All three change the list in place and return None. append and extend are fast; insert near the front is slow for big lists.

Every result below was run with Python 3.13.5. The timings come from timeit on my machine, so your numbers will differ, but the gaps between them hold.

In this guide:

  • Quick comparison table
  • append vs extend: one item vs many
  • All three return None
  • The string trap: extend splits strings
  • extend works with any iterable
  • insert: positions, negative indexes and out-of-range
  • + vs += vs extend
  • The nested list bug: appending the same list twice
  • Slice assignment: insert many items at a position
  • Flattening a list of lists: extend in a loop
  • The mutable default argument trap
  • Speed: append, extend, comprehension, insert(0) and deque
  • Which one should you use?
  • Frequently asked questions

Quick comparison table

append(x)extend(iterable)insert(i, x)
AddsOne itemEvery item from an iterableOne item
WhereEndEndBefore index i
[1, 2] with [3, 4][1, 2, [3, 4]][1, 2, 3, 4]insert(1, [3, 4]) gives [1, [3, 4], 2]
Length grows by1len(iterable)1
With a string "bc"["a", "bc"]["a", "b", "c"]["bc", "a"] with index 0
ReturnsNoneNoneNone
Time (average)O(1)O(k) for k itemsO(n), shifts later items
Same asa[len(a):] = [x]a += iterablea[i:i] = [x]

The Python tutorial on lists defines each method in terms of slice assignment, which is the "Same as" row.

append vs extend: one item vs many

a = [1, 2]
a.append([3, 4])
print("append:", a, len(a))

b = [1, 2]
b.extend([3, 4])
print("extend:", b, len(b))

c = [1, 2]
c.insert(1, 99)
print("insert:", c)
append: [1, 2, [3, 4]] 3
extend: [1, 2, 3, 4] 4
insert: [1, 99, 2]

append never looks inside its argument. It adds the whole list as one element, so the length goes from 2 to 3. extend loops over the argument and adds each element, so the length goes to 4. If you see a nested list where you expected a flat one, you used append where you needed extend.

All three return None

nums = [1, 2]
result = nums.append(3)
print(result, nums)
nums = nums.append(4)
print(nums)
None [1, 2, 3]
None

Writing nums = nums.append(4) replaces your list with None. The next line that uses it fails with AttributeError: 'NoneType' object has no attribute, or with 'NoneType' object is not subscriptable if you index it. Call the method on its own line.

The string trap: extend splits strings

letters = ["a"]
letters.extend("bc")
print(letters)
words = ["hi"]
words.append("there")
words.extend(["you"])
print(words)
['a', 'b', 'c']
['hi', 'there', 'you']

A string is an iterable of characters, so extend("bc") adds "b" and "c" separately. To add one word, use append("word"), or wrap it: extend(["word"]).

extend works with any iterable

items = [0]
items.extend(range(1, 4))
items.extend((4, 5))
items.extend({6})
items.extend({"k": "v"})
items.extend(x * 10 for x in (7, 8))
print(items)
try:
    items.extend(9)
except TypeError as e:
    print("TypeError:", e)
[0, 1, 2, 3, 4, 5, 6, 'k', 70, 80]
TypeError: 'int' object is not iterable

extend accepts ranges, tuples, sets, generators and dictionaries. A dictionary gives its keys, here 'k'. The order of a set is not guaranteed, so do not rely on it. A single number is not iterable, so extend(9) raises TypeError; use append(9).

insert: positions, negative indexes and out-of-range

nums = [10, 20, 30]
nums.insert(0, 5)
print("insert(0):", nums)
nums.insert(-1, 25)
print("insert(-1):", nums)
nums.insert(100, 99)
print("insert(100):", nums)
nums.insert(len(nums), 100)
print("insert(len):", nums)
insert(0): [5, 10, 20, 30]
insert(-1): [5, 10, 20, 25, 30]
insert(100): [5, 10, 20, 25, 30, 99]
insert(len): [5, 10, 20, 25, 30, 99, 100]

Three things surprise people. insert(-1, x) puts x before the last item, not at the end. An index larger than the list does not raise IndexError; the item just goes at the end. And insert(len(a), x) is the same as append(x). Use insert when position matters; otherwise use append.

+ vs += vs extend

a = [1, 2]
b = a
a = a + [3]
print("a + [3]:", a, "| b:", b)

c = [1, 2]
d = c
c += [3]
print("c += [3]:", c, "| d:", d)

e = [1, 2]
try:
    e = e + (3, 4)
except TypeError as err:
    print("TypeError:", err)
e += (3, 4)
print("e += (3, 4):", e)
a + [3]: [1, 2, 3] | b: [1, 2]
c += [3]: [1, 2, 3] | d: [1, 2, 3]
TypeError: can only concatenate list (not "tuple") to list
e += (3, 4): [1, 2, 3, 4]

a = a + [3] builds a new list, so b, which still points at the old list, is unchanged. c += [3] changes the list in place, like extend, so d sees the change. + only accepts another list, while += accepts any iterable, as the tuple example shows. This difference matters when the same list is shared, for example as a function argument. My post on copy vs deepcopy vs assignment covers shared references.

The nested list bug: appending the same list twice

rows = []
row = [0, 0]
rows.append(row)
rows.append(row)
rows[0][0] = 9
print(rows)
safe = []
safe.append(row.copy())
safe.append(row.copy())
safe[0][0] = 1
print(safe)
[[9, 0], [9, 0]]
[[1, 0], [9, 0]]

append stores a reference, not a copy. Both rows are the same object, so changing one changes both. The fix is to append a copy, or build each row fresh, for example [[0, 0] for _ in range(3)]. In the output, the copies started from [9, 0] because row had already been changed, and only the first copy was then edited.

Slice assignment: insert many items at a position

nums = [1, 5]
nums[1:1] = [2, 3, 4]
print(nums)
nums[len(nums):] = [6]
print(nums)
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5, 6]

There is no insert_many(). To insert several items in the middle, assign to an empty slice: nums[1:1] = [2, 3, 4]. Assigning to nums[len(nums):] is what extend does. The mutable sequence operations table lists all of these.

Flattening a list of lists: extend in a loop

from itertools import chain
groups = [[1, 2], [3], [4, 5]]

flat = []
for g in groups:
    flat.extend(g)
print("extend loop:", flat)

print("comprehension:", [x for g in groups for x in g])
print("chain:", list(chain.from_iterable(groups)))

wrong = []
for g in groups:
    wrong.append(g)
print("append loop:", wrong)

a, b = [1, 2], [3]
print("unpacking:", [*a, *b], "| a still:", a)
extend loop: [1, 2, 3, 4, 5]
comprehension: [1, 2, 3, 4, 5]
chain: [1, 2, 3, 4, 5]
append loop: [[1, 2], [3], [4, 5]]
unpacking: [1, 2, 3] | a still: [1, 2]

This is the most common real use of extend. Each group is added item by item, so the result is flat. Using append in the same loop just copies the nested structure. A nested list comprehension or itertools.chain.from_iterable() gives the same flat result in one line, and [*a, *b] builds a new list without changing a. If the data comes from an API, the free tools page has browser-based helpers for looking at its shape first.

The mutable default argument trap

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag("python"))
print(add_tag("git"))

def add_tag_fixed(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

print(add_tag_fixed("python"))
print(add_tag_fixed("git"))
['python']
['python', 'git']
['python']
['git']

The default list is created once, when the function is defined, not on every call. Each append adds to that same list, so the second call returns ['python', 'git']. Use None as the default and create a fresh list inside the function, as the fixed version does. Linters such as Ruff and Pylint flag mutable defaults for this reason.

Speed: append, extend, comprehension, insert(0) and deque

import timeit
from collections import deque
n = 20_000
tests = {
    "append in a loop": "x = []\nfor i in range(n): x.append(i)",
    "extend once": "x = []\nx.extend(range(n))",
    "list comprehension": "x = [i for i in range(n)]",
    "insert(0) in a loop": "x = []\nfor i in range(n): x.insert(0, i)",
    "deque.appendleft": "x = deque()\nfor i in range(n): x.appendleft(i)",
}
for name, stmt in tests.items():
    best = min(timeit.repeat(stmt, globals=globals(), number=20, repeat=5)) / 20
    print(f"{name:22} {best * 1000:8.3f} ms for {n:,} items")
append in a loop          0.269 ms for 20,000 items
extend once               0.166 ms for 20,000 items
list comprehension        0.207 ms for 20,000 items
insert(0) in a loop      31.315 ms for 20,000 items
deque.appendleft          0.331 ms for 20,000 items

Building 20,000 items with extend or a list comprehension was fastest, and an append loop was close. insert(0, x) in a loop was over 100 times slower, because each insert shifts every existing item one place. If you need to add to the front often, use collections.deque, whose appendleft() is fast at both ends. For more on choosing a structure, see list vs tuple vs set vs dict.

Which one should you use?

  • Adding one item at the end: append(x).
  • Adding all items from another list, tuple or generator: extend(items) or a += items.
  • Adding one item at a specific position: insert(i, x).
  • Adding several items at a position: slice assignment, a[i:i] = items.
  • Need a new list and keep the old one: a + b or [*a, *b].
  • Many inserts at the front: collections.deque.
  • Building a list from a loop: a list comprehension is often shortest.

Frequently asked questions

What is the difference between append and extend in Python?

append adds its argument as one item, so appending a list creates a nested list. extend adds each item from an iterable, so the list stays flat.

What is the difference between append and insert?

append always adds at the end. insert(i, x) adds before index i, and it is slower for large lists because later items have to shift.

Why does append return None?

append changes the list in place and returns None, like extend, insert and sort. Do not assign the result back to the list.

Why does extend split my string into letters?

A string is an iterable of characters. Use append("word") or extend(["word"]) to add the whole string as one item.

Is += the same as extend?

For lists, yes: a += b changes a in place and accepts any iterable. a = a + b creates a new list and only accepts another list.

What does insert(-1, x) do?

It inserts x before the last item, not at the end. Use append(x) or insert(len(a), x) to add at the end.

Can insert add multiple items at once?

No. Use slice assignment instead, for example a[1:1] = [2, 3, 4].

Last checked: 9 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.

Share X LinkedIn WhatsApp Facebook
Was this helpful?

0%