Python – Lists & List Comprehensions

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 None as 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 key parameter with lambdas for custom sort orders.
  • Lists are versatile enough to serve as stacks. For queues, reach for collections.deque instead.

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.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *