Skip to content

Lists

Lists are among Python's most valuable and frequently used data types, and they are the heart of countless programs. 📋 So, what exactly is a list, and why are they so indispensable?

In essence, a list is a dynamic and mutable sequence of values. Think of it as a versatile bag storing various items—numbers, words, and even a medley of data types—all within a single container. This flexibility makes lists an invaluable asset for any Python programmer.

Tip

Closely related to lists are tuples, which are immutable, but can be managed with similar syntax.

Let’s explore the concept further with a real-world analogy. Imagine you have a collection of names, distances, and mixed data types like this:

Example Python Lists
1
2
3
users = ["Jim", "Dwight", "Michael"]
distance_in_kms = [32, 97, 51, 403, 21, 53, 81]
mixed_types = ["Jim", 23, 3.14, True, "Berzerk"]

In this example, users contains a list of names of string types, distance_in_kms stores a list of distances in int type, and mixed_types seemingly defies convention by accommodating a diverse set of data types. Lists are compelling because they allow you to organize and manipulate such data effortlessly.

Accessing List Items

List items can be accessed via their index system like this:

Accessing List Items via Indexc
users = ["Jim", "Dwight", "Michael"]
print(f"What is your favourite moment when {users[0]} pulls a prank on {users[1]}?")

Returns:

What is your favourite moment when Jim pulls a prank on Dwight?

Lists can even be accessed through negative indexing like this:

Negative Indexing of Lists
users = ["Jim", "Dwight", "Michael"]
print(f"What was one thing that {users[-1]} did you found extra funny?")

Outputs:

What was one thing that Michael did you found extra funny?

Adding and Removing Items from Lists

What makes lists genuinely exceptional is their mutability. Unlike some data types in Python, lists can change and adapt as your program runs. Flexible, like a good morning stretch. 🧘 Adding, removing, or modifying elements within a list makes it a dynamic tool for handling evolving data. This adaptability is particularly useful when managing data collections that may grow or shrink in size.

Appending Items to Lists
1
2
3
users = ["Jim", "Dwight", "Michael"]
users.append("Pam")
print(f"We've added {users[-1]} to the list!")

Results in:

We've added Pam to the list!

More Ways to Add Items

Insert at Specific Position
users = ["Jim", "Dwight", "Michael"]
users.insert(1, "Andy")  # Insert at index 1
print(users)  # ['Jim', 'Andy', 'Dwight', 'Michael']

# Extend with multiple items
users.extend(["Pam", "Angela"])
print(users)  # ['Jim', 'Andy', 'Dwight', 'Michael', 'Pam', 'Angela']

# You can also use + to concatenate lists
more_users = users + ["Kevin", "Oscar"]
print(more_users)

Removing Items

Removing List Items
users = ["Jim", "Dwight", "Michael", "Andy", "Pam"]

# Remove by value (first occurrence only)
users.remove("Andy")
print(users)  # ['Jim', 'Dwight', 'Michael', 'Pam']

# Remove by index and get the value back
last_user = users.pop()      # Removes and returns last item
print(f"Removed: {last_user}")  # Removed: Pam

second_user = users.pop(1)   # Remove at index 1
print(f"Removed: {second_user}")  # Removed: Dwight

# Remove by index without returning
del users[0]
print(users)  # ['Michael']

# Clear the entire list
users.clear()
print(users)  # []

Removing Non-existent Items

remove() raises a ValueError if the item doesn't exist. Use if item in list: first, or wrap in a try/except block.

List Methods

Lists come with many useful built-in methods:

Useful List Methods
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]

# Count occurrences
print(numbers.count(1))  # 2
print(numbers.count(7))  # 0

# Find index of first occurrence
print(numbers.index(5))  # 4
# numbers.index(99)  # ValueError if not found!

# Reverse in place
numbers.reverse()
print(numbers)  # [3, 5, 6, 2, 9, 5, 1, 4, 1, 3]

# Get length
print(len(numbers))  # 10

Sorting Lists

