Python Lists — The Complete Guide to Python's Most Versatile Data Structure

Python BasicsListsFree Lesson

Advertisement

Python Lists — The Complete Guide to Python's Most Versatile Data Structure

šŸ’” Lists are the most widely used data structure in Python. This comprehensive guide covers everything from basic creation to advanced techniques, giving you complete mastery over Python's most versatile collection type.

Learning Objectives

By the end of this tutorial, you will be able to:

  • Create lists using various methods and understand when to use each
  • Use indexing and slicing to access and manipulate list elements
  • Apply all built-in list methods confidently
  • Write basic and intermediate list comprehensions
  • Understand nested lists and 2D data structures
  • Recognize performance implications of different operations
  • Avoid common pitfalls that trip up Python developers
  • Apply lists to solve real-world programming problems

What Are Lists?

A list is an ordered, mutable collection in Python. Think of it as a container that holds multiple items in a specific sequence.

# Lists can contain any type of data — integers, strings, floats, booleans, even other lists
mixed = [42, "hello", 3.14, True, None, [1, 2, 3]]
print(mixed)
# Output: [42, 'hello', 3.14, True, None, [1, 2, 3]]

Key Characteristics

CharacteristicDescriptionExample
OrderedElements maintain their insertion order[1, 2, 3] is different from [3, 2, 1]
MutableElements can be changed after creationlst[0] = 99 works fine
DynamicCan grow or shrink at runtimelst.append(4) adds an element
HeterogeneousCan mix different types[1, "two", 3.0] is valid
Duplicates allowedSame value can appear multiple times[1, 1, 2, 3] is valid
Zero-indexedFirst element is at index 0lst[0] gets the first item

Lists Store References, Not Copies

When you create a list, Python stores references (pointers) to the objects, not the objects themselves:

a = [1, 2, 3]
b = a          # b points to the same list object as a
b.append(4)
print(a)       # [1, 2, 3, 4] — a is also changed!
print(a is b)  # True — same object in memory

This is why understanding shallow vs deep copy matters (covered later).


Creating Lists

1. Literal Syntax (Most Common)

# Empty list
empty = []

# List of strings
fruits = ["apple", "banana", "cherry"]

# List of numbers
numbers = [1, 2, 3, 4, 5]

# Mixed types
mixed = [1, "two", 3.0, True]

print(fruits)   # ['apple', 'banana', 'cherry']
print(numbers)  # [1, 2, 3, 4, 5]
print(mixed)    # [1, 'two', 3.0, True]

2. The list() Constructor

# From a string (each character becomes an element)
chars = list("Python")
print(chars)  # ['P', 'y', 't', 'h', 'o', 'n']

# From a range
numbers = list(range(1, 6))
print(numbers)  # [1, 2, 3, 4, 5]

# 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)

3. List Multiplication (Initialization)

# Create a list with repeated values
zeros = [0] * 5
print(zeros)  # [0, 0, 0, 0, 0]

ones = [1] * 3
print(ones)   # [1, 1, 1]

# Repeat a pattern
pattern = [0, 1] * 4
print(pattern)  # [0, 1, 0, 1, 0, 1, 0, 1]

# WARNING: Be careful with mutable objects in multiplication
nested = [[0]] * 3
nested[0][0] = 99
print(nested)  # [[99], [99], [99]] — All sublists are the same object!

4. List Comprehension (Preview)

# Create a list of squares
squares = [x**2 for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

# Filter even numbers
evens = [x for x in range(10) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# Transform strings
upper = [word.upper() for word in ["hello", "world"]]
print(upper)  # ['HELLO', 'WORLD']

5. Using dict() and zip()

# Create list of tuples from two lists
names = ["Alice", "Bob"]
scores = [95, 87]
paired = list(zip(names, scores))
print(paired)  # [('Alice', 95), ('Bob', 87)]

# Create list from dictionary
prices = {"apple": 1.20, "banana": 0.50}
items = list(prices.keys())
print(items)  # ['apple', 'banana']

Indexing and Slicing

Lists use the same indexing and slicing mechanics as strings.

Positive Indexing

fruits = ["apple", "banana", "cherry", "date", "elderberry"]
#           0        1        2        3         4

print(fruits[0])   # apple
print(fruits[2])   # cherry
print(fruits[-1])  # elderberry (last element)
print(fruits[-2])  # date (second to last)

Slicing

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(numbers[2:5])     # [2, 3, 4] — from index 2 to 4
print(numbers[:4])      # [0, 1, 2, 3] — first 4 elements
print(numbers[6:])      # [6, 7, 8, 9] — from index 6 to end
print(numbers[::2])     # [0, 2, 4, 6, 8] — every other element
print(numbers[::-1])    # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — reversed
print(numbers[1:8:3])   # [1, 4, 7] — every 3rd from index 1 to 7

Slice Assignment (Mutating Slices)

This is where lists differ from strings — you can assign to slices:

letters = ['a', 'b', 'c', 'd', 'e']

# Replace a slice
letters[1:3] = ['B', 'C', 'D']  # Replaces 2 elements with 3
print(letters)  # ['a', 'B', 'C', 'D', 'd', 'e']

# Insert using slice assignment
letters[1:1] = ['X', 'Y']  # Insert before index 1
print(letters)  # ['a', 'X', 'Y', 'B', 'C', 'D', 'd', 'e']

# Delete using slice assignment
letters[2:5] = []  # Remove elements at indices 2, 3, 4
print(letters)  # ['a', 'X', 'B', 'd', 'e']

# Replace entire list content
letters[:] = [1, 2, 3]
print(letters)  # [1, 2, 3]

Copying with Slicing

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

# This creates a shallow copy
copy = original[:]
copy[0] = 99

print(original)  # [1, 2, 3, 4, 5] — unchanged
print(copy)      # [99, 2, 3, 4, 5] — modified

# Equivalent to list.copy()
copy2 = original.copy()

Modifying Individual Elements

scores = [85, 92, 78, 95, 88]
scores[2] = 100  # Change the 78 to 100
print(scores)    # [85, 92, 100, 95, 88]

scores[-1] = 0   # Change the last element
print(scores)    # [85, 92, 100, 95, 0]

List Methods (Comprehensive)

Adding Elements

MethodDescriptionTime Complexity
append(x)Add x to the endO(1) amortized
extend(iterable)Add all items from iterableO(k) where k is length of iterable
insert(i, x)Insert x at position iO(n)
# append() — adds a single element to the end
fruits = ["apple", "banana"]
fruits.append("cherry")
print(fruits)  # ['apple', 'banana', 'cherry']

# You can append any object, including lists (as a single element)
fruits.append(["date", "elderberry"])
print(fruits)  # ['apple', 'banana', 'cherry', ['date', 'elderberry']]
print(len(fruits))  # 4 — the nested list counts as one element

# extend() — adds multiple elements from an iterable
fruits = ["apple", "banana"]
fruits.extend(["cherry", "date"])
print(fruits)  # ['apple', 'banana', 'cherry', 'date']

# extend vs append — key difference
a = [1, 2]
a.extend([3, 4])  # Adds 3 and 4 as separate elements
print(a)  # [1, 2, 3, 4]

b = [1, 2]
b.append([3, 4])  # Adds [3, 4] as a single element
print(b)  # [1, 2, [3, 4]]

# insert() — adds element at a specific position
names = ["Alice", "Bob", "David"]
names.insert(2, "Charlie")  # Insert "Charlie" at index 2
print(names)  # ['Alice', 'Bob', 'Charlie', 'David']

names.insert(0, "Zara")  # Insert at the beginning
print(names)  # ['Zara', 'Alice', 'Bob', 'Charlie', 'David']

Removing Elements

MethodDescriptionTime Complexity
remove(x)Remove first occurrence of xO(n)
pop(i)Remove and return element at index iO(n) for i≠-1, O(1) for i=-1
del lst[i]Remove element at index iO(n)
clear()Remove all elementsO(n)
# remove() — removes first occurrence of a value
colors = ["red", "blue", "green", "blue"]
colors.remove("blue")
print(colors)  # ['red', 'green', 'blue'] — only first "blue" removed

# Raises ValueError if value not found
try:
    colors.remove("yellow")
except ValueError as e:
    print(f"Error: {e}")

# pop() — removes and returns element at index
stack = [10, 20, 30, 40, 50]
last = stack.pop()
print(last)   # 50
print(stack)  # [10, 20, 30, 40]

second = stack.pop(1)
print(second)  # 20
print(stack)   # [10, 30, 40]

# pop() on empty list raises IndexError
try:
    [].pop()
except IndexError as e:
    print(f"Error: {e}")

# del — keyword for removing elements
data = [10, 20, 30, 40, 50]
del data[2]       # Remove element at index 2
print(data)       # [10, 20, 40, 50]

del data[1:3]     # Remove a slice
print(data)       # [10, 50]

del data[:]       # Clear the list (same as data.clear())
print(data)       # []

# clear() — remove all elements
items = [1, 2, 3, 4, 5]
items.clear()
print(items)  # []

Searching

MethodDescriptionTime Complexity
index(x)Return index of first occurrence of xO(n)
index(x, i, j)Find x between indices i and jO(j-i)
count(x)Count occurrences of xO(n)
x in lstCheck if x is in the listO(n)
# index() — find position of an element
primes = [2, 3, 5, 7, 11, 13, 17]
print(primes.index(7))   # 3
print(primes.index(13))  # 5

# index() with start and stop parameters
numbers = [1, 2, 3, 2, 1]
print(numbers.index(2, 2))   # Find 2 starting from index 2 → 3
print(numbers.index(1, 1, 4))  # Find 1 between index 1 and 3 → 4? No, let's check
# Actually this would raise ValueError since 1 is not between indices 1-3
# Let me correct:
print(numbers.index(1, 0, 4))  # Find 1 between index 0 and 3 → 0

# count() — how many times does a value appear?
data = [1, 2, 2, 3, 2, 4, 2]
print(data.count(2))  # 4
print(data.count(5))  # 0 (not found)

# The 'in' operator — membership testing
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits)   # True
print("grape" in fruits)    # False
print("grape" not in fruits)  # True

Sorting

Method/FunctionDescriptionReturns
lst.sort()Sort list in-placeNone (modifies original)
sorted(lst)Return new sorted listNew list
lst.sort(key=fn)Sort with custom keyNone
lst.sort(reverse=True)Sort in descending orderNone
reversed(lst)Return reverse iteratorIterator (not a list)
lst.reverse()Reverse in-placeNone
# sort() — in-place sorting (modifies the original list)
scores = [85, 92, 78, 95, 88]
scores.sort()
print(scores)  # [78, 85, 88, 92, 95]

scores.sort(reverse=True)
print(scores)  # [95, 92, 88, 85, 78]

# sorted() — returns a new sorted list (original unchanged)
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
new_sorted = sorted(numbers)
print(numbers)     # [3, 1, 4, 1, 5, 9, 2, 6] — original unchanged
print(new_sorted)  # [1, 1, 2, 3, 4, 5, 6, 9]

# Sorting with a key function
words = ["banana", "apple", "cherry", "date"]
words.sort(key=len)  # Sort by string length
print(words)  # ['date', 'apple', 'banana', 'cherry']

# Case-insensitive sorting
names = ["charlie", "Alice", "bob"]
names.sort(key=str.lower)
print(names)  # ['Alice', 'bob', 'charlie']

# Sort by a specific attribute or index
students = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
students.sort(key=lambda student: student[1])  # Sort by score
print(students)  # [('Charlie', 78), ('Alice', 85), ('Bob', 92)]

# reversed() — returns an iterator, not a list
nums = [1, 2, 3]
rev = reversed(nums)
print(list(rev))  # [3, 2, 1] — need to convert to list
print(nums)       # [1, 2, 3] — original unchanged

# reverse() — in-place reversal
nums = [1, 2, 3]
nums.reverse()
print(nums)  # [3, 2, 1]

Other Methods

MethodDescriptionTime Complexity
copy()Return a shallow copyO(n)
len(lst)Return number of elementsO(1)
sum(lst)Sum all numeric elementsO(n)
min(lst)Return smallest elementO(n)
max(lst)Return largest elementO(n)
# copy() — create a shallow copy
original = [1, 2, 3, [4, 5]]
copied = original.copy()
copied[0] = 99
print(original)  # [1, 2, 3, [4, 5]] — unchanged

# But shallow copy has caveats with nested objects
copied[3][0] = 99
print(original)  # [1, 2, 3, [99, 5]] — nested list is shared!

# len(), sum(), min(), max()
numbers = [4, 2, 7, 1, 9, 3]
print(len(numbers))  # 6
print(sum(numbers))  # 26
print(min(numbers))  # 1
print(max(numbers))  # 9

# With strings
words = ["apple", "banana", "cherry"]
print(min(words))  # 'apple' (lexicographic minimum)
print(max(words))  # 'cherry' (lexicographic maximum)

List Comprehensions (Preview)

List comprehensions provide a concise way to create lists. A detailed tutorial is available in the list comprehensions module, but here's what you need to know:

Basic Syntax

# [expression for item in iterable]
squares = [x**2 for x in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Equivalent loop:
squares = []
for x in range(10):
    squares.append(x**2)

Filtering with Conditions

# [expression for item in iterable if condition]
evens = [x for x in range(20) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Filter and transform
long_words = [word.upper() for word in ["hi", "hello", "hey", "howdy"] if len(word) > 2]
print(long_words)  # ['HELLO', 'HOWDY']

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 3x3 identity matrix
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]]

Conditional Expressions (Ternary)

# [expr_if_true if condition else expr_if_false for item in iterable]
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
print(labels)  # ['even', 'odd', 'even', 'odd', 'even']

Nested Lists

Nested lists (lists within lists) are commonly used for matrices, tables, and structured data.

2D Lists / Matrices

# Create a 3x3 matrix
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# Access individual elements
print(matrix[0][0])  # 1 (row 0, column 0)
print(matrix[1][2])  # 6 (row 1, column 2)
print(matrix[2][1])  # 8 (row 2, column 1)

# Access entire rows
print(matrix[0])     # [1, 2, 3] (first row)
print(matrix[-1])    # [7, 8, 9] (last row)

# Modify elements
matrix[1][1] = 99
print(matrix)  # [[1, 2, 3], [4, 99, 6], [7, 8, 9]]

# Iterate through a 2D list
for row in matrix:
    for element in row:
        print(element, end=" ")
    print()
# Output:
# 1 2 3
# 4 99 6
# 7 8 9

Matrix Operations

# Matrix addition
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]

C = [[A[i][j] + B[i][j] for j in range(2)] for i in range(2)]
print(C)  # [[6, 8], [10, 12]]

# Transpose a matrix
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)  # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

Deep Copy vs Shallow Copy

This is critical when working with nested lists:

import copy

original = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Shallow copy (using list methods)
shallow = original.copy()
shallow[0][0] = 99
print(original[0][0])  # 99 — original is affected!

# Deep copy (using copy module)
original = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original[0][0])  # 1 — original is NOT affected
Copy TypeWhat It DoesUse When
copy() / [:] / list()Shallow copy — copies referencesTop-level elements are immutable
copy.deepcopy()Deep copy — recursively copies everythingNested mutable objects

Lists vs Other Sequences

Lists vs Tuples

FeatureList []Tuple ()
MutableYesNo
Syntax[1, 2, 3](1, 2, 3)
PerformanceSlightly slowerSlightly faster
Use caseCollection of similar items that changesFixed record of heterogeneous data
Dictionary keyNo (unhashable)Yes (if all elements are hashable)
# Use a list when data changes
shopping_cart = ["milk", "eggs", "bread"]
shopping_cart.append("butter")

# Use a tuple when data is fixed
rgb_color = (255, 128, 0)  # RGB values won't change

# Tuples can be dictionary keys; lists cannot
locations = {
    (40.7128, -74.0060): "New York",
    (51.5074, -0.1278): "London"
}

# This would raise TypeError:
# locations[[40.7128, -74.0060]] = "New York"

Lists vs NumPy Arrays

FeatureListNumPy Array
Element typesCan be mixedMust be same type
Math operationsElement-wise not built-inVectorized operations
MemoryMore memory efficient for small dataMore efficient for large numerical data
SpeedSlower for computationsMuch faster for computations
# Lists — element-wise operations require loops
a = [1, 2, 3]
b = [4, 5, 6]
c = [a[i] + b[i] for i in range(len(a))]  # [5, 7, 9]

# NumPy — vectorized operations
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = a + b  # array([5, 7, 9])

Lists vs Generators

FeatureListGenerator
MemoryStores all elementsProduces elements on demand
IterationCan iterate multiple timesSingle pass only
IndexingSupports lst[i]No random access
Use caseWhen you need the full dataWhen you only need to iterate once
# List — all elements stored in memory
squares_list = [x**2 for x in range(1_000_000)]  # ~8MB of memory

# Generator — produces values on demand
squares_gen = (x**2 for x in range(1_000_000))  # ~0 memory

# Use generators for large datasets
import sys
print(sys.getsizeof(squares_list))  # ~8,448,728 bytes
print(sys.getsizeof(squares_gen))   # ~208 bytes

Common Patterns

