Introduction
Lists are the workhorse of Python data structures. If you write Python for any length of time, you will use lists more than almost anything else. They are ordered, mutable, and flexible enough to hold any mix of data types. Whether you are storing user records, processing CSV rows, building queues, or transforming datasets, lists are your go-to tool.
In this tutorial we will cover everything you need to work with lists confidently — from creation and manipulation all the way through list comprehensions, copying gotchas, and real-world patterns you will use on the job every day.
1. Creating Lists
There are several ways to create a list in Python. Pick the one that fits your situation.
List Literal
The most common approach — just use square brackets.
# empty list
empty = []
# list of integers
numbers = [1, 2, 3, 4, 5]
# mixed types — perfectly valid
mixed = ["Alice", 30, True, 3.14, None]
# nested list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(numbers) # [1, 2, 3, 4, 5]
print(mixed) # ['Alice', 30, True, 3.14, None]
The list() Constructor
Converts any iterable into a list.
# from a string
chars = list("hello")
print(chars) # ['h', 'e', 'l', 'l', 'o']
# from a tuple
from_tuple = list((10, 20, 30))
print(from_tuple) # [10, 20, 30]
# from a set (order not guaranteed)
from_set = list({3, 1, 2})
print(from_set) # [1, 2, 3] — order may vary
# from a dictionary (keys only)
from_dict = list({"name": "Alice", "age": 30})
print(from_dict) # ['name', 'age']
Using range()
Generate sequences of numbers quickly.
# 0 through 9
nums = list(range(10))
print(nums) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 5 through 14
nums = list(range(5, 15))
print(nums) # [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
# even numbers from 0 to 20
evens = list(range(0, 21, 2))
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# countdown
countdown = list(range(10, 0, -1))
print(countdown) # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
2. Accessing Elements
Lists are zero-indexed. You can access elements from the front or the back, and you can slice out sub-lists with ease.
Indexing
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# first element
print(fruits[0]) # apple
# third element
print(fruits[2]) # cherry
# last element
print(fruits[-1]) # elderberry
# second to last
print(fruits[-2]) # date
Slicing
Slicing uses the syntax list[start:stop:step]. The start index is inclusive, the stop index is exclusive.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# elements from index 2 to 5 (exclusive)
print(numbers[2:5]) # [2, 3, 4]
# first three elements
print(numbers[:3]) # [0, 1, 2]
# from index 7 to the end
print(numbers[7:]) # [7, 8, 9]
# every other element
print(numbers[::2]) # [0, 2, 4, 6, 8]
# reverse the list
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# last three elements
print(numbers[-3:]) # [7, 8, 9]
# copy the entire list via slice
copy = numbers[:]
print(copy) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3. Modifying Lists
Lists are mutable — you can change them in place after creation. This is one of the key differences between lists and tuples.
Direct Assignment
colors = ["red", "green", "blue"]
# replace an element
colors[1] = "yellow"
print(colors) # ['red', 'yellow', 'blue']
# replace a slice
colors[1:3] = ["orange", "purple", "pink"]
print(colors) # ['red', 'orange', 'purple', 'pink']
append()
Adds a single element to the end of the list. This is O(1) amortized — very fast.
stack = []
stack.append("first")
stack.append("second")
stack.append("third")
print(stack) # ['first', 'second', 'third']
insert()
Inserts an element at a specific index. Everything after that index shifts right.
letters = ["a", "c", "d"]
letters.insert(1, "b") # insert 'b' at index 1
print(letters) # ['a', 'b', 'c', 'd']
# insert at the beginning
letters.insert(0, "z")
print(letters) # ['z', 'a', 'b', 'c', 'd']
extend()
Appends every element from an iterable to the list. Unlike append(), it unpacks the iterable.
first = [1, 2, 3]
second = [4, 5, 6]
first.extend(second)
print(first) # [1, 2, 3, 4, 5, 6]
# extend with any iterable
first.extend(range(7, 10))
print(first) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# compare: append vs extend
a = [1, 2]
a.append([3, 4]) # adds the list as a single element
print(a) # [1, 2, [3, 4]]
b = [1, 2]
b.extend([3, 4]) # adds each element
print(b) # [1, 2, 3, 4]
4. Removing Elements
Python gives you several ways to remove elements, each suited to a different scenario.
remove()
Removes the first occurrence of a value. Raises ValueError if not found.
fruits = ["apple", "banana", "cherry", "banana"]
fruits.remove("banana") # removes the FIRST 'banana'
print(fruits) # ['apple', 'cherry', 'banana']
# safe removal
try:
fruits.remove("mango")
except ValueError:
print("mango not in list")
pop()
Removes and returns an element by index. Defaults to the last element if no index is given.
numbers = [10, 20, 30, 40, 50]
# pop the last element
last = numbers.pop()
print(last) # 50
print(numbers) # [10, 20, 30, 40]
# pop at a specific index
second = numbers.pop(1)
print(second) # 20
print(numbers) # [10, 30, 40]
del Statement
Removes by index or slice. Does not return the removed value.
letters = ["a", "b", "c", "d", "e"]
# delete a single element
del letters[2]
print(letters) # ['a', 'b', 'd', 'e']
# delete a slice
del letters[1:3]
print(letters) # ['a', 'e']
# delete the entire list
del letters
# print(letters) # NameError: name 'letters' is not defined
clear()
Empties the list but keeps the list object itself.
items = [1, 2, 3, 4, 5]
items.clear()
print(items) # []
print(type(items)) # <class 'list'>
5. List Operations
Python lists support a handful of operators that make common tasks concise.
# Concatenation with +
list_a = [1, 2, 3]
list_b = [4, 5, 6]
combined = list_a + list_b
print(combined) # [1, 2, 3, 4, 5, 6]
# Repetition with *
zeros = [0] * 5
print(zeros) # [0, 0, 0, 0, 0]
pattern = [1, 2] * 3
print(pattern) # [1, 2, 1, 2, 1, 2]
# Membership with in
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # True
print("mango" in fruits) # False
print("mango" not in fruits) # True
# Length with len()
print(len(fruits)) # 3
# Count occurrences
numbers = [1, 2, 2, 3, 3, 3]
print(numbers.count(3)) # 3
# Find index of first occurrence
print(numbers.index(2)) # 1
6. Sorting
Sorting is one of the most common operations. Python gives you two choices: sort in place or return a new sorted list.
sort() — In-Place
Modifies the original list and returns None.
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
# ascending (default)
numbers.sort()
print(numbers) # [1, 1, 2, 3, 4, 5, 6, 9]
# descending
numbers.sort(reverse=True)
print(numbers) # [9, 6, 5, 4, 3, 2, 1, 1]
sorted() — Returns New List
Leaves the original untouched.
original = [3, 1, 4, 1, 5]
sorted_list = sorted(original)
print(original) # [3, 1, 4, 1, 5] — unchanged
print(sorted_list) # [1, 1, 3, 4, 5]
Custom Sorting with key
The key parameter accepts a function that extracts a comparison key from each element.
# sort strings by length
words = ["python", "is", "a", "powerful", "language"]
words.sort(key=len)
print(words) # ['a', 'is', 'python', 'powerful', 'language']
# sort by absolute value
numbers = [-5, 3, -1, 8, -2]
sorted_nums = sorted(numbers, key=abs)
print(sorted_nums) # [-1, -2, 3, -5, 8]
# sort dictionaries by a specific key
students = [
{"name": "Alice", "grade": 88},
{"name": "Bob", "grade": 95},
{"name": "Charlie", "grade": 72},
]
# sort by grade descending
students.sort(key=lambda s: s["grade"], reverse=True)
for s in students:
print(f"{s['name']}: {s['grade']}")
# Bob: 95
# Alice: 88
# Charlie: 72
# multi-key sort: by grade descending, then name ascending
students_v2 = [
{"name": "Alice", "grade": 88},
{"name": "Bob", "grade": 88},
{"name": "Charlie", "grade": 95},
]
students_v2.sort(key=lambda s: (-s["grade"], s["name"]))
for s in students_v2:
print(f"{s['name']}: {s['grade']}")
# Charlie: 95
# Alice: 88
# Bob: 88
7. List Comprehensions
List comprehensions are one of the most Pythonic features in the language. They let you create new lists by transforming and filtering existing iterables — all in a single, readable expression.
Basic Syntax
# syntax: [expression for item in iterable]
# squares of 0 through 9
squares = [x**2 for x in range(10)]
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# uppercase words
words = ["hello", "world", "python"]
upper = [w.upper() for w in words]
print(upper) # ['HELLO', 'WORLD', 'PYTHON']
# string formatting
names = ["alice", "bob", "charlie"]
formatted = [name.title() for name in names]
print(formatted) # ['Alice', 'Bob', 'Charlie']
With Conditions (Filtering)
# syntax: [expression for item in iterable if condition]
# even numbers only
evens = [x for x in range(20) if x % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# words longer than 4 characters
words = ["hi", "hello", "hey", "howdy", "greetings"]
long_words = [w for w in words if len(w) > 4]
print(long_words) # ['hello', 'howdy', 'greetings']
# filter and transform
numbers = [1, -2, 3, -4, 5, -6]
positive_squares = [x**2 for x in numbers if x > 0]
print(positive_squares) # [1, 9, 25]
If-Else in Comprehensions
When you need an else clause, the conditional goes before the for.
# syntax: [expr_if_true if condition else expr_if_false for item in iterable]
numbers = [1, -2, 3, -4, 5]
abs_values = [x if x >= 0 else -x for x in numbers]
print(abs_values) # [1, 2, 3, 4, 5]
labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even', 'odd']
Nested Comprehensions
# flatten a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# create a multiplication table
table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
for row in table:
print(row)
# [1, 2, 3, 4, 5]
# [2, 4, 6, 8, 10]
# [3, 6, 9, 12, 15]
# [4, 8, 12, 16, 20]
# [5, 10, 15, 20, 25]
# all pairs from two lists
colors = ["red", "blue"]
sizes = ["S", "M", "L"]
combinations = [(c, s) for c in colors for s in sizes]
print(combinations)
# [('red', 'S'), ('red', 'M'), ('red', 'L'), ('blue', 'S'), ('blue', 'M'), ('blue', 'L')]
Comprehensions vs map() and filter()
numbers = [1, 2, 3, 4, 5]
# map approach
squares_map = list(map(lambda x: x**2, numbers))
# comprehension approach — generally preferred
squares_comp = [x**2 for x in numbers]
print(squares_map) # [1, 4, 9, 16, 25]
print(squares_comp) # [1, 4, 9, 16, 25]
# filter approach
evens_filter = list(filter(lambda x: x % 2 == 0, numbers))
# comprehension approach
evens_comp = [x for x in numbers if x % 2 == 0]
print(evens_filter) # [2, 4]
print(evens_comp) # [2, 4]
# comprehensions are almost always more readable
# use map/filter when passing an existing function
result = list(map(str, numbers)) # this is clean enough
print(result) # ['1', '2', '3', '4', '5']
8. Nested Lists
Lists can contain other lists, which gives you an easy way to represent tables, grids, and matrices.
# 2D list (matrix)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# access element at row 1, column 2
print(matrix[1][2]) # 6
# access entire row
print(matrix[0]) # [1, 2, 3]
# access a column (no built-in syntax — use comprehension)
col_1 = [row[1] for row in matrix]
print(col_1) # [2, 5, 8]
# iterate over all elements
for i, row in enumerate(matrix):
for j, val in enumerate(row):
print(f"matrix[{i}][{j}] = {val}")
# create an m x n grid of zeros
rows, cols = 3, 4
grid = [[0] * cols for _ in range(rows)]
for row in grid:
print(row)
# [0, 0, 0, 0]
# [0, 0, 0, 0]
# [0, 0, 0, 0]
# WARNING: do NOT create a grid this way
bad_grid = [[0] * cols] * rows # all rows reference the SAME list!
bad_grid[0][0] = 99
print(bad_grid) # [[99, 0, 0, 0], [99, 0, 0, 0], [99, 0, 0, 0]] — oops!
Flattening a Nested List
nested = [[1, 2], [3, 4], [5, 6]]
# comprehension
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6]
# itertools approach for deeply nested structures
import itertools
flat_iter = list(itertools.chain.from_iterable(nested))
print(flat_iter) # [1, 2, 3, 4, 5, 6]
# recursive flattening for arbitrary depth
def flatten(lst):
result = []
for item in lst:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
deeply_nested = [1, [2, [3, [4, [5]]]]]
print(flatten(deeply_nested)) # [1, 2, 3, 4, 5]
9. Copying Lists
This is where many Python developers get burned. Understanding the difference between assignment, shallow copy, and deep copy is essential.
The Assignment Trap
original = [1, 2, 3]
alias = original # NOT a copy — both names point to the same list
alias.append(4)
print(original) # [1, 2, 3, 4] — original is modified!
print(alias is original) # True
Shallow Copy
Creates a new list, but inner objects are still shared references.
import copy
original = [1, 2, 3]
# three ways to shallow copy
copy1 = original.copy()
copy2 = original[:]
copy3 = list(original)
copy4 = copy.copy(original)
copy1.append(4)
print(original) # [1, 2, 3] — original is safe
print(copy1) # [1, 2, 3, 4]
# but with nested lists, shallow copy is not enough
nested = [[1, 2], [3, 4]]
shallow = nested.copy()
shallow[0][0] = 99 # modifies the inner list
print(nested) # [[99, 2], [3, 4]] — original is affected!
print(shallow) # [[99, 2], [3, 4]]
Deep Copy
Recursively copies everything. Use this when your list contains mutable objects.
import copy
nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0][0] = 99
print(nested) # [[1, 2], [3, 4]] — original is safe
print(deep) # [[99, 2], [3, 4]]
10. Built-in Functions for Lists
Python ships with many built-in functions that work beautifully with lists.
numbers = [10, 20, 30, 40, 50]
print(len(numbers)) # 5
print(sum(numbers)) # 150
print(min(numbers)) # 10
print(max(numbers)) # 50
# any() — True if at least one element is truthy
print(any([0, 0, 1])) # True
print(any([0, 0, 0])) # False
# all() — True if every element is truthy
print(all([1, 2, 3])) # True
print(all([1, 0, 3])) # False
# practical: check if all scores pass
scores = [75, 82, 91, 68, 88]
all_passing = all(s >= 60 for s in scores)
print(all_passing) # True
enumerate() — Index + Value
Stop writing for i in range(len(list)). Use enumerate instead.
fruits = ["apple", "banana", "cherry"]
# bad — do not do this
for i in range(len(fruits)):
print(i, fruits[i])
# good — use enumerate
for i, fruit in enumerate(fruits):
print(i, fruit)
# start counting from 1
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
zip() — Iterate in Parallel
names = ["Alice", "Bob", "Charlie"]
scores = [88, 95, 72]
grades = ["B+", "A", "C"]
for name, score, grade in zip(names, scores, grades):
print(f"{name}: {score} ({grade})")
# Alice: 88 (B+)
# Bob: 95 (A)
# Charlie: 72 (C)
# create a dictionary from two lists
name_score = dict(zip(names, scores))
print(name_score) # {'Alice': 88, 'Bob': 95, 'Charlie': 72}
# unzip with zip(*...)
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
nums, letters = zip(*pairs)
print(nums) # (1, 2, 3)
print(letters) # ('a', 'b', 'c')
11. Stacks and Queues with Lists
Lists can serve double duty as stacks and (with the right tool) queues.
Stack (LIFO)
Use append() to push and pop() to pop. Both are O(1).
stack = []
# push
stack.append("page_1")
stack.append("page_2")
stack.append("page_3")
print(stack) # ['page_1', 'page_2', 'page_3']
# pop
current = stack.pop()
print(current) # page_3
print(stack) # ['page_1', 'page_2']
# peek (look without removing)
if stack:
print(stack[-1]) # page_2
# practical: bracket matching
def is_balanced(s):
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char)
elif char in ')]}':
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
return len(stack) == 0
print(is_balanced("({[]})")) # True
print(is_balanced("({[}])")) # False
print(is_balanced("((())")) # False
Queue (FIFO)
Do not use list.pop(0) for queues — it is O(n) because every element must shift. Use collections.deque instead.
from collections import deque
queue = deque()
# enqueue
queue.append("task_1")
queue.append("task_2")
queue.append("task_3")
print(queue) # deque(['task_1', 'task_2', 'task_3'])
# dequeue
next_task = queue.popleft() # O(1)
print(next_task) # task_1
print(queue) # deque(['task_2', 'task_3'])
# practical: process tasks
while queue:
task = queue.popleft()
print(f"Processing: {task}")
# Processing: task_2
# Processing: task_3
12. Practical Examples
Let us put it all together with four real-world examples.
Example 1: Todo List Manager
class TodoList:
def __init__(self):
self.tasks = []
def add(self, task):
self.tasks.append({"task": task, "done": False})
print(f"Added: {task}")
def complete(self, index):
if 0 <= index < len(self.tasks):
self.tasks[index]["done"] = True
print(f"Completed: {self.tasks[index]['task']}")
else:
print("Invalid task index")
def remove(self, index):
if 0 <= index < len(self.tasks):
removed = self.tasks.pop(index)
print(f"Removed: {removed['task']}")
else:
print("Invalid task index")
def show(self):
if not self.tasks:
print("No tasks!")
return
for i, t in enumerate(self.tasks):
status = "done" if t["done"] else "pending"
print(f" {i}. [{status}] {t['task']}")
def pending(self):
return [t for t in self.tasks if not t["done"]]
def completed(self):
return [t for t in self.tasks if t["done"]]
todo = TodoList()
todo.add("Write Python tutorial")
todo.add("Review pull request")
todo.add("Deploy to production")
todo.complete(0)
todo.show()
# 0. [done] Write Python tutorial
# 1. [pending] Review pull request
# 2. [pending] Deploy to production
print(f"Pending: {len(todo.pending())}")
print(f"Completed: {len(todo.completed())}")
Example 2: Matrix Operations
def print_matrix(m, label=""):
if label:
print(f"{label}:")
for row in m:
print(f" {row}")
print()
def matrix_add(a, b):
"""Add two matrices element-wise."""
return [
[a[i][j] + b[i][j] for j in range(len(a[0]))]
for i in range(len(a))
]
def matrix_transpose(m):
"""Transpose a matrix (swap rows and columns)."""
return [[row[i] for row in m] for i in range(len(m[0]))]
def matrix_multiply(a, b):
"""Multiply two matrices."""
rows_a, cols_a = len(a), len(a[0])
rows_b, cols_b = len(b), len(b[0])
assert cols_a == rows_b, "Incompatible dimensions"
return [
[sum(a[i][k] * b[k][j] for k in range(cols_a)) for j in range(cols_b)]
for i in range(rows_a)
]
# usage
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
print_matrix(matrix_add(A, B), "A + B")
# A + B:
# [6, 8]
# [10, 12]
print_matrix(matrix_transpose(A), "Transpose of A")
# Transpose of A:
# [1, 3]
# [2, 4]
print_matrix(matrix_multiply(A, B), "A x B")
# A x B:
# [19, 22]
# [43, 50]
Example 3: Data Filtering Pipeline
employees = [
{"name": "Alice", "dept": "Engineering", "salary": 95000, "years": 5},
{"name": "Bob", "dept": "Marketing", "salary": 65000, "years": 3},
{"name": "Charlie", "dept": "Engineering", "salary": 110000, "years": 8},
{"name": "Diana", "dept": "Engineering", "salary": 88000, "years": 2},
{"name": "Eve", "dept": "Marketing", "salary": 72000, "years": 6},
{"name": "Frank", "dept": "Sales", "salary": 58000, "years": 1},
]
# pipeline: filter -> transform -> sort
# step 1: engineers only
engineers = [e for e in employees if e["dept"] == "Engineering"]
# step 2: add a seniority level
for eng in engineers:
eng["level"] = "Senior" if eng["years"] >= 5 else "Mid" if eng["years"] >= 3 else "Junior"
# step 3: sort by salary descending
engineers.sort(key=lambda e: e["salary"], reverse=True)
# step 4: format output
report = [
f"{e['name']} ({e['level']}) - ${e['salary']:,}"
for e in engineers
]
for line in report:
print(line)
# Charlie (Senior) - $110,000
# Alice (Senior) - $95,000
# Diana (Junior) - $88,000
# summary stats
salaries = [e["salary"] for e in engineers]
print(f"\nAverage salary: ${sum(salaries) / len(salaries):,.0f}")
print(f"Total headcount: {len(engineers)}")
Example 4: Simple Inventory System
class Inventory:
def __init__(self):
self.items = []
def add_item(self, name, quantity, price):
# check if item already exists
for item in self.items:
if item["name"].lower() == name.lower():
item["quantity"] += quantity
print(f"Updated {name}: quantity is now {item['quantity']}")
return
self.items.append({"name": name, "quantity": quantity, "price": price})
print(f"Added {name}: {quantity} units at ${price:.2f} each")
def remove_item(self, name, quantity=1):
for item in self.items:
if item["name"].lower() == name.lower():
if item["quantity"] >= quantity:
item["quantity"] -= quantity
if item["quantity"] == 0:
self.items.remove(item)
print(f"Removed {name} from inventory")
else:
print(f"Removed {quantity} {name}(s). Remaining: {item['quantity']}")
else:
print(f"Not enough {name} in stock (have {item['quantity']})")
return
print(f"{name} not found in inventory")
def search(self, keyword):
return [item for item in self.items if keyword.lower() in item["name"].lower()]
def total_value(self):
return sum(item["quantity"] * item["price"] for item in self.items)
def low_stock(self, threshold=5):
return [item for item in self.items if item["quantity"] <= threshold]
def report(self):
if not self.items:
print("Inventory is empty")
return
print(f"{'Item':<20} {'Qty':>5} {'Price':>8} {'Value':>10}")
print("-" * 45)
for item in sorted(self.items, key=lambda x: x["name"]):
value = item["quantity"] * item["price"]
print(f"{item['name']:<20} {item['quantity']:>5} ${item['price']:>7.2f} ${value:>9.2f}")
print("-" * 45)
print(f"{'Total':<26} {'':<8} ${self.total_value():>9.2f}")
inv = Inventory()
inv.add_item("Laptop", 10, 999.99)
inv.add_item("Mouse", 50, 29.99)
inv.add_item("Keyboard", 30, 79.99)
inv.add_item("Monitor", 3, 349.99)
inv.report()
print(f"\nLow stock items: {[i['name'] for i in inv.low_stock()]}")
13. Common Pitfalls
These are the mistakes that trip up developers at every level. Learn them once and you will save yourself hours of debugging.
Pitfall 1: Mutable Default Arguments
# BAD — the default list is shared across all calls
def add_item_bad(item, items=[]):
items.append(item)
return items
print(add_item_bad("a")) # ['a']
print(add_item_bad("b")) # ['a', 'b'] — not ['b']!
print(add_item_bad("c")) # ['a', 'b', 'c'] — keeps growing
# GOOD — use None as default
def add_item_good(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item_good("a")) # ['a']
print(add_item_good("b")) # ['b'] — correct!
Pitfall 2: Modifying a List While Iterating
# BAD — skips elements
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
if num % 2 == 0:
numbers.remove(num)
print(numbers) # [1, 3, 5] — might look right, but try with [2, 4, 6, 8]
# GOOD — iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]:
if num % 2 == 0:
numbers.remove(num)
print(numbers) # [1, 3, 5]
# BEST — use a list comprehension
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers) # [1, 3, 5]
Pitfall 3: Shallow Copy Surprise
import copy
# shallow copy does NOT copy nested objects
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0][0] = 99
print(original) # [[99, 2], [3, 4]] — original is affected!
# always use deepcopy for nested structures
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # [[1, 2], [3, 4]] — safe
Pitfall 4: Using * to Create Nested Lists
# BAD — all rows share the same inner list
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
# GOOD — use a comprehension
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
14. Best Practices
Follow these habits and your list code will be clean, fast, and easy to maintain.
# 1. Prefer list comprehensions over manual loops
# bad
result = []
for x in range(10):
if x % 2 == 0:
result.append(x ** 2)
# good
result = [x ** 2 for x in range(10) if x % 2 == 0]
# 2. Use enumerate() instead of range(len(...))
# bad
for i in range(len(items)):
print(i, items[i])
# good
for i, item in enumerate(items):
print(i, item)
# 3. Use zip() to iterate over multiple lists
# bad
for i in range(len(names)):
print(names[i], scores[i])
# good
for name, score in zip(names, scores):
print(name, score)
# 4. Use unpacking
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# 5. Prefer 'in' over index checking
# bad
found = False
for item in items:
if item == target:
found = True
break
# good
found = target in items
# 6. Use list.sort() when you do not need the original
# sort() is slightly faster than sorted() because it does not create a new list
data = [3, 1, 4, 1, 5]
data.sort() # in-place, no new list created
# 7. Use collections.deque for queue operations
from collections import deque
queue = deque() # O(1) append and popleft
15. Key Takeaways
- Lists are ordered, mutable, and allow duplicates. They are your default choice when you need a collection of items.
- Indexing starts at 0. Use negative indices to count from the end. Slicing is powerful — learn
[start:stop:step]and you will use it everywhere. - append() and pop() are O(1). insert(0, ...) and pop(0) are O(n). Use deque when you need fast operations at both ends.
- List comprehensions are Pythonic. They are faster and more readable than manual for loops with append. Use them liberally.
- Know the difference between shallow and deep copy. Assignment creates an alias, not a copy. Use
copy.deepcopy()for nested structures. - Never use a mutable default argument. Use
Noneas the default and create the list inside the function. - Never modify a list while iterating over it. Use a comprehension or iterate over a copy.
- Use enumerate() and zip() instead of index-based loops. Your code will be shorter and less error-prone.
- sort() mutates, sorted() returns a new list. Use the
keyparameter with lambdas for custom sort orders. - Lists are versatile enough to serve as stacks. For queues, reach for
collections.dequeinstead.
Lists are fundamental to Python programming. Master them and you have a solid foundation for everything else — from data processing to algorithms to web development. Practice the examples in this tutorial, experiment with variations, and you will have list fluency in no time.