Python offers two ways to sort: sort() modifies the list in place, while sorted() returns a new sorted list. Knowing when to use each is like knowing when to brew fresh coffee versus reheating yesterday's — one gives you something new, the other changes what you've got. ☕

Sorting Lists
numbers = [3, 1, 4, 1, 5, 9, 2, 6]

# Sort in place (modifies original)
numbers.sort()
print(numbers)  # [1, 1, 2, 3, 4, 5, 6, 9]

# Sort in reverse
numbers.sort(reverse=True)
print(numbers)  # [9, 6, 5, 4, 3, 2, 1, 1]

# sorted() returns a NEW list, leaving original unchanged
original = [3, 1, 4, 1, 5]
sorted_copy = sorted(original)
print(original)     # [3, 1, 4, 1, 5] — unchanged!
print(sorted_copy)  # [1, 1, 3, 4, 5]

Sorting with a Key Function

For complex sorting, use the key parameter:

Sorting with Key
words = ["banana", "pie", "Washington", "a"]

# Sort by length
words.sort(key=len)
print(words)  # ['a', 'pie', 'banana', 'Washington']

# Sort case-insensitively
words = ["banana", "Apple", "cherry"]
words.sort(key=str.lower)
print(words)  # ['Apple', 'banana', 'cherry']

# Sort objects by attribute
users = [
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35}
]
users.sort(key=lambda u: u["age"])
print([u["name"] for u in users])  # ['Bob', 'Alice', 'Charlie']

Copying Lists

Be careful when copying lists — assignment creates a reference, not a copy!

The Aliasing Trap
1
2
3
4
5
original = [1, 2, 3]
alias = original  # Both point to the SAME list!

alias.append(4)
print(original)  # [1, 2, 3, 4] — original changed too! 😱

Shallow Copy

Creates a new list, but nested objects are still shared:

Shallow Copy
original = [1, 2, 3]

# Three ways to shallow copy
copy1 = original.copy()
copy2 = original[:]
copy3 = list(original)

copy1.append(4)
print(original)  # [1, 2, 3] — original unchanged ✅
print(copy1)     # [1, 2, 3, 4]

Deep Copy (for Nested Lists)

For lists containing other lists or objects, you need a deep copy:

Deep Copy
import copy

original = [[1, 2], [3, 4]]

shallow = original.copy()
deep = copy.deepcopy(original)

# Modify nested list
shallow[0].append(999)
print(original)  # [[1, 2, 999], [3, 4]] — shallow copy shares nested lists!
print(shallow)   # [[1, 2, 999], [3, 4]]
print(deep)      # [[1, 2], [3, 4]] — deep copy is independent ✅

When to Deep Copy

Use copy.deepcopy() when your list contains:

  • Other lists or dicts
  • Custom objects
  • Any mutable nested structures

For flat lists of immutable items (numbers, strings), shallow copy is fine.

List Operations

List Operations
# Concatenation
a = [1, 2, 3]
b = [4, 5, 6]
c = a + b
print(c)  # [1, 2, 3, 4, 5, 6]

# Repetition
d = [0] * 5
print(d)  # [0, 0, 0, 0, 0]

# But be careful with mutable items!
matrix = [[0]] * 3  # Creates 3 references to the SAME list!
matrix[0].append(1)
print(matrix)  # [[0, 1], [0, 1], [0, 1]] — all changed! 😱

# Correct way for nested lists:
matrix = [[0] for _ in range(3)]
matrix[0].append(1)
print(matrix)  # [[0, 1], [0], [0]] — only first changed ✅

Key Takeaways

Concept What to Remember
Creating [1, 2, 3] or list()
Accessing Zero-indexed: list[0], negative: list[-1]
Adding append(), insert(i, x), extend(), +
Removing remove(x), pop(), pop(i), del, clear()
Sorting list.sort() in-place, sorted(list) returns new
Key sorting sort(key=len) or sort(key=lambda x: ...)
Shallow copy list.copy(), list[:], list(original)
Deep copy copy.deepcopy(list) for nested structures
Gotcha a = b creates alias, not copy!