Stacking / Unpacking

# Unpacking a list
first, second, *rest = [1, 2, 3, 4, 5]
print(first)   # 1
print(second)  # 2
print(rest)    # [3, 4, 5]

# Unpacking with ignore
first, _, third, *_ = [1, 2, 3, 4, 5]
print(first)  # 1
print(third)  # 3

# Swapping variables using unpacking
a, b = 10, 20
a, b = b, a
print(a, b)  # 20 10

# Unpacking in function arguments
def greet(name, age):
    return f"{name} is {age} years old"

person = ["Alice", 30]
print(greet(*person))  # "Alice is 30 years old"

Rotating Lists

from collections import deque

def rotate_left(lst, k):
    """Rotate list k positions to the left."""
    d = deque(lst)
    d.rotate(-k)
    return list(d)

def rotate_right(lst, k):
    """Rotate list k positions to the right."""
    d = deque(lst)
    d.rotate(k)
    return list(d)

numbers = [1, 2, 3, 4, 5]
print(rotate_left(numbers, 2))   # [3, 4, 5, 1, 2]
print(rotate_right(numbers, 2))  # [4, 5, 1, 2, 3]

Removing Duplicates (Preserving Order)

def remove_duplicates(lst):
    """Remove duplicates while preserving order."""
    seen = set()
    result = []
    for item in lst:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

data = [3, 1, 2, 3, 1, 4, 2, 5]
print(remove_duplicates(data))  # [3, 1, 2, 4, 5]

# Simpler approach (Python 3.7+ preserves dict insertion order)
unique = list(dict.fromkeys(data))
print(unique)  # [3, 1, 2, 4, 5]

Flattening Nested Lists

# Flatten a list of lists
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Using itertools.chain for better performance
from itertools import chain
flat = list(chain.from_iterable(nested))
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Grouping Elements

from itertools import groupby

data = [1, 1, 2, 2, 2, 3, 3, 3, 3]
for value, group in groupby(sorted(data)):
    print(f"{value}: {list(group)}")
# Output:
# 1: [1, 1]
# 2: [2, 2, 2]
# 3: [3, 3, 3, 3]

Performance Considerations

Understanding time complexity helps you write efficient code:

OperationTime ComplexityNotes
append(x)O(1) amortizedFast — Python pre-allocates memory
pop() (from end)O(1)Fast — same as append
pop(0) (from start)O(n)Slow — all elements shift
insert(0, x) (at start)O(n)Slow — all elements shift
x in lst (search)O(n)Linear search through list
lst[i] (index access)O(1)Random access is fast
len(lst)O(1)Length is cached
sort()O(n log n)Timsort algorithm
copy()O(n)Must copy all elements

