Lists are one of the very first data structures I learned in Python, and honestly, they’re also one of the ones I use the most, even years later. What surprised me as I got deeper into Python was just how many built-in methods and operators lists support — far more than I initially realized. In this guide, I want to go through every commonly used list method and operator, explain how each works internally, and cover the performance characteristics that actually matter when I’m writing production code.
What Is a Python List?
A Python list is a mutable, ordered collection that can hold elements of mixed types. Internally, CPython implements a list as a dynamic array — a contiguous block of memory holding pointers to Python objects, with some extra capacity pre-allocated to make appending efficient.
my_list = [1, "two", 3.0, [4, 5], True]
print(my_list)
Output:
[1, 'two', 3.0, [4, 5], True]
Core List Methods
append()
Adds a single element to the end of the list.
fruits = ["apple", "banana"]
fruits.append("cherry")
print(fruits)
Output:
['apple', 'banana', 'cherry']
append() runs in amortized O(1) time. CPython over-allocates extra space when the underlying array needs to grow, so most appends don’t require a full reallocation — only occasionally does the array need to be resized and copied.
extend()
Adds all elements from an iterable to the end of the list.
fruits.extend(["mango", "kiwi"])
print(fruits)
Output:
['apple', 'banana', 'cherry', 'mango', 'kiwi']
A common mistake I see (and made myself early on) is using append() when extend() was intended:
fruits.append(["grape", "fig"])
print(fruits)
Output:
['apple', 'banana', 'cherry', 'mango', 'kiwi', ['grape', 'fig']]
Notice append() added the whole list as a single nested element, while extend() would have added each item individually.
insert()
Inserts an element at a specific index, shifting subsequent elements to the right.
fruits.insert(1, "orange")
print(fruits)
Output:
['apple', 'orange', 'banana', 'cherry', 'mango', 'kiwi', ['grape', 'fig']]
insert() is O(n) because every element after the insertion point has to shift over by one position in memory.
remove()
Removes the first matching value.
fruits.remove("banana")
print(fruits)
Output:
['apple', 'orange', 'cherry', 'mango', 'kiwi', ['grape', 'fig']]
If the value isn’t found, remove() raises a ValueError.
pop()
Removes and returns an element at a given index (default: the last element).
last_item = fruits.pop()
print(last_item)
print(fruits)
first_item = fruits.pop(0)
print(first_item)
Output:
['grape', 'fig']
['apple', 'orange', 'cherry', 'mango', 'kiwi']
apple
Popping from the end is O(1); popping from the beginning or middle is O(n) because remaining elements have to shift left.
index()
Returns the index of the first matching value.
print(fruits.index("mango"))
Output:
2
count()
Counts occurrences of a value.
numbers = [1, 2, 2, 3, 2, 4]
print(numbers.count(2))
Output:
3
sort()
Sorts the list in place.
numbers = [5, 2, 8, 1, 9]
numbers.sort()
print(numbers)
numbers.sort(reverse=True)
print(numbers)
Output:
[1, 2, 5, 8, 9]
[9, 8, 5, 2, 1]
sort() uses a highly optimized algorithm called Timsort, which is a hybrid of merge sort and insertion sort, tuned to perform very well on real-world, partially ordered data. Its worst-case time complexity is O(n log n).
I can also sort using a custom key:
words = ["banana", "kiwi", "fig", "apple"]
words.sort(key=len)
print(words)
Output:
['fig', 'kiwi', 'apple', 'banana']
reverse()
Reverses the list in place.
numbers.reverse()
print(numbers)
Output:
[1, 2, 5, 8, 9]
copy()
Creates a shallow copy of the list.
original = [1, 2, [3, 4]]
copied = original.copy()
copied[0] = 100
print(original)
print(copied)
Output:
[1, 2, [3, 4]]
[100, 2, [3, 4]]
I have to be careful here: copy() only performs a shallow copy, meaning nested mutable objects (like the inner list [3, 4]) are shared between both lists.
copied[2].append(5)
print(original)
print(copied)
Output:
[1, 2, [3, 4, 5]]
[100, 2, [3, 4, 5]]
For a true deep copy, I use the copy module:
import copy
deep_copied = copy.deepcopy(original)
clear()
Removes all elements from the list.
sample = [1, 2, 3]
sample.clear()
print(sample)
Output:
[]
Supported Operators
Concatenation with +
a = [1, 2, 3]
b = [4, 5, 6]
print(a + b)
Output:
[1, 2, 3, 4, 5, 6]
This creates a new list — it doesn’t modify a or b in place.
Repetition with *
print([0] * 5)
Output:
[0, 0, 0, 0, 0]
A subtle trap I’ve hit before: multiplying a list containing a mutable object doesn’t create independent copies of that object.
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
Output:
[[1, 0, 0], [1, 0, 0], [1, 0, 0]]
All three rows are actually references to the same inner list, so modifying one modifies all of them. The correct way to build an independent grid is with a list comprehension:
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)
Output:
[[1, 0, 0], [0, 0, 0], [0, 0, 0]]
Membership Testing with in
print(3 in [1, 2, 3])
print(9 in [1, 2, 3])
Output:
True
False
Membership testing on a list is O(n) because Python has to check elements one by one. For frequent membership checks on large collections, I switch to a set, which offers average O(1) lookup.
Comparison Operators
Lists support element-wise comparison:
print([1, 2, 3] == [1, 2, 3])
print([1, 2, 3] < [1, 2, 4])
print([1, 2] < [1, 2, 3])
Output:
True
True
True
Comparisons work lexicographically, similar to how strings compare — Python compares elements pairwise until it finds a difference.
Slicing
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:5])
print(numbers[:3])
print(numbers[7:])
print(numbers[::2])
print(numbers[::-1])
Output:
[2, 3, 4]
[0, 1, 2]
[7, 8, 9]
[0, 2, 4, 6, 8]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Slicing always creates a new list and runs in O(k), where k is the length of the slice.
Internal Working: Why Lists Are Fast for Some Things, Slow for Others
Because Python’s list is a dynamic array under the hood, it gets:
- O(1) indexing, since it’s just pointer arithmetic to find the memory address of the nth element.
- O(1) amortized appending, thanks to over-allocation strategy (CPython typically grows the array by roughly 1.125x plus a small constant when it needs to resize).
- O(n) insertion/removal at arbitrary positions, since elements have to shift.
This is the exact opposite trade-off profile of a linked list, which offers O(1) insertion/removal (given a node reference) but O(n) indexing.
Real-World and Automation Use Cases
- Data pipelines: collecting and transforming records with
append()and comprehensions. - Sorting and ranking data: using
sort()with customkeyfunctions for reports and dashboards. - Deduplication workflows: combining lists with
set()conversions when order doesn’t matter, or manual logic when it does. - Batch processing: slicing large lists into smaller chunks for processing.
def chunk_list(data, size):
return [data[i:i + size] for i in range(0, len(data), size)]
print(chunk_list(list(range(10)), 3))
Output:
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Common Mistakes and Debugging Tips
- Mutating a list while iterating over it. This can cause elements to be skipped or processed twice.
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n)
print(nums)
Output:
[1, 3, 5]
This happens to work for this specific case but is fragile in general — I always iterate over a copy (for n in nums[:]) or build a new list instead when I need to modify while iterating.
- Using
*to create nested lists, which shares references as shown earlier. - Confusing
sort()withsorted().sort()modifies the list in place and returnsNone;sorted()returns a new list and leaves the original untouched. - Using
remove()expecting it to remove all occurrences. It only removes the first match.
Best Practices I Follow
- Use
sorted()when I need the original list preserved; usesort()when in-place modification is intended and desired. - Use list comprehensions instead of manually building lists in a loop with repeated
.append()calls for cleaner code. - Use
copy.deepcopy()whenever nested mutable structures need true independence. - Prefer
setfor membership-heavy workloads on large collections.
FAQs
Q: What’s the time complexity of list.append()? Amortized O(1).
Q: Is list.sort() stable? Yes, Timsort is a stable sorting algorithm — equal elements retain their relative order.
Q: How do I remove duplicates from a list? list(set(my_list)) if order doesn’t matter, or list(dict.fromkeys(my_list)) if I need to preserve order.
Q: What’s the difference between remove() and del? remove(value) deletes the first occurrence of a value; del list[index] deletes by position.
Troubleshooting Tips
- If a
ValueErroris raised fromremove(), check that the value actually exists in the list first, perhaps usingin. - If nested list mutations behave unexpectedly, check whether you’re dealing with shared references from a shallow copy or
*repetition. - If sorting throws a
TypeError, check whether the list contains mixed, non-comparable types (e.g., strings mixed with integers).
Summary
Python’s list methods and operators cover an impressively wide range of operations, and understanding their time complexity and internal behavior has made a real difference in how I write efficient, bug-free code. The dynamic array design behind Python lists explains virtually every performance characteristic I’ve come across — fast indexing and appending, slower insertion and removal in the middle, and the sharing pitfalls that come with shallow copies.
References
- Python Official Documentation: More on Lists
- Python Official Documentation: copy module
- Python Wiki: TimeComplexity
- PEP 3135 – New Super (referenced for general Python 3 data model changes)