Post

Python Lists: The Complete Guide with All Methods

Master Python lists — creation, indexing, slicing, comprehensions, and every built-in method (append, extend, insert, sort, and more) with runnable examples and gotchas.

Python Lists: The Complete Guide with All Methods

Lists are the workhorse of Python. They’re mutable, ordered, and can hold anything — numbers, strings, other lists, even functions. If you’re coming from another language, think of them as resizable arrays with a huge standard library of built-in behavior. This guide covers everything from the fundamentals to every list method you’ll actually use.

List Fundamentals

Creating Lists

1
2
3
4
5
6
empty = []
numbers = [1, 2, 3, 4, 5]
mixed = [1, "two", 3.0, [4, 5]]
from_range = list(range(5))          # [0, 1, 2, 3, 4]
from_string = list("abc")            # ['a', 'b', 'c']
squares = [x**2 for x in range(6)]   # comprehension: [0, 1, 4, 9, 16, 25]

List comprehensions are the idiomatic way to build a list from an iterable in one line. They’re faster than an equivalent for loop with repeated .append() calls because the loop runs in C rather than bytecode-by-bytecode in the interpreter.

Indexing and Negative Indexing

1
2
3
4
fruits = ["apple", "banana", "cherry", "date"]
fruits[0]      # "apple"   — first element
fruits[-1]     # "date"    — last element
fruits[-2]     # "cherry"  — second to last

Negative indices count from the end, so -1 is always the last item without needing len(fruits) - 1.

Slicing

1
2
3
4
5
fruits[1:3]    # ["banana", "cherry"]  — index 1 up to (not including) 3
fruits[:2]     # ["apple", "banana"]   — from start
fruits[2:]     # ["cherry", "date"]    — to end
fruits[::-1]   # ["date", "cherry", "banana", "apple"]  — reversed copy
fruits[::2]    # every second element

Slicing always returns a new list — it never mutates the original, and it never raises an IndexError even if the bounds are out of range.

Mutation

1
2
3
fruits[0] = "avocado"          # replace by index
fruits[1:3] = ["kiwi", "mango"]  # replace a slice with a different-length slice
del fruits[0]                   # remove by index

Lists are mutable, so passing one into a function lets that function change it in place — a common source of bugs if you don’t intend that.

Nesting

1
2
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix[1][2]   # 6 — row 1, column 2

Nested lists are just lists whose elements happen to be lists. There’s no special “2D array” type in core Python.

Iteration and Membership

1
2
3
4
5
6
7
for fruit in fruits:
    print(fruit)

for i, fruit in enumerate(fruits):
    print(i, fruit)

"kiwi" in fruits       # True — membership test, O(n)

Copying: Shallow vs Deep

This is where most bugs come from:

1
2
3
4
5
6
7
8
9
10
import copy

original = [[1, 2], [3, 4]]
shallow = original.copy()        # or list(original) or original[:]
shallow[0].append(99)
print(original)   # [[1, 2, 99], [3, 4]]  — inner list is SHARED!

deep = copy.deepcopy(original)
deep[0].append(100)
print(original)   # unaffected

.copy(), list(x), and x[:] all produce a shallow copy: the outer list is new, but nested mutable objects (lists, dicts) inside it are still shared references. If your list contains only immutable elements (numbers, strings, tuples), a shallow copy is all you need. If it contains nested lists or dicts you plan to mutate, use copy.deepcopy().

List Methods

list.append()

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

Adds a single item to the end of the list, in place. Returns None — don’t write nums = nums.append(4), you’ll end up with None. Gotcha: append() adds the argument as one element, even if it’s a list — nums.append([5, 6]) gives [1, 2, 3, [5, 6]], not a flattened list. Use extend() for that.

list.extend()

1
2
3
nums = [1, 2, 3]
nums.extend([4, 5])
print(nums)   # [1, 2, 3, 4, 5]

Appends every item from an iterable individually, in place. Equivalent to nums += [4, 5]. Use when you want to merge two lists (or add multiple items) rather than nest one inside the other.

list.insert()

1
2
3
nums = [1, 2, 4]
nums.insert(2, 3)
print(nums)   # [1, 2, 3, 4]

Inserts an item at a given index, shifting everything after it to the right. Gotcha: inserting near the front of a large list is O(n) because every subsequent element has to move — if you’re doing this repeatedly, consider collections.deque instead.

list.remove()

1
2
3
nums = [1, 2, 3, 2]
nums.remove(2)
print(nums)   # [1, 3, 2]  — only the FIRST match is removed

Removes the first occurrence of a value by value, not by index. Raises ValueError if the value isn’t present — wrap in a try/except or check in first if that’s expected.

list.pop()

1
2
3
nums = [1, 2, 3]
last = nums.pop()      # removes and returns 3
first = nums.pop(0)    # removes and returns 1

Removes and returns the item at the given index (default: the last one). This is the only removal method that gives you the value back, which makes it perfect for stack (pop()) and queue-like (pop(0)) patterns — though for queues, deque.popleft() is O(1) versus pop(0)’s O(n).

list.clear()

1
2
3
nums = [1, 2, 3]
nums.clear()
print(nums)   # []

Removes all items in place, leaving an empty list. Equivalent to del nums[:], but more readable. Use when you want to empty a list that other variables still reference (unlike nums = [], which just rebinds the name and leaves other references pointing at the old, non-empty list).

list.copy()

1
2
3
4
nums = [1, 2, 3]
backup = nums.copy()
backup.append(4)
print(nums, backup)   # [1, 2, 3] [1, 2, 3, 4]

Returns a shallow copy — see the fundamentals section above for the nested-list caveat.

list.index()

1
2
3
fruits = ["apple", "banana", "cherry"]
fruits.index("cherry")        # 2
fruits.index("banana", 1)     # search starting at index 1

Returns the index of the first matching value. Raises ValueError if not found — check with in first, or catch the exception, rather than letting it crash unhandled.

list.count()

1
2
nums = [1, 2, 2, 3, 2]
nums.count(2)   # 3

Returns how many times a value appears. Use when you need a quick frequency check on one value; for counting all values at once, collections.Counter is far more efficient than calling .count() in a loop.

list.sort()

1
2
3
4
5
6
nums = [3, 1, 4, 1, 5]
nums.sort()                        # [1, 1, 3, 4, 5], in place
nums.sort(reverse=True)            # [5, 4, 3, 1, 1]

words = ["banana", "kiwi", "fig"]
words.sort(key=len)                # ["fig", "kiwi", "banana"]

Sorts in place and returns None. Use the key parameter to sort by a derived value instead of the raw elements. Gotcha: if you need the original order preserved and a new list returned, use the builtin sorted(nums) instead — .sort() mutates and returns nothing.

list.reverse()

1
2
3
nums = [1, 2, 3]
nums.reverse()
print(nums)   # [3, 2, 1]

Reverses the list in place. For a reversed copy without touching the original, use nums[::-1] or reversed(nums).

Method Summary Table

MethodReturnsMutates?
append(x)NoneYes
extend(iter)NoneYes
insert(i, x)NoneYes
remove(x)NoneYes
pop(i=-1)The removed itemYes
clear()NoneYes
copy()New list (shallow)No
index(x)Index (int)No
count(x)Count (int)No
sort(key=, reverse=)NoneYes
reverse()NoneYes

Performance Notes

A few facts worth internalizing if you use lists heavily:

  • Appending is amortized O(1): CPython over-allocates space when a list grows, so append() doesn’t reallocate on every call — only occasionally, when the reserved space runs out.
  • Inserting or removing at the front is O(n): both insert(0, x) and pop(0) have to shift every remaining element by one position. If you need fast operations at both ends, use collections.deque instead of a list.
  • Membership tests (in) are O(n): a list has to be scanned linearly. If you’re checking membership repeatedly against a large collection, convert it to a set first — set membership is O(1) on average.
  • List comprehensions beat for + append() loops: the comprehension’s iteration happens in a single specialized bytecode loop, avoiding the repeated attribute lookup (nums.append) and function-call overhead of calling a bound method on every iteration.

Lists vs. Tuples

A common early-Python question is when to reach for a list versus a tuple. The short answer: use a list when the collection needs to change size or contents over its lifetime (mutable), and a tuple when it represents a fixed, immutable record — coordinates, RGB values, a row read from a database. Tuples are also hashable (as long as their contents are), so they can be used as dictionary keys or set members; lists cannot, since Python won’t hash a mutable object.

1
2
point = (3, 4)          # tuple: fixed pair, won't change
scores = [90, 85, 77]   # list: will grow/shrink/reorder over time

Where to Go Next

Lists are one piece of Python’s core toolkit. To round it out:

Once you’re comfortable with these eleven methods, slicing, and the shallow-vs-deep copy distinction, you know essentially everything you need to use Python lists effectively in real code.

Khushal Jethava
Khushal Jethava

Machine Learning Engineer at Codiste, specializing in Generative AI, NLP, and Computer Vision. Building production AI systems with Python.

This post is licensed under CC BY 4.0 by the author.