Module 4.1

Lists and Tuples

Think of a Python list like a train with connected cargo boxes. Each box holds one piece of data, boxes are numbered from 0, and you can add, remove, or rearrange boxes anytime. Tuples are similar, but the boxes are welded shut - once created, they cannot be changed.

45 min
Beginner
Hands-on
What You'll Learn
  • Create and access list elements
  • Positive and negative indexing
  • Essential list methods (append, insert, remove)
  • List comprehensions for concise code
  • Tuples and mutability concepts
Contents
01

Introduction to Lists

A list in Python is an ordered, mutable collection that can hold items of any type. Lists are one of the most versatile and commonly used data structures in Python, perfect for storing sequences of related data.

Key Concept

What is a List?

A list is a built-in Python data structure that stores an ordered sequence of elements. Lists are defined using square brackets [] with elements separated by commas. Unlike arrays in some languages, Python lists can contain mixed data types.

Why it matters: Lists are the foundation for handling collections of data in Python. From storing user inputs to processing datasets, lists are essential for almost every Python program.

Creating Lists

There are several ways to create lists in Python. The most common method uses square bracket notation. You can create empty lists, lists with initial values, or convert other iterables to lists.

# Creating lists with square brackets
fruits = ["apple", "banana", "cherry"]
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True]  # Mixed types allowed

# Empty list creation
empty1 = []
empty2 = list()

# List from other iterables
chars = list("hello")  # ['h', 'e', 'l', 'l', 'o']
range_list = list(range(5))  # [0, 1, 2, 3, 4]

print(fruits)  # ['apple', 'banana', 'cherry']

What this code does: Demonstrates multiple ways to create lists. Square brackets are the most common approach. The list() constructor can convert strings, ranges, and other iterables into lists.

List Characteristics

Understanding the key characteristics of lists helps you use them effectively. Lists are ordered (elements maintain their position), mutable (can be changed after creation), and allow duplicate values.

Ordered

Elements have a defined order that does not change. New items are added at the end by default.

Mutable

You can add, remove, or change elements after the list is created without creating a new list.

Allows Duplicates

Lists can contain the same value multiple times. Each occurrence is stored separately.

# List characteristics demonstration
colors = ["red", "blue", "red", "green"]  # Duplicates allowed

# Check list length
print(len(colors))  # 4

# Check if item exists
print("blue" in colors)  # True
print("yellow" in colors)  # False

# Lists can be nested
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(matrix[1][2])  # 6 (row 1, column 2)

What this code does: Shows practical list operations: checking length with len(), testing membership with in, and accessing nested lists using chained indexing.

02

Indexing and Access

Every element in a list has a position called an index. Python uses zero-based indexing, meaning the first element is at index 0. Understanding positive and negative indexing is crucial for working with lists effectively.

List Indexing Visualization
fruits = [...]
"apple"
"banana"
"cherry"
"date"
"elder"
0
1
2
3
4
-5
-4
-3
-2
-1
Positive: Count from start (0)
Negative: Count from end (-1)

Positive Indexing

Positive indices start at 0 for the first element and increase by 1 for each subsequent element. The last element has an index equal to the length minus one.

# Positive indexing (starts at 0)
fruits = ["apple", "banana", "cherry", "date", "elder"]

print(fruits[0])   # 'apple' - first element
print(fruits[1])   # 'banana' - second element
print(fruits[4])   # 'elder' - fifth element (last)

# Accessing with variable index
index = 2
print(fruits[index])  # 'cherry'

# IndexError if out of range
# print(fruits[5])  # IndexError: list index out of range

What this code does: Demonstrates positive indexing starting from 0. Accessing an index beyond the list length raises an IndexError.

Negative Indexing

Negative indices count from the end of the list. Index -1 refers to the last element, -2 to the second-to-last, and so on. This is especially useful when you need to access elements from the end without knowing the list length.

# Negative indexing (starts at -1 from end)
fruits = ["apple", "banana", "cherry", "date", "elder"]

print(fruits[-1])  # 'elder' - last element
print(fruits[-2])  # 'date' - second to last
print(fruits[-5])  # 'apple' - first element

# Practical use: get last item without knowing length
scores = [85, 92, 78, 96, 88]
latest_score = scores[-1]
print(f"Latest score: {latest_score}")  # Latest score: 88

What this code does: Shows how negative indexing provides convenient access from the end. Using [-1] is cleaner than [len(list)-1] for getting the last element.

Modifying Elements

Since lists are mutable, you can change any element by assigning a new value to a specific index. This updates the list in place without creating a new list.

# Modifying list elements
colors = ["red", "green", "blue"]
print(f"Before: {colors}")  # ['red', 'green', 'blue']

# Change single element
colors[1] = "yellow"
print(f"After: {colors}")  # ['red', 'yellow', 'blue']

# Change using negative index
colors[-1] = "purple"
print(f"Final: {colors}")  # ['red', 'yellow', 'purple']

What this code does: Demonstrates in-place modification of list elements using index assignment. The original list object is modified, not replaced.

Practice: Indexing

Task: Given a list temperatures = [72, 68, 75, 80, 77, 82, 79], print the first temperature (Monday) and the last temperature (Sunday) using appropriate indexing.

Show Solution
temperatures = [72, 68, 75, 80, 77, 82, 79]

# Access first element (index 0)
monday_temp = temperatures[0]

# Access last element (index -1)
sunday_temp = temperatures[-1]

print(f"Monday: {monday_temp}")   # Monday: 72
print(f"Sunday: {sunday_temp}")   # Sunday: 79

Task: Write code to swap the first and last elements of items = ["start", "middle1", "middle2", "end"]. Print the result to verify the swap.

Show Solution
items = ["start", "middle1", "middle2", "end"]
print(f"Before: {items}")

# Swap using tuple unpacking
items[0], items[-1] = items[-1], items[0]

print(f"After: {items}")
# After: ['end', 'middle1', 'middle2', 'start']

Task: Given values = [45, 23, 67, 12, 89, 34], find the minimum value and its index, then replace that minimum value with 0. Print the index, original min, and the modified list.

Show Solution
values = [45, 23, 67, 12, 89, 34]

# Find minimum and its index
min_val = min(values)
min_idx = values.index(min_val)

print(f"Minimum: {min_val} at index {min_idx}")

# Replace minimum with 0
values[min_idx] = 0

print(f"Modified: {values}")
# Modified: [45, 23, 67, 0, 89, 34]
03

List Methods

Python lists come with powerful built-in methods for adding, removing, and manipulating elements. These methods modify the list in place and are essential tools for working with collections.

Method Description Returns Example
append(x) Add item to end None lst.append(5)
insert(i, x) Insert item at index None lst.insert(0, 5)
extend(iter) Add all items from iterable None lst.extend([1,2])
remove(x) Remove first occurrence None lst.remove(5)
pop(i) Remove and return item Removed item lst.pop()
index(x) Find index of item Integer index lst.index(5)
count(x) Count occurrences Integer count lst.count(5)
sort() Sort list in place None lst.sort()
reverse() Reverse list in place None lst.reverse()
clear() Remove all items None lst.clear()

Adding Elements

Python provides three main methods for adding elements: append() for single items at the end, insert() for specific positions, and extend() for multiple items.

# Adding elements to lists
tasks = ["email", "meeting"]

# append() - add to end
tasks.append("review")
print(tasks)  # ['email', 'meeting', 'review']

# insert() - add at specific index
tasks.insert(0, "coffee")
print(tasks)  # ['coffee', 'email', 'meeting', 'review']

# extend() - add multiple items
tasks.extend(["lunch", "coding"])
print(tasks)  # ['coffee', 'email', 'meeting', 'review', 'lunch', 'coding']

What this code does: Shows three ways to add elements. append() is fastest for adding single items. insert() shifts existing elements. extend() unpacks iterables to add each item.

Common Mistake: Using append() with a list adds the entire list as one element: [1, 2].append([3, 4]) gives [1, 2, [3, 4]]. Use extend() instead to add individual items.

Removing Elements

Remove elements using remove() by value, pop() by index (returns the removed item), or clear() to empty the entire list.

# Removing elements from lists
colors = ["red", "green", "blue", "green", "yellow"]

# remove() - removes first occurrence by value
colors.remove("green")
print(colors)  # ['red', 'blue', 'green', 'yellow']

# pop() - removes by index and returns the value
removed = colors.pop(1)
print(f"Removed: {removed}")  # Removed: blue
print(colors)  # ['red', 'green', 'yellow']

# pop() without index removes last item
last = colors.pop()
print(f"Last: {last}")  # Last: yellow

What this code does: Demonstrates removal methods. remove() raises ValueError if item not found. pop() raises IndexError if index is invalid.

Sorting and Searching

Lists can be sorted in place with sort() or reversed with reverse(). Use index() to find element positions and count() to count occurrences.

# Sorting and searching
numbers = [64, 25, 12, 22, 11, 25]

# Sort in ascending order
numbers.sort()
print(numbers)  # [11, 12, 22, 25, 25, 64]

# Sort in descending order
numbers.sort(reverse=True)
print(numbers)  # [64, 25, 25, 22, 12, 11]

# Find index and count
print(numbers.index(25))  # 1 (first occurrence)
print(numbers.count(25))  # 2 (appears twice)

What this code does: Shows in-place sorting with optional reverse parameter. index() returns the position of the first match. count() returns how many times a value appears.

sort() vs sorted(): list.sort() modifies the list in place and returns None. sorted(list) returns a new sorted list, leaving the original unchanged.

Practice: List Methods

Task: Start with an empty list. Add "milk" using append. Then add "bread", "eggs", and "butter" all at once using extend. Print the final list.

Show Solution
shopping = []

# Add single item
shopping.append("milk")

# Add multiple items
shopping.extend(["bread", "eggs", "butter"])

print(shopping)
# ['milk', 'bread', 'eggs', 'butter']

Task: Given votes = ["yes", "no", "yes", "yes", "no", "yes"], remove ALL occurrences of "no" using a loop. Print the count of "yes" votes remaining.

Show Solution
votes = ["yes", "no", "yes", "yes", "no", "yes"]

# Remove all "no" votes
while "no" in votes:
    votes.remove("no")

print(votes)  # ['yes', 'yes', 'yes', 'yes']
print(f"Yes votes: {votes.count('yes')}")  # Yes votes: 4

Task: Create a stack (LIFO - Last In First Out). Push values 10, 20, 30 onto the stack. Pop two values and print each. Finally print the remaining stack contents.

Show Solution
stack = []

# Push operations (append adds to end/top)
stack.append(10)
stack.append(20)
stack.append(30)
print(f"Stack after push: {stack}")  # [10, 20, 30]

# Pop operations (pop removes from end/top)
top1 = stack.pop()
print(f"Popped: {top1}")  # Popped: 30

top2 = stack.pop()
print(f"Popped: {top2}")  # Popped: 20

print(f"Remaining: {stack}")  # Remaining: [10]

Task: Given words = ["python", "is", "an", "amazing", "programming", "language"], sort the list by word length (shortest first). Print the sorted list.

Show Solution
words = ["python", "is", "an", "amazing", "programming", "language"]

# Sort by length using key parameter
words.sort(key=len)

print(words)
# ['is', 'an', 'python', 'amazing', 'language', 'programming']
04

Tuples and Mutability

While lists are mutable, Python also provides tuples - immutable sequences. Understanding the difference between mutable and immutable data structures is fundamental to writing correct and efficient Python code.

Mutable vs Immutable
MUTABLE (Lists)
my_list = [1, 2, 3]
my_list[0] = 99
[99, 2, 3]

Same object, modified in place

IMMUTABLE (Tuples)
my_tuple = (1, 2, 3)
my_tuple[0] = 99
TypeError!

Cannot modify after creation

Creating and Using Tuples

Tuples are created using parentheses () or simply by separating values with commas. They support indexing and slicing just like lists, but cannot be modified after creation.

# Creating tuples
point = (10, 20)
rgb = (255, 128, 0)
single = (42,)  # Single element tuple needs trailing comma

# Tuple without parentheses (tuple packing)
coords = 3.5, 7.2, 9.1

# Accessing elements (same as lists)
print(point[0])   # 10
print(rgb[-1])    # 0

# Tuple unpacking
x, y = point
print(f"x={x}, y={y}")  # x=10, y=20

What this code does: Demonstrates tuple creation and unpacking. Note the trailing comma for single-element tuples. Tuple unpacking assigns each element to a separate variable.

Why Use Tuples?

Tuples have specific use cases where immutability is beneficial. They are faster than lists, can be used as dictionary keys, and signal that data should not be changed.

Faster

Tuples are slightly faster than lists due to their immutability.

Dict Keys

Tuples can be used as dictionary keys; lists cannot.

Data Safety

Prevents accidental modification of data that should stay constant.

Return Values

Functions often return multiple values as tuples.

# Practical tuple uses
# 1. Multiple return values
def get_min_max(numbers):
    return min(numbers), max(numbers)  # Returns tuple

data = [5, 2, 8, 1, 9]
minimum, maximum = get_min_max(data)
print(f"Range: {minimum} to {maximum}")  # Range: 1 to 9

# 2. Tuple as dictionary key
locations = {
    (40.7128, -74.0060): "New York",
    (51.5074, -0.1278): "London"
}
print(locations[(40.7128, -74.0060)])  # New York

What this code does: Shows practical tuple applications. Functions can return multiple values as tuples. Coordinate pairs work well as dictionary keys since tuples are hashable.

Converting Between Lists and Tuples

You can convert between lists and tuples using the list() and tuple() constructors. This is useful when you need to modify tuple data or freeze list data.

# Converting between lists and tuples
my_list = [1, 2, 3, 4, 5]
my_tuple = (10, 20, 30)

# List to tuple (freeze the data)
frozen = tuple(my_list)
print(frozen)  # (1, 2, 3, 4, 5)

# Tuple to list (allow modifications)
editable = list(my_tuple)
editable.append(40)
print(editable)  # [10, 20, 30, 40]

What this code does: Shows type conversion between lists and tuples. Convert to tuple when you want to prevent changes; convert to list when you need to modify the data.

Practice: Tuples

Task: Given person = ("Alice", 28, "Engineer"), unpack the tuple into variables name, age, and job. Print a formatted string with all three values.

Show Solution
person = ("Alice", 28, "Engineer")

# Tuple unpacking
name, age, job = person

print(f"{name} is {age} years old and works as an {job}")
# Alice is 28 years old and works as an Engineer

Task: Write a function calculate_stats(numbers) that takes a list of numbers and returns a tuple containing (sum, average, count). Test with [10, 20, 30, 40, 50].

Show Solution
def calculate_stats(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return total, average, count  # Returns tuple

# Test the function
data = [10, 20, 30, 40, 50]
total, avg, cnt = calculate_stats(data)

print(f"Sum: {total}, Avg: {avg}, Count: {cnt}")
# Sum: 150, Avg: 30.0, Count: 5

Task: Create a dictionary mapping (row, col) coordinates to chess piece names. Add at least 4 pieces (e.g., (0,0):"Rook", (0,4):"King"). Write code to look up what piece is at (0, 4).

Show Solution
chess_board = {
    (0, 0): "Rook",
    (0, 1): "Knight", 
    (0, 4): "King",
    (0, 7): "Rook",
    (1, 0): "Pawn"
}

# Look up piece at position
pos = (0, 4)
piece = chess_board.get(pos, "Empty")
print(f"Position {pos}: {piece}")  # Position (0, 4): King
05

List Comprehensions

List comprehensions provide a concise way to create lists based on existing sequences. They combine loops and conditional logic into a single readable line, making your code more Pythonic and often faster.

Syntax

List Comprehension Structure

[expression for item in iterable if condition]

Components: expression - what to include in new list; item - variable for each element; iterable - source sequence; if condition - optional filter.

Basic List Comprehensions

The simplest comprehension applies an expression to each element in an iterable. This replaces multi-line loops with a single expressive line.

# Traditional loop approach
squares_loop = []
for x in range(1, 6):
    squares_loop.append(x ** 2)
print(squares_loop)  # [1, 4, 9, 16, 25]

# List comprehension (same result)
squares_comp = [x ** 2 for x in range(1, 6)]
print(squares_comp)  # [1, 4, 9, 16, 25]

# Transform strings
names = ["alice", "bob", "charlie"]
upper_names = [name.upper() for name in names]
print(upper_names)  # ['ALICE', 'BOB', 'CHARLIE']

What this code does: Compares traditional loop with list comprehension. Both create identical results, but comprehensions are more concise and often faster due to optimization.

Comprehensions with Conditions

Add an if clause to filter elements. Only items that satisfy the condition are included in the resulting list.

# Filter with condition
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Get only even numbers
evens = [n for n in numbers if n % 2 == 0]
print(evens)  # [2, 4, 6, 8, 10]

# Get numbers greater than 5
big_nums = [n for n in numbers if n > 5]
print(big_nums)  # [6, 7, 8, 9, 10]

# Combine transform and filter
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
print(even_squares)  # [4, 16, 36, 64, 100]

What this code does: Shows filtering with conditions. The if clause acts as a filter, and the expression before for transforms included elements.

Nested Comprehensions

List comprehensions can be nested for working with multi-dimensional data. The outer loop comes first, followed by inner loops.

# 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 multiplication table
table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(table)  # [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

# Filter from nested structure
words = [["hello", "hi"], ["bye", "goodbye"]]
long_words = [w for group in words for w in group if len(w) > 3]
print(long_words)  # ['hello', 'goodbye']

What this code does: Demonstrates nested comprehensions. Read order matches nested loops: outer loop first, then inner loops, then the condition.

Readability Tip: If a comprehension becomes too complex (multiple conditions, nested loops), consider using a traditional for loop instead. Code clarity is more important than brevity.

Practice: List Comprehensions

Task: Use a list comprehension to create a list of cubes from 1 to 10 (1, 8, 27, ..., 1000). Print the result.

Show Solution
# Create cubes using list comprehension
cubes = [x ** 3 for x in range(1, 11)]
print(cubes)
# [1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]

Task: Given words = ["cat", "elephant", "dog", "hippopotamus", "ant", "butterfly"], create a new list containing only words with more than 5 characters, converted to uppercase.

Show Solution
words = ["cat", "elephant", "dog", "hippopotamus", "ant", "butterfly"]

# Filter and transform in one comprehension
long_words = [w.upper() for w in words if len(w) > 5]

print(long_words)
# ['ELEPHANT', 'HIPPOPOTAMUS', 'BUTTERFLY']

Task: Given sentence = "List comprehensions are powerful", use a list comprehension to extract all vowels (a, e, i, o, u) in lowercase. Print the list and the count of vowels.

Show Solution
sentence = "List comprehensions are powerful"
vowels = "aeiouAEIOU"

# Extract vowels, convert to lowercase
found = [c.lower() for c in sentence if c in vowels]

print(found)
# ['i', 'o', 'e', 'e', 'i', 'o', 'a', 'e', 'o', 'e', 'u']
print(f"Total vowels: {len(found)}")  # Total vowels: 11

Task: Given nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]], create a flat list containing only odd numbers. Use a single list comprehension.

Show Solution
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

# Flatten and filter for odd numbers
odds = [n for sublist in nested for n in sublist if n % 2 != 0]

print(odds)  # [1, 3, 5, 7, 9]
06

Advanced List Operations

Master advanced techniques including nested lists, list copying behaviors, memory management, and performance considerations to write efficient and bug-free code.

Nested Lists and Multidimensional Data

Lists can contain other lists, creating multidimensional structures perfect for matrices, tables, game boards, and complex data hierarchies. Access elements using multiple indices.

Concept

Nested List Structure

A nested list is a list where elements are themselves lists, creating a table-like or tree-like structure.

Access Pattern: matrix[row][col] - first index selects the row (inner list), second index selects the column (element within that list).

# Creating a 3x3 matrix (tic-tac-toe board)
board = [
    ["X", "O", "X"],
    ["O", "X", "O"],
    ["X", " ", "O"]
]

# Access specific cell
print(board[0][0])  # "X" (top-left)
print(board[2][1])  # " " (bottom-middle)

# Modify a cell
board[2][1] = "X"
print(board[2])  # ["X", "X", "O"]

# Iterate through all cells
for row_idx, row in enumerate(board):
    for col_idx, cell in enumerate(row):
        print(f"[{row_idx},{col_idx}] = {cell}")
# [0,0] = X
# [0,1] = O
# [0,2] = X
# [1,0] = O
# ...

What this code does: Creates a 2D list representing a game board. Uses double indexing [row][col] to access cells. enumerate() provides both index and value during iteration.

Creating Nested Lists with Comprehensions

List comprehensions can generate nested lists efficiently. Use nested loops within the comprehension to create multidimensional structures.

# Create a 4x3 matrix of zeros
zeros = [[0 for _ in range(3)] for _ in range(4)]
print(zeros)
# [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

# Create multiplication table (5x5)
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]

# Create identity matrix (3x3)
identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
print(identity)
# [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

What this code does: Demonstrates nested comprehensions for 2D list generation. The outer comprehension creates rows, inner comprehension creates columns. Identity matrix uses conditional expression.

Common Pitfall: Never use [[0] * 3] * 4 to create nested lists! This creates references to the same inner list. Modifying one row modifies all rows. Always use comprehensions: [[0]*3 for _ in range(4)].

Shallow vs Deep Copying

Understanding list copying is critical to avoid unexpected bugs. Python provides three ways to copy lists, each with different behaviors regarding nested structures.

Assignment (=)

Creates an alias. Both variables point to the same list object. Changes affect both.

Shallow Copy

Creates new list, but nested objects are shared. Use list.copy() or list[:].

Deep Copy

Recursively copies all nested objects. Use copy.deepcopy() for complete independence.

import copy

# Original nested list
original = [[1, 2], [3, 4]]

# 1. Assignment - creates alias
alias = original
alias[0][0] = 999
print(original)  # [[999, 2], [3, 4]] - CHANGED!

# 2. Shallow copy - outer list copied, inner lists shared
original = [[1, 2], [3, 4]]
shallow = original.copy()  # or original[:]
shallow[0][0] = 999
print(original)  # [[999, 2], [3, 4]] - inner list STILL SHARED!

# 3. Deep copy - complete independence
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 999
print(original)  # [[1, 2], [3, 4]] - UNCHANGED!

What this code does: Demonstrates three copying methods. Assignment shares memory. Shallow copy creates new outer list but shares inner lists. Deep copy creates completely independent structure.

List Performance Considerations

Different list operations have different time complexities. Understanding these helps you write efficient code, especially for large datasets.

Operation Example Time Complexity Explanation
Index access lst[5] O(1) Direct memory access, constant time
Append lst.append(x) O(1) Adds to end, amortized constant
Pop last lst.pop() O(1) Removes from end, no shifting
Insert lst.insert(0, x) O(n) Must shift all elements after insert point
Delete del lst[0] O(n) Must shift all elements after deletion
Search x in lst O(n) Must check each element sequentially
Copy lst.copy() O(n) Must copy every element
Sort lst.sort() O(n log n) Uses Timsort algorithm
Performance Tip: Prefer append() over insert(0, x) for building lists. If you need to add to the front frequently, consider using collections.deque which has O(1) append and prepend operations.

List Aliasing and Identity

Understanding the difference between equality (==) and identity (is) prevents subtle bugs, especially when working with nested structures or function parameters.

# Equality vs Identity
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1

# Equality (==) - compares values
print(list1 == list2)  # True (same content)
print(list1 == list3)  # True (same content)

# Identity (is) - compares memory addresses
print(list1 is list2)  # False (different objects)
print(list1 is list3)  # True (same object)

# Modifying through alias
list3.append(4)
print(list1)  # [1, 2, 3, 4] - changed!
print(list2)  # [1, 2, 3] - unchanged

# Function parameters are references
def modify_list(lst):
    lst.append(999)  # Modifies original!

my_list = [1, 2, 3]
modify_list(my_list)
print(my_list)  # [1, 2, 3, 999]

What this code does: Demonstrates the difference between equal values and identical objects. is checks if variables point to the same memory location. Function parameters are references, so modifications affect the original.

Memory-Efficient List Operations

For large datasets, use generators and itertools instead of creating intermediate lists. This saves memory and improves performance.

# Memory-inefficient: creates full list in memory
large_squares = [x**2 for x in range(1000000)]
total = sum(large_squares)  # Uses lots of memory

# Memory-efficient: generator expression
large_squares_gen = (x**2 for x in range(1000000))
total = sum(large_squares_gen)  # Uses minimal memory

# Processing in chunks
data = list(range(100))

# Inefficient: creates multiple intermediate lists
result = []
for x in data:
    if x % 2 == 0:
        result.append(x * 2)

# Better: single list comprehension
result = [x * 2 for x in data if x % 2 == 0]

# Best for large data: generator
result_gen = (x * 2 for x in data if x % 2 == 0)
for val in result_gen:  # Process one at a time
    pass  # Do something with val

What this code does: Compares memory usage approaches. Generator expressions use () instead of [] and produce values on demand. For data processing pipelines, generators are significantly more efficient.

Practice: Advanced Operations

Task: Use a nested list comprehension to create a 4x4 matrix containing numbers 1-16 in row-major order (first row: 1-4, second row: 5-8, etc.). Print the matrix nicely formatted.

Show Solution
# Create 4x4 matrix with sequential numbers
matrix = [[i*4 + j + 1 for j in range(4)] for i in range(4)]

# Print formatted
for row in matrix:
    print(row)
# [1, 2, 3, 4]
# [5, 6, 7, 8]
# [9, 10, 11, 12]
# [13, 14, 15, 16]

Task: Write a function flatten(nested) that recursively flattens a list of any depth. Test with [1, [2, [3, [4]], 5], 6] - should return [1, 2, 3, 4, 5, 6].

Show Solution
def flatten(nested):
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten(item))  # Recursive call
        else:
            result.append(item)
    return result

# Test
nested = [1, [2, [3, [4]], 5], 6]
flat = flatten(nested)
print(flat)  # [1, 2, 3, 4, 5, 6]

Task: Write a function rotate_90(matrix) that rotates a square matrix 90 degrees clockwise in-place. Test with [[1,2,3], [4,5,6], [7,8,9]] - should become [[7,4,1], [8,5,2], [9,6,3]].

Show Solution
def rotate_90(matrix):
    n = len(matrix)
    # Transpose
    for i in range(n):
        for j in range(i+1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
    # Reverse each row
    for i in range(n):
        matrix[i].reverse()

# Test
mat = [[1,2,3], [4,5,6], [7,8,9]]
rotate_90(mat)
for row in mat:
    print(row)
# [7, 4, 1]
# [8, 5, 2]
# [9, 6, 3]

Task: Write a function deep_copy(lst) that recursively copies a nested list without using the copy module. Test with [[1, 2], [3, [4, 5]]] - verify modifications don't affect the original.

Show Solution
def deep_copy(lst):
    if not isinstance(lst, list):
        return lst  # Base case: not a list
    return [deep_copy(item) for item in lst]

# Test
original = [[1, 2], [3, [4, 5]]]
copied = deep_copy(original)

# Modify copy
copied[1][1][0] = 999

print(original)  # [[1, 2], [3, [4, 5]]] - unchanged
print(copied)    # [[1, 2], [3, [999, 5]]] - modified

Interactive Visualizations

List Operations Visualizer

See how list operations work step-by-step with visual feedback. Experiment with different operations to understand their behavior.

Current List
1
2
3
4
5
Operations:
Operation Log
Ready - select an operation

Slicing Playground

Experiment with different slice parameters to understand how Python extracts sublists. Try positive and negative indices, steps, and edge cases.

Source List:
[10, 20, 30, 40, 50, 60, 70, 80, 90]
Result:
Click "Execute Slice" to see result
Syntax: list[start:stop:step]
Leave fields blank to use defaults (None for start/stop, 1 for step)
Quick Examples:

Comprehension Builder

Build list comprehensions interactively and see the resulting list in real-time. Understand how each component affects the output.

Build Your Comprehension:
Generates range(0, n)
Generated Code:
[x * 2 for x in range(10)]
Output:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Quick Templates:

Real-World Applications

Lists are fundamental to solving real programming problems. From managing user data to processing files and implementing algorithms, understanding practical list patterns makes you a more effective developer.

1. Data Processing Pipeline

Process and transform data through multiple stages using lists and comprehensions. This pattern is common in data analysis, ETL pipelines, and batch processing.

# Real-world data processing example
# Processing student test scores

raw_scores = [
    "Alice:85", "Bob:92", "Charlie:78", "Diana:95", 
    "Eve:88", "Frank:72", "Grace:91"
]

# Stage 1: Parse data
students = [score.split(":") for score in raw_scores]
# [['Alice', '85'], ['Bob', '92'], ...]

# Stage 2: Convert scores to integers
parsed = [[name, int(score)] for name, score in students]
# [['Alice', 85], ['Bob', 92], ...]

# Stage 3: Calculate letter grades
def get_grade(score):
    if score >= 90: return 'A'
    if score >= 80: return 'B'
    if score >= 70: return 'C'
    if score >= 60: return 'D'
    return 'F'

results = [[name, score, get_grade(score)] for name, score in parsed]

# Stage 4: Filter and report
top_students = [name for name, score, grade in results if grade in ['A', 'B']]
print(f"Honor roll: {', '.join(top_students)}")
# Honor roll: Alice, Bob, Diana, Eve, Grace

average = sum(score for _, score, _ in results) / len(results)
print(f"Class average: {average:.1f}")
# Class average: 85.9

Real-world use: This pattern appears in data ETL (Extract, Transform, Load) pipelines, log file processing, and CSV data analysis. Each stage transforms the data incrementally.

2. Shopping Cart Implementation

Lists naturally represent collections of items, making them perfect for shopping carts, inventory systems, and order management.

# Shopping cart with list operations
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity=1):
        """Add item to cart"""
        self.items.append({
            'name': name,
            'price': price,
            'quantity': quantity
        })
        print(f"Added {quantity}x {name}")
    
    def remove_item(self, name):
        """Remove item by name"""
        for item in self.items:
            if item['name'] == name:
                self.items.remove(item)
                print(f"Removed {name}")
                return
        print(f"{name} not found in cart")
    
    def update_quantity(self, name, new_quantity):
        """Update item quantity"""
        for item in self.items:
            if item['name'] == name:
                item['quantity'] = new_quantity
                print(f"Updated {name} to {new_quantity}")
                return
    
    def get_total(self):
        """Calculate total price"""
        return sum(item['price'] * item['quantity'] for item in self.items)
    
    def apply_discount(self, percent):
        """Apply discount to all items"""
        for item in self.items:
            item['price'] *= (1 - percent / 100)
    
    def get_summary(self):
        """Print cart summary"""
        print("\n--- Shopping Cart ---")
        for item in self.items:
            total = item['price'] * item['quantity']
            print(f"{item['name']}: ${item['price']:.2f} x {item['quantity']} = ${total:.2f}")
        print(f"Total: ${self.get_total():.2f}")

# Usage example
cart = ShoppingCart()
cart.add_item("Laptop", 999.99, 1)
cart.add_item("Mouse", 29.99, 2)
cart.add_item("Keyboard", 79.99, 1)
cart.update_quantity("Mouse", 3)
cart.apply_discount(10)  # 10% off
cart.get_summary()
# --- Shopping Cart ---
# Laptop: $899.99 x 1 = $899.99
# Mouse: $26.99 x 3 = $80.97
# Keyboard: $71.99 x 1 = $71.99
# Total: $1052.95

Real-world use: E-commerce platforms, point-of-sale systems, and mobile shopping apps use similar list-based structures to manage cart items with dynamic pricing and quantities.

3. Log File Analysis

Parse and analyze log files to extract insights, detect errors, and monitor system health. Lists excel at filtering and aggregating log entries.

# Analyzing web server access logs
logs = [
    "2026-01-22 10:15:23 INFO User login: alice@example.com",
    "2026-01-22 10:16:45 ERROR Database connection failed",
    "2026-01-22 10:17:12 INFO User login: bob@example.com",
    "2026-01-22 10:18:33 WARNING High memory usage: 85%",
    "2026-01-22 10:19:05 ERROR File not found: config.json",
    "2026-01-22 10:20:18 INFO User logout: alice@example.com",
    "2026-01-22 10:21:47 ERROR API timeout: /api/users",
    "2026-01-22 10:22:55 INFO User login: charlie@example.com"
]

# Extract error messages
errors = [log for log in logs if "ERROR" in log]
print(f"Found {len(errors)} errors:")
for err in errors:
    print(f"  - {err.split('ERROR')[1].strip()}")
# Found 3 errors:
#   - Database connection failed
#   - File not found: config.json
#   - API timeout: /api/users

# Count by severity
levels = ["INFO", "WARNING", "ERROR"]
counts = {level: sum(1 for log in logs if level in log) for level in levels}
print(f"\nLog summary: {counts}")
# Log summary: {'INFO': 4, 'WARNING': 1, 'ERROR': 3}

# Extract user activities
login_pattern = "User login:"
logins = [log.split(login_pattern)[1].strip() for log in logs if login_pattern in log]
print(f"\nLogins: {logins}")
# Logins: ['alice@example.com', 'bob@example.com', 'charlie@example.com']

# Time-based analysis
def get_hour(log_line):
    time = log_line.split()[1]
    return int(time.split(':')[0])

hourly_activity = {}
for log in logs:
    hour = get_hour(log)
    hourly_activity[hour] = hourly_activity.get(hour, 0) + 1

print(f"\nActivity by hour: {hourly_activity}")
# Activity by hour: {10: 8}

Real-world use: DevOps tools, monitoring systems, and security analysis platforms use similar patterns to process millions of log entries, detecting anomalies and generating alerts.

4. Task Scheduling System

Manage priority queues, deadlines, and task dependencies using sorted lists and custom comparison logic.

# Task management with priorities
tasks = [
    {"name": "Fix critical bug", "priority": 1, "hours": 3},
    {"name": "Write documentation", "priority": 3, "hours": 5},
    {"name": "Code review", "priority": 2, "hours": 2},
    {"name": "Deploy to production", "priority": 1, "hours": 1},
    {"name": "Team meeting", "priority": 2, "hours": 1},
    {"name": "Update dependencies", "priority": 3, "hours": 2}
]

# Sort by priority (ascending), then by hours (ascending)
sorted_tasks = sorted(tasks, key=lambda t: (t['priority'], t['hours']))

print("Task schedule (priority order):")
total_hours = 0
for task in sorted_tasks:
    total_hours += task['hours']
    print(f"P{task['priority']} - {task['name']} ({task['hours']}h) - Total: {total_hours}h")
# P1 - Deploy to production (1h) - Total: 1h
# P1 - Fix critical bug (3h) - Total: 4h
# P2 - Team meeting (1h) - Total: 5h
# P2 - Code review (2h) - Total: 7h
# P3 - Update dependencies (2h) - Total: 9h
# P3 - Write documentation (5h) - Total: 14h

# Find tasks that fit in 4-hour window
available_hours = 4
scheduled = []
remaining_hours = available_hours

for task in sorted_tasks:
    if task['hours'] <= remaining_hours:
        scheduled.append(task['name'])
        remaining_hours -= task['hours']

print(f"\nCan complete in {available_hours}h: {scheduled}")
# Can complete in 4h: ['Deploy to production', 'Fix critical bug']

# Group by priority
from itertools import groupby
by_priority = {
    priority: list(group) 
    for priority, group in groupby(sorted_tasks, key=lambda t: t['priority'])
}

for priority, tasks_list in by_priority.items():
    print(f"\nPriority {priority}: {len(tasks_list)} tasks")
    for task in tasks_list:
        print(f"  - {task['name']}")

Real-world use: Project management software, operating system schedulers, and workflow engines use priority-based task sorting to optimize resource allocation and meet deadlines.

5. Data Validation and Cleaning

Clean and validate user input, removing duplicates, handling missing values, and ensuring data consistency.

# Data cleaning pipeline
raw_emails = [
    "alice@example.com",
    "BOB@EXAMPLE.COM",
    "  charlie@test.com  ",
    "alice@example.com",  # duplicate
    "invalid-email",
    None,
    "",
    "diana@company.org",
    "Eve@Example.Com"  # duplicate (case-insensitive)
]

# Step 1: Remove None and empty strings
step1 = [email for email in raw_emails if email]
print(f"After removing empty: {len(step1)} items")

# Step 2: Strip whitespace and convert to lowercase
step2 = [email.strip().lower() for email in step1]
print(f"After normalization: {step2[:3]}")

# Step 3: Validate email format (simple check)
import re
email_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
step3 = [email for email in step2 if re.match(email_pattern, email)]
print(f"After validation: {len(step3)} valid emails")

# Step 4: Remove duplicates (preserve order)
seen = set()
step4 = []
for email in step3:
    if email not in seen:
        seen.add(email)
        step4.append(email)

print(f"After deduplication: {len(step4)} unique emails")
print(f"Final result: {step4}")
# Final result: ['alice@example.com', 'bob@example.com', 'charlie@test.com', 'diana@company.org', 'eve@example.com']

# Alternative: Using dict.fromkeys() for deduplication
unique_emails = list(dict.fromkeys(step3))
print(f"Alternative method: {unique_emails}")

Real-world use: Form validation, data import tools, and ETL pipelines use multi-stage cleaning to ensure data quality before processing or storage.

Common List Patterns

Master these proven patterns to solve recurring problems elegantly. These idioms appear frequently in production code and make you a more effective Python developer.

Pattern 1: Finding Elements

Efficiently search for specific elements or conditions within lists using various techniques.

# Finding patterns
numbers = [10, 25, 30, 15, 45, 20, 35]

# Find first element matching condition
def find_first(lst, condition):
    for item in lst:
        if condition(item):
            return item
    return None

first_over_30 = find_first(numbers, lambda x: x > 30)
print(first_over_30)  # 35

# Find all matching elements
all_over_25 = [x for x in numbers if x > 25]
print(all_over_25)  # [30, 45, 35]

# Find index of element
try:
    idx = numbers.index(30)
    print(f"Found 30 at index {idx}")  # Found 30 at index 2
except ValueError:
    print("Not found")

# Find all indices matching condition
indices = [i for i, x in enumerate(numbers) if x > 25]
print(f"Indices of values > 25: {indices}")  # [2, 4, 6]

# Check if any/all elements match
has_large = any(x > 40 for x in numbers)
all_positive = all(x > 0 for x in numbers)
print(f"Has large: {has_large}, All positive: {all_positive}")
# Has large: True, All positive: True

Pattern use: Finding elements is essential for search features, data validation, and filtering operations in web applications and data processing.

Pattern 2: Aggregation and Reduction

Combine list elements into summary values using sum, max, min, and custom reduction operations.

# Aggregation patterns
sales = [
    {"product": "Laptop", "price": 999, "quantity": 5},
    {"product": "Mouse", "price": 25, "quantity": 50},
    {"product": "Keyboard", "price": 75, "quantity": 30},
    {"product": "Monitor", "price": 300, "quantity": 15}
]

# Total revenue
total_revenue = sum(item['price'] * item['quantity'] for item in sales)
print(f"Total revenue: ${total_revenue}")  # $11,995

# Most expensive product
most_expensive = max(sales, key=lambda x: x['price'])
print(f"Most expensive: {most_expensive['product']} at ${most_expensive['price']}")

# Best seller (by quantity)
best_seller = max(sales, key=lambda x: x['quantity'])
print(f"Best seller: {best_seller['product']} ({best_seller['quantity']} units)")

# Average price
avg_price = sum(item['price'] for item in sales) / len(sales)
print(f"Average price: ${avg_price:.2f}")

# Custom reduction: concatenate product names
from functools import reduce
product_list = reduce(lambda acc, item: f"{acc}, {item['product']}", sales, "Products:")
print(product_list[10:])  # remove "Products: " prefix

# Group and sum by category (if we had categories)
price_ranges = {"cheap": [], "mid": [], "expensive": []}
for item in sales:
    if item['price'] < 50:
        price_ranges["cheap"].append(item['product'])
    elif item['price'] < 200:
        price_ranges["mid"].append(item['product'])
    else:
        price_ranges["expensive"].append(item['product'])

print(f"Price ranges: {price_ranges}")

Pattern use: Analytics dashboards, financial reports, and business intelligence tools use these patterns to compute totals, averages, and rankings.

Pattern 3: Chunking and Batching

Split large lists into smaller batches for processing, pagination, or parallel execution.

# Chunking patterns
data = list(range(1, 26))  # [1, 2, 3, ..., 25]

# Split into chunks of size n
def chunk_list(lst, chunk_size):
    return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)]

batches = chunk_list(data, 5)
print(f"Split into {len(batches)} batches:")
for i, batch in enumerate(batches, 1):
    print(f"  Batch {i}: {batch}")
# Batch 1: [1, 2, 3, 4, 5]
# Batch 2: [6, 7, 8, 9, 10]
# ...

# Process batches (simulate API calls)
def process_batch(batch):
    return sum(batch)  # Simulate processing

results = [process_batch(batch) for batch in batches]
print(f"Batch results: {results}")  # [15, 40, 65, 90, 115]

# Split into n groups (distribute evenly)
def split_into_groups(lst, num_groups):
    k, m = divmod(len(lst), num_groups)
    return [lst[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(num_groups)]

groups = split_into_groups(data, 4)
print(f"\nSplit into {len(groups)} even groups:")
for i, group in enumerate(groups, 1):
    print(f"  Group {i} ({len(group)} items): {group[:3]}...")

# Sliding window pattern
def sliding_window(lst, window_size):
    return [lst[i:i+window_size] for i in range(len(lst) - window_size + 1)]

windows = sliding_window([1, 2, 3, 4, 5], 3)
print(f"\nSliding windows of size 3: {windows}")
# [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

Pattern use: Batch processing systems, pagination in web apps, parallel processing frameworks, and time-series analysis use chunking to manage data efficiently.

Pattern 4: Merging and Zipping

Combine multiple lists element-wise or merge sorted lists to create unified datasets.

# Merging patterns
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
cities = ["NYC", "LA", "Chicago"]

# Zip lists together
people = list(zip(names, ages, cities))
print(people)
# [('Alice', 25, 'NYC'), ('Bob', 30, 'LA'), ('Charlie', 35, 'Chicago')]

# Create dictionary from zipped lists
people_dict = {name: {"age": age, "city": city} 
               for name, age, city in zip(names, ages, cities)}
print(people_dict)

# Unzip tuples back to separate lists
names2, ages2, cities2 = zip(*people)
print(f"Names: {names2}")

# Merge two sorted lists (maintaining order)
list1 = [1, 3, 5, 7]
list2 = [2, 4, 6, 8]

def merge_sorted(l1, l2):
    result = []
    i, j = 0, 0
    while i < len(l1) and j < len(l2):
        if l1[i] < l2[j]:
            result.append(l1[i])
            i += 1
        else:
            result.append(l2[j])
            j += 1
    result.extend(l1[i:])
    result.extend(l2[j:])
    return result

merged = merge_sorted(list1, list2)
print(f"Merged: {merged}")  # [1, 2, 3, 4, 5, 6, 7, 8]

# Interleave lists
def interleave(l1, l2):
    result = []
    for a, b in zip(l1, l2):
        result.extend([a, b])
    return result

interleaved = interleave([1, 3, 5], [2, 4, 6])
print(f"Interleaved: {interleaved}")  # [1, 2, 3, 4, 5, 6]

Pattern use: Database joins, data synchronization, merge sort algorithm, and combining results from multiple sources use these merging techniques.

Pattern 5: Partitioning and Filtering

Separate lists into multiple groups based on conditions or criteria.

# Partitioning patterns
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Simple partition: evens and odds
evens = [x for x in numbers if x % 2 == 0]
odds = [x for x in numbers if x % 2 != 0]
print(f"Evens: {evens}, Odds: {odds}")

# Partition function (returns both groups)
def partition(lst, condition):
    true_list = [x for x in lst if condition(x)]
    false_list = [x for x in lst if not condition(x)]
    return true_list, false_list

small, large = partition(numbers, lambda x: x <= 5)
print(f"Small: {small}, Large: {large}")

# Multi-way partition
def partition_by_ranges(lst, thresholds):
    """Partition into ranges defined by thresholds"""
    groups = [[] for _ in range(len(thresholds) + 1)]
    for item in lst:
        for i, threshold in enumerate(thresholds):
            if item < threshold:
                groups[i].append(item)
                break
        else:
            groups[-1].append(item)
    return groups

ranges = partition_by_ranges(numbers, [4, 7])
print(f"Ranges: {ranges}")  # [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

# Filter chain (multiple conditions)
def filter_chain(lst, *conditions):
    """Apply multiple filter conditions"""
    result = lst
    for condition in conditions:
        result = [x for x in result if condition(x)]
    return result

result = filter_chain(
    numbers,
    lambda x: x > 2,      # greater than 2
    lambda x: x < 9,      # less than 9
    lambda x: x % 2 == 0  # even
)
print(f"Filter chain result: {result}")  # [4, 6, 8]

Pattern use: Data categorization, A/B testing splits, quality control systems, and recommendation engines use partitioning to segment data for different processing paths.

Challenge Problems

Test your mastery with these comprehensive challenges that combine multiple concepts.

Task: Create an inventory system using a list of dictionaries. Implement functions to: add items, remove items by name, update quantity, calculate total value, and find items below a quantity threshold.

Show Solution
inventory = []

def add_item(name, price, quantity):
    inventory.append({"name": name, "price": price, "quantity": quantity})
    print(f"Added {name}")

def remove_item(name):
    for item in inventory:
        if item['name'] == name:
            inventory.remove(item)
            print(f"Removed {name}")
            return
    print(f"{name} not found")

def update_quantity(name, new_qty):
    for item in inventory:
        if item['name'] == name:
            item['quantity'] = new_qty
            print(f"Updated {name} to {new_qty}")
            return

def total_value():
    return sum(item['price'] * item['quantity'] for item in inventory)

def low_stock(threshold):
    return [item['name'] for item in inventory if item['quantity'] < threshold]

# Test
add_item("Laptop", 999, 5)
add_item("Mouse", 29, 2)
add_item("Keyboard", 79, 10)
print(f"Total value: ${total_value()}")
print(f"Low stock (< 5): {low_stock(5)}")

Task: Write encode(lst) and decode(encoded) functions. Encode converts [1,1,1,2,2,3] to [(3,1), (2,2), (1,3)]. Decode does the reverse.

Show Solution
def encode(lst):
    if not lst:
        return []
    
    result = []
    current = lst[0]
    count = 1
    
    for i in range(1, len(lst)):
        if lst[i] == current:
            count += 1
        else:
            result.append((count, current))
            current = lst[i]
            count = 1
    
    result.append((count, current))
    return result

def decode(encoded):
    result = []
    for count, value in encoded:
        result.extend([value] * count)
    return result

# Test
original = [1,1,1,2,2,3,3,3,3,4]
encoded = encode(original)
decoded = decode(encoded)

print(f"Original: {original}")
print(f"Encoded: {encoded}")
print(f"Decoded: {decoded}")
print(f"Match: {original == decoded}")

Task: Given numbers = [1, 2, 3, 4, 5] and target = 5, find all continuous sublists that sum to the target. Should return [[2, 3], [5]].

Show Solution
def find_sublists_with_sum(lst, target):
    result = []
    
    for start in range(len(lst)):
        current_sum = 0
        for end in range(start, len(lst)):
            current_sum += lst[end]
            if current_sum == target:
                result.append(lst[start:end+1])
                break  # No need to check further from this start
            elif current_sum > target:
                break  # Sum exceeded, move to next start
    
    return result

# Test
numbers = [1, 2, 3, 4, 5]
target = 5
result = find_sublists_with_sum(numbers, target)
print(f"Sublists that sum to {target}: {result}")
# [[2, 3], [5]]

# More tests
print(find_sublists_with_sum([1, 4, 20, 3, 10, 5], 33))  # [[20, 3, 10]]
print(find_sublists_with_sum([10, 2, -2, -20, 10], -10))  # [[-20, 10]]

Task: Write rotate(lst, k) that rotates list k positions to the right. rotate([1,2,3,4,5], 2) should return [4,5,1,2,3]. Handle negative k (rotate left) and k larger than list length.

Show Solution
def rotate(lst, k):
    if not lst:
        return lst
    
    n = len(lst)
    k = k % n  # Handle k > n and k < -n
    
    if k == 0:
        return lst.copy()
    
    # Split and recombine
    return lst[-k:] + lst[:-k]

# Alternative using slicing from the other direction
def rotate_alt(lst, k):
    if not lst:
        return lst
    k = k % len(lst)
    return lst[k:] + lst[:k]

# Tests
original = [1, 2, 3, 4, 5]
print(f"Original: {original}")
print(f"Rotate right 2: {rotate(original, 2)}")   # [4, 5, 1, 2, 3]
print(f"Rotate left 2: {rotate(original, -2)}")   # [3, 4, 5, 1, 2]
print(f"Rotate 7 (> len): {rotate(original, 7)}") # [4, 5, 1, 2, 3]
print(f"Rotate 0: {rotate(original, 0)}")         # [1, 2, 3, 4, 5]

Task: Write longest_increasing(lst) that returns the length of the longest strictly increasing subsequence. For [10, 9, 2, 5, 3, 7, 101, 18], answer is 4 (subsequence: [2, 3, 7, 101]).

Show Solution
def longest_increasing(lst):
    if not lst:
        return 0
    
    # dp[i] = length of longest increasing subsequence ending at i
    dp = [1] * len(lst)
    
    for i in range(1, len(lst)):
        for j in range(i):
            if lst[j] < lst[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    
    return max(dp)

# Test
test_cases = [
    [10, 9, 2, 5, 3, 7, 101, 18],  # 4
    [0, 1, 0, 3, 2, 3],             # 4
    [7, 7, 7, 7, 7],                # 1
    [1, 2, 3, 4, 5],                # 5
]

for lst in test_cases:
    result = longest_increasing(lst)
    print(f"{lst} → Longest: {result}")

Performance and Best Practices

Understanding performance characteristics and following best practices helps you write efficient, maintainable code. Learn when to use lists versus other data structures and how to optimize list operations.

Time Complexity Quick Reference

Different operations have vastly different performance characteristics. Choosing the right approach can make the difference between fast and slow code.

Operation Code Example Time Best For Alternative
Access by index lst[5] O(1) Random access -
Append to end lst.append(x) O(1)* Building lists -
Pop from end lst.pop() O(1) Stack operations -
Insert at start lst.insert(0, x) O(n) Occasional inserts collections.deque
Delete from start del lst[0] O(n) Occasional deletes collections.deque
Search value x in lst O(n) Small lists set or dict
Find index lst.index(x) O(n) Small lists dict mapping
Count occurrences lst.count(x) O(n) Small lists Counter
Extend lst.extend(other) O(k) Merging lists -
Sort in place lst.sort() O(n log n) Ordering data Keep sorted
Slice lst[1:10] O(k) Extracting ranges itertools.islice
Copy lst.copy() O(n) Preserving data Avoid if possible
Note: O(1)* for append is amortized constant time. Occasionally, Python needs to resize the internal array, which takes O(n), but this happens rarely enough that the average cost per append is constant.

List vs Other Data Structures

Lists are not always the best choice. Understanding alternatives helps you pick the right tool for each job.

Use Lists When:
  • Order matters and you need to maintain it
  • You need to access elements by index
  • You append to the end frequently
  • You need to store duplicate values
  • Collection size is moderate (< 10,000)
Avoid Lists When:
  • You need fast membership testing (use set)
  • You insert/delete from start often (use deque)
  • You need key-value pairs (use dict)
  • Data should be immutable (use tuple)
  • You need unique values only (use set)

Performance Benchmarks

Real-world performance comparisons show the practical impact of choosing the right operation.

# Performance comparison example
import time

def benchmark(func, *args, iterations=100000):
    start = time.time()
    for _ in range(iterations):
        func(*args)
    return time.time() - start

# Test 1: Append vs Insert at position 0
lst_append = []
lst_insert = []

append_time = benchmark(lambda: lst_append.append(1))
insert_time = benchmark(lambda: lst_insert.insert(0, 1))

print(f"Append: {append_time:.4f}s")
print(f"Insert at 0: {insert_time:.4f}s")
print(f"Insert is {insert_time/append_time:.1f}x slower")
# Insert at 0: ~100x slower for large lists!

# Test 2: List membership vs Set membership
large_list = list(range(10000))
large_set = set(range(10000))

list_search_time = benchmark(lambda: 9999 in large_list, iterations=1000)
set_search_time = benchmark(lambda: 9999 in large_set, iterations=1000)

print(f"\nList search: {list_search_time:.4f}s")
print(f"Set search: {set_search_time:.4f}s")
print(f"Set is {list_search_time/set_search_time:.0f}x faster")
# Set is ~1000x faster for large collections!

# Test 3: List comprehension vs loop
def loop_squares(n):
    result = []
    for i in range(n):
        result.append(i**2)
    return result

def comp_squares(n):
    return [i**2 for i in range(n)]

loop_time = benchmark(loop_squares, 1000, iterations=1000)
comp_time = benchmark(comp_squares, 1000, iterations=1000)

print(f"\nLoop append: {loop_time:.4f}s")
print(f"Comprehension: {comp_time:.4f}s")
print(f"Comprehension is {loop_time/comp_time:.1f}x faster")

Key insight: These benchmarks show why choosing the right operation matters. Insert at position 0 can be 100x slower than append. Set membership is 1000x faster than list membership for large collections.

Memory Optimization Tips

Lists consume memory. For large datasets or memory-constrained environments, use these strategies to reduce memory footprint.

Use Generators Instead of Lists
# Bad: Creates full list in memory
squares = [x**2 for x in range(1000000)]
total = sum(squares)  # Uses ~8MB

# Good: Generator creates values on demand
squares_gen = (x**2 for x in range(1000000))
total = sum(squares_gen)  # Uses ~80 bytes!

Generators are perfect for one-time iteration over large datasets.

Use array Module for Homogeneous Data
# List of integers (28 bytes per item)
big_list = [1, 2, 3] * 1000000

# array module (4 bytes per item)
import array
big_array = array.array('i', [1, 2, 3] * 1000000)
# Uses 7x less memory!

For numeric data only, array module offers significant space savings.

Reuse Lists Instead of Creating New Ones
# Bad: Creates new list each iteration
for _ in range(1000):
    temp = [0] * 10000
    process(temp)  # Memory churns

# Good: Reuse same list
temp = [0] * 10000
for _ in range(1000):
    temp[:] = [0] * 10000  # Reuses memory
    process(temp)

Reusing reduces garbage collection overhead in tight loops.

Pre-allocate When Size is Known
# Slower: Growing list dynamically
result = []
for i in range(10000):
    result.append(i * 2)

# Faster: Pre-allocate with comprehension
result = [i * 2 for i in range(10000)]
# Or use list multiplication then assign
result = [0] * 10000
for i in range(10000):
    result[i] = i * 2

Pre-allocation avoids multiple memory reallocations as list grows.

Code Style Best Practices

Follow Python conventions and idioms to write code that's readable, maintainable, and Pythonic.

Do: Use Comprehensions
squares = [x**2 for x in range(10)]
Don't: Verbose loops for simple transformations
squares = []
for x in range(10):
    squares.append(x**2)
Do: Use enumerate() for index+value
for i, val in enumerate(items):
    print(f"{i}: {val}")
Don't: Manual index tracking
for i in range(len(items)):
    print(f"{i}: {items[i]}")
Do: Use zip() to iterate multiple lists
for name, age in zip(names, ages):
    print(f"{name} is {age}")
Don't: Index both lists manually
for i in range(len(names)):
    print(f"{names[i]} is {ages[i]}")
Do: Use 'in' for membership testing
if item in my_list:
    process(item)
Don't: Manual iteration to find
found = False
for x in my_list:
    if x == item:
        found = True
if found: process(item)
Do: Use slicing for subsequences
first_five = items[:5]
last_three = items[-3:]
Don't: Manual element selection
first_five = []
for i in range(5):
    first_five.append(items[i])
Do: Use unpacking for fixed-size lists
x, y, z = coordinates
Don't: Index access for unpacking
x = coordinates[0]
y = coordinates[1]
z = coordinates[2]
Pythonic Principle: "There should be one -- and preferably only one -- obvious way to do it." Follow established idioms like comprehensions and built-in functions for cleaner, more maintainable code.

Key Takeaways

Lists Are Ordered and Mutable

Lists maintain element order and can be modified after creation using methods like append, insert, remove, and pop

Zero-Based Indexing

Use positive indices (0, 1, 2...) from the start or negative indices (-1, -2, -3...) from the end to access elements

Built-in Methods

Master essential methods: append, extend, insert for adding; remove, pop, clear for removing; sort, reverse for ordering

Tuples Are Immutable

Tuples cannot be changed after creation, making them faster, hashable (can be dict keys), and safer for constant data

List Comprehensions

Use [expression for item in iterable if condition] syntax for concise, readable, and efficient list creation

Type Conversion

Convert freely between lists and tuples using list() and tuple() constructors based on mutability needs

Knowledge Check

1 What is the output of fruits = ["a", "b", "c"]; print(fruits[-2])?
2 Which method adds multiple items from another list to the end of a list?
3 What is the main difference between lists and tuples?
4 What does [x*2 for x in range(3)] produce?
5 Which method removes and returns an element by index?
6 How do you create a single-element tuple?
Answer all questions to check your score