When to Use collections.deque

from collections import deque

# deque is optimized for append/popleft operations
# Use it when you frequently add/remove from the beginning

# List — O(n) for insert at start
lst = [1, 2, 3]
lst.insert(0, 0)  # Slow for large lists

# deque — O(1) for appendleft
dq = deque([1, 2, 3])
dq.appendleft(0)  # Fast regardless of size

Performance Example

import time

# Bad: Building a list by inserting at the start
start = time.time()
lst = []
for i in range(100_000):
    lst.insert(0, i)  # O(n) each time — O(n²) total
print(f"insert(0, x): {time.time() - start:.4f}s")

# Good: Build list and reverse, or use deque
start = time.time()
lst = []
for i in range(100_000):
    lst.append(i)  # O(1) each time — O(n) total
lst.reverse()
print(f"append + reverse: {time.time() - start:.4f}s")

# Best: Use deque for this pattern
start = time.time()
dq = deque()
for i in range(100_000):
    dq.appendleft(i)
print(f"deque.appendleft: {time.time() - start:.4f}s")

Common Mistakes

1. Mutable Default Arguments

# WRONG — mutable default is shared across all calls
def add_item(item, lst=[]):
    lst.append(item)
    return lst

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] — unexpected! The default list persists

# CORRECT — use None as default
def add_item(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

print(add_item(1))  # [1]
print(add_item(2))  # [2] — works correctly

2. Modifying While Iterating

# WRONG — skipping elements during iteration
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Bad! Modifies list while iterating

print(numbers)  # [1, 3, 5] — seems OK but is unreliable

# CORRECT — create a new list or iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers)  # [1, 3, 5]

# Alternative: iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]:  # numbers[:] creates a copy
    if num % 2 == 0:
        numbers.remove(num)

3. Shallow Copy Confusion

# Problem: nested objects are shared in shallow copies
original = [[1, 2], [3, 4]]
shallow = original.copy()

shallow[0][0] = 99
print(original[0][0])  # 99 — original is modified!

# Solution: use deep copy for nested structures
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)

deep[0][0] = 99
print(original[0][0])  # 1 — original is safe

4. == vs is for Lists

a = [1, 2, 3]
b = [1, 2, 3]
c = a

print(a == b)   # True — same values
print(a is b)   # False — different objects in memory
print(a is c)   # True — c points to the same object as a

# Use == to compare values
# Use is to check if two variables reference the same object

5. Forgetting sort() Returns None

# WRONG — sort() returns None, not the sorted list
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print(result)  # None!
print(numbers) # [1, 1, 3, 4, 5] — the list is sorted in-place

# CORRECT — either use sort() for in-place, or sorted() for a new list
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers)  # Returns new list
print(sorted_numbers)  # [1, 1, 3, 4, 5]

Practice Exercises

Exercise 1: Remove Duplicates from a List

Write a function that removes duplicates from a list while preserving the original order.

def remove_duplicates(lst):
    """Remove duplicates while preserving order."""
    seen = set()
    result = []
    for item in lst:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

# Test cases
print(remove_duplicates([1, 2, 2, 3, 4, 4, 5]))  # [1, 2, 3, 4, 5]
print(remove_duplicates(["a", "b", "a", "c", "b"]))  # ['a', 'b', 'c']
print(remove_duplicates([]))  # []
print(remove_duplicates([1, 1, 1, 1]))  # [1]

See Solution

def remove_duplicates(lst):
    """Remove duplicates while preserving order."""
    seen = set()
    result = []
    for item in lst:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result

# Test cases
print(remove_duplicates([1, 2, 2, 3, 4, 4, 5]))  # [1, 2, 3, 4, 5]
print(remove_duplicates(["a", "b", "a", "c", "b"]))  # ['a', 'b', 'c']
print(remove_duplicates([]))  # []
print(remove_duplicates([1, 1, 1, 1]))  # [1]

Exercise 2: Rotate a List

Write a function that rotates a list by k positions to the right. Elements that fall off the end wrap around to the beginning.

def rotate_right(lst, k):
    """Rotate list k positions to the right."""
    if not lst:
        return lst
    k = k % len(lst)  # Handle k > len(lst)
    return lst[-k:] + lst[:-k]

# Test cases
print(rotate_right([1, 2, 3, 4, 5], 2))  # [4, 5, 1, 2, 3]
print(rotate_right([1, 2, 3, 4, 5], 7))  # [4, 5, 1, 2, 3] (7 % 5 = 2)
print(rotate_right([1, 2, 3, 4, 5], 0))  # [1, 2, 3, 4, 5]
print(rotate_right([], 3))  # []

See Solution

def rotate_right(lst, k):
    """Rotate list k positions to the right."""
    if not lst:
        return lst
    k = k % len(lst)  # Handle k > len(lst)
    return lst[-k:] + lst[:-k]

# Test cases
print(rotate_right([1, 2, 3, 4, 5], 2))  # [4, 5, 1, 2, 3]
print(rotate_right([1, 2, 3, 4, 5], 7))  # [4, 5, 1, 2, 3] (7 % 5 = 2)
print(rotate_right([1, 2, 3, 4, 5], 0))  # [1, 2, 3, 4, 5]
print(rotate_right([], 3))  # []

Exercise 3: Find Common Elements

Write a function that finds the common elements between two lists (without duplicates in the result).

def common_elements(lst1, lst2):
    """Find common elements between two lists."""
    return list(set(lst1) & set(lst2))

# Alternative preserving order
def common_elements_ordered(lst1, lst2):
    """Find common elements preserving order from lst1."""
    set2 = set(lst2)
    seen = set()
    result = []
    for item in lst1:
        if item in set2 and item not in seen:
            seen.add(item)
            result.append(item)
    return result

# Test cases
print(common_elements([1, 2, 3, 4, 5], [3, 4, 5, 6, 7]))  # [3, 4, 5]
print(common_elements(["a", "b", "c"], ["b", "c", "d"]))  # ['b', 'c']
print(common_elements([1, 1, 2, 3], [1, 2, 2]))  # [1, 2]

See Solution

def common_elements(lst1, lst2):
    """Find common elements between two lists."""
    return list(set(lst1) & set(lst2))

# Alternative preserving order
def common_elements_ordered(lst1, lst2):
    """Find common elements preserving order from lst1."""
    set2 = set(lst2)
    seen = set()
    result = []
    for item in lst1:
        if item in set2 and item not in seen:
            seen.add(item)
            result.append(item)
    return result

# Test cases
print(common_elements([1, 2, 3, 4, 5], [3, 4, 5, 6, 7]))  # [3, 4, 5]
print(common_elements(["a", "b", "c"], ["b", "c", "d"]))  # ['b', 'c']
print(common_elements([1, 1, 2, 3], [1, 2, 2]))  # [1, 2]

Key Takeaways

  1. Lists are ordered, mutable, and can hold any data type — they're Python's most versatile collection
  2. Use literal syntax [] for creating lists — it's faster and more Pythonic than list()
  3. Understand append() vs extend() — append adds one element, extend adds multiple
  4. sort() modifies in-place, sorted() returns a new list — choose based on whether you need to preserve the original
  5. List comprehensions are concise and fast — use them for simple transformations and filters
  6. Watch out for shallow copies with nested lists — use copy.deepcopy() when needed
  7. Avoid mutable default arguments — always use None as the default
  8. Consider performance — insert(0, x) is O(n), use deque for frequent start insertions
  9. Use in for membership testing — but remember it's O(n) for lists
  10. Lists are not the best choice for everything — use tuples for fixed data, sets for unique elements, and generators for large datasets

āœļø Next Steps: Now that you've mastered lists, move on to the List Comprehensions tutorial to learn advanced comprehension patterns, or explore Tuples to understand immutable sequences.

Advertisement

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement