Python Lists and List Methods: The Complete Beginner's Guide

Variables hold one value; lists hold many. This chapter covers indexing and slicing, every essential list method, sorting with the key argument, shallow copies, list comprehensions, and nested lists — the toolkit behind almost every real Python program.

Chapter 5 of 20 · Beginner · 40 min · Python Programming Course

In Day 2 you learned that variables store one value. Lists store many. A single list can hold a hundred names, a thousand prices, or every score a student has ever earned — and Python gives you a complete toolkit to add, remove, search, sort, and transform that data with clean, readable code.

Lists are the most used data structure in Python. Not occasionally — constantly. Every time you process a collection of items, filter results, loop through records, or build something from multiple pieces of data, you are almost certainly using a list. Mastering lists is what separates someone who can write Python scripts from someone who can build Python programs.

This chapter covers everything: how lists work, how to access and modify their contents, every essential list method, sorting, list comprehensions, and nested lists.

What you will learn in this chapter

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

  • Create lists and access elements using positive and negative indexing
  • Slice lists to extract sublists
  • Add, remove, and modify list elements
  • Use essential list methods: append(), insert(), remove(), pop(), sort(), reverse(), index(), count(), extend(), copy(), and clear()
  • Sort lists with custom rules using the key parameter
  • Write list comprehensions to create and transform lists in one line
  • Work with nested lists and 2D data structures
  • Avoid the most common list mistakes beginners make

Estimated time: 40 minutes reading + 20 minutes practice

What is a Python list?

A list is an ordered, mutable collection of items stored in a single variable. Ordered means the items have a defined sequence that Python preserves. Mutable means you can change the list after creating it — add items, remove items, change items — unlike strings, which are immutable.

# Creating lists
fruits = ["apple", "banana", "cherry"]
scores = [95, 87, 72, 91, 88]
mixed = ["Alice", 25, True, 3.14]
empty = []

print(fruits)
print(scores)
print(mixed)
print(empty)

Four things worth noting immediately:

  • Lists use square brackets. This distinguishes them from tuples (parentheses) and dictionaries (curly braces).
  • Lists can hold any data type — strings, integers, floats, booleans, even other lists. A single list can mix types freely, though in practice most real lists contain one consistent type.
  • Lists preserve insertion order. The items come out in exactly the same order you put them in, unless you explicitly sort or rearrange them.
  • Lists allow duplicates. [1, 2, 2, 3, 3, 3] is a perfectly valid list. If you need unique values only, that is what sets are for (covered in Day 6).

List indexing — accessing individual items

Every item in a list has a numbered position called an index, starting at 0. Access any item using square brackets with its index:

fruits = ["apple", "banana", "cherry", "mango", "grape"]

print(fruits[0])
print(fruits[1])
print(fruits[3])
print(fruits[4])

Negative indexing

Negative indexes count backward from the end. -1 is always the last item, regardless of list length:

fruits = ["apple", "banana", "cherry", "mango", "grape"]

print(fruits[-1])
print(fruits[-2])
print(fruits[-5])
scores = [88, 92, 75, 96, 83]
print(f"Latest score: {scores[-1]}")
print(f"Previous score: {scores[-2]}")

Index out of range

Accessing an index beyond the list length raises an IndexError:

fruits = ["apple", "banana", "cherry"]
# print(fruits[5])
# print(fruits[-4])

List slicing — extracting sublists

Slicing extracts a portion of a list using the same [start:end:step] syntax you learned for strings in Day 4. The start index is included; the end index is excluded:

scores = [95, 87, 72, 91, 88, 76, 94]

print(scores[0:3])
print(scores[2:5])
print(scores[:3])
print(scores[4:])
print(scores[:])
print(scores[::2])
print(scores[::-1])

Slicing creates a new list — it does not modify the original. This makes slicing safe for extracting data without affecting the source list.

Modifying lists — adding items

Lists are mutable. Here are the main ways to add items.

append() — add to the end

The most common way to grow a list. Adds a single item to the end:

cart = ["laptop", "mouse"]

cart.append("keyboard")
cart.append("monitor")

print(cart)

append() modifies the list in place and returns None. Do not assign its return value:

# cart = cart.append("keyboard")   # cart would become None — common mistake

insert() — add at a specific position

fruits = ["apple", "cherry", "mango"]

fruits.insert(1, "banana")
print(fruits)

fruits.insert(0, "avocado")
print(fruits)

All existing items shift right to make room. If the index is beyond the list length, the item is added at the end without error.

extend() — add multiple items from another iterable

team_a = ["Alice", "Bob"]
team_b = ["Carol", "David", "Eve"]

team_a.extend(team_b)
print(team_a)

extend() vs append() with a list:

numbers = [1, 2, 3]

numbers.append([4, 5])
print(numbers)

numbers = [1, 2, 3]
numbers.extend([4, 5])
print(numbers)

If you want to merge two lists into one flat list, use extend() or the + operator — not append() with a list argument.

The + operator — combining lists

list_a = [1, 2, 3]
list_b = [4, 5, 6]

combined = list_a + list_b
print(combined)
print(list_a)
print(list_b)

Unlike extend(), + creates a new list without modifying either original.

Modifying lists — removing items

remove() — remove by value

Removes the first occurrence of the specified value:

fruits = ["apple", "banana", "cherry", "banana"]

fruits.remove("banana")
print(fruits)
# fruits.remove("grape")   # ValueError if not in list

Safe pattern:

if "banana" in fruits:
    fruits.remove("banana")

pop() — remove by index and return the value

scores = [95, 87, 72, 91, 88]

last = scores.pop()
print(last)
print(scores)

second = scores.pop(1)
print(second)
print(scores)

Use pop() when you need to both remove and use the removed item.

del — remove by index or slice

fruits = ["apple", "banana", "cherry", "mango", "grape"]

del fruits[1]
print(fruits)

del fruits[1:3]
print(fruits)

clear() — empty the entire list

cart = ["laptop", "mouse", "keyboard"]
cart.clear()
print(cart)

Modifying list items directly

fruits = ["apple", "banana", "cherry"]

fruits[1] = "mango"
print(fruits)

fruits[0:2] = ["kiwi", "grape"]
print(fruits)

Lists compared to strings (why mutability matters)

Day 4 showed that strings are immutable: you cannot assign to s[0]. Lists are the oppositeitems[0] = "new" is legal because the list object stays the same while its contents change. That difference drives how you choose a structure: when you need to grow, shrink, or reorder a collection in place, a list is the default tool. When you need stable text that should not accidentally change character by character, use a string (and build new strings from slices and methods).

This also affects how functions behave. Passing a list into a function passes a reference to the same object — if the function mutates the list, callers see the change. Passing a string means any "changes" inside the function must return new string values. As your programs grow, keeping that mental model straight prevents confusing bugs.

Built-in helpers that accept lists

Python ships with built-ins that treat lists as first-class collections:

marks = [72, 88, 91, 79]
print(len(marks))
print(sum(marks))
print(max(marks))
print(min(marks))
print(sorted(marks))

any() and all() are useful with boolean expressions or truthy values:

flags = [True, False, True]
print(any(flags))
print(all(flags))

scores = [80, 92, 74]
print(all(s >= 70 for s in scores))

zip() pairs lists element-wise (handy when you have parallel columns of data):

names = ["Alice", "Bob", "Carol"]
ages = [22, 30, 27]

for name, age in zip(names, ages):
    print(f"{name} is {age}")

These functions are not list methods — they are general tools — but lists are the structure you feed them most often in beginner and intermediate code.

Stack patterns: append and pop together

A stack is "last in, first out." Python lists make stacks trivial: push with append(), pop with pop() (no index). This pattern appears in undo systems, depth-first search, parsing expressions, and backtracking interview problems.

stack = []
stack.append("page:home")
stack.append("page:profile")
stack.append("page:settings")

print(stack.pop())
print(stack.pop())
print(stack)

For a queue ("first in, first out"), lists work but pop(0) is slow on very large lists because Python has to shift every remaining item. Later you will use collections.deque for high-performance queues — but for learning-sized data, lists are fine.

Searching lists

in operator — check membership

fruits = ["apple", "banana", "cherry"]

print("banana" in fruits)
print("mango" in fruits)
print("mango" not in fruits)

index() — find position

fruits = ["apple", "banana", "cherry", "banana"]

print(fruits.index("banana"))
print(fruits.index("cherry"))

count() — count occurrences

scores = [88, 95, 72, 88, 91, 88, 76]

print(scores.count(88))
print(scores.count(100))

Sorting lists

sort() — sort in place

Sorts the list permanently, modifying it directly. Returns None:

numbers = [64, 34, 25, 12, 22, 11, 90]

numbers.sort()
print(numbers)

numbers.sort(reverse=True)
print(numbers)
names = ["Charlie", "Alice", "Eve", "Bob", "David"]

names.sort()
print(names)

names.sort(reverse=True)
print(names)

sorted() — sort without modifying the original

scores = [88, 95, 72, 91, 76]

sorted_scores = sorted(scores)
print(sorted_scores)
print(scores)

sort() vs sorted(): use sort() when you want to permanently reorder. Use sorted() when you need a sorted view but must keep the original order.

Sorting with a key — custom rules

The key parameter accepts a function that transforms each item before comparison:

words = ["banana", "apple", "kiwi", "cherry", "fig"]
words.sort(key=len)
print(words)

names = ["Charlie", "alice", "Bob", "david"]
names.sort(key=str.lower)
print(names)

students = [("Alice", 88), ("Bob", 95), ("Carol", 72)]
students.sort(key=lambda x: x[1])
print(students)

reverse() — reverse order in place

numbers = [1, 2, 3, 4, 5]
numbers.reverse()
print(numbers)

To reverse without modifying the original, use slicing:

original = [1, 2, 3, 4, 5]
reversed_copy = original[::-1]
print(reversed_copy)
print(original)

Copying lists — a critical concept

Assignment does not copy a list — it creates a second reference to the same list:

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

alias.append(6)
print(original)
print(alias)

Three common ways to make an independent shallow copy:

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

copy1 = original.copy()
copy2 = original[:]
copy3 = list(original)

copy1.append(6)
print(original)
print(copy1)

For lists containing other lists (nested structures), shallow copies still share inner lists — for deep independence use copy.deepcopy() from the copy module when you need it.

Shallow copies with nested lists (watch the inner lists)

outer = [[1, 2], [3, 4]]
dup = outer.copy()

dup[0][0] = 99
print(outer)
print(dup)

Both outer[0] and dup[0] refer to the same inner list. Changing one row through either name updates the shared inner object. When your rows must be independent, copy the inner lists too — for example [row[:] for row in matrix] for a two-level shallow structure, or use deepcopy when the nesting is deeper or irregular.

List comprehensions

A list comprehension creates a new list by applying an expression to each item in an iterable.

numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
    squares.append(n ** 2)
print(squares)
numbers = [1, 2, 3, 4, 5]
squares = [n ** 2 for n in numbers]
print(squares)

With a condition

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

evens = [n for n in numbers if n % 2 == 0]
print(evens)

big = [n for n in numbers if n > 5]
print(big)

odd_squares = [n ** 2 for n in numbers if n % 2 != 0]
print(odd_squares)

With strings

fruits = ["apple", "banana", "cherry", "mango"]

upper_fruits = [f.upper() for f in fruits]
print(upper_fruits)

long_fruits = [f for f in fruits if len(f) > 5]
print(long_fruits)

lengths = [len(f) for f in fruits]
print(lengths)

Use comprehensions when the logic stays readable. If nesting and conditions become hard to follow, prefer a normal for loop.

squares = [n ** 2 for n in range(1, 11)]
print(squares)

Nested comprehensions (powerful, easy to overdo)

Sometimes you build a list from two dimensions in one expression:

pairs = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]
print(pairs)

That is valid Python, but readability drops fast as conditions multiply. A good rule: if you need a comment to explain the comprehension, rewrite it as nested loops with clear variable names. Search engines and humans both reward the sections where sorting and comprehensions are shown with straight-line examples first — save the dense one-liners for when you genuinely need compactness.

enumerate(): loop with an index without range(len())

Beginners often write for i in range(len(items)): to access both index and value. enumerate() is clearer and harder to get wrong:

tasks = ["draft", "review", "ship"]

for i, task in enumerate(tasks):
    print(f"{i + 1}. {task}")

for i, task in enumerate(tasks, start=1):
    print(f"{i}. {task}")

enumerate() returns tuples (index, value) — the same tuple unpacking idea you will use everywhere in Python.

Lists from text: split(), strip(), and join()

Files and user input arrive as strings, but processing usually wants lists. str.split() breaks text into a list of parts:

line = "Alice,88,92,79"
parts = line.split(",")
print(parts)

sentence = "  learn   python   lists  "
words = sentence.split()
print(words)

split() with no argument splits on any whitespace and ignores leading/trailing gaps — perfect for turning a sentence into tokens. To clean each token, combine with a comprehension:

raw = ["  hi ", "BYE", " ok "]
clean = [w.strip().lower() for w in raw]
print(clean)

Going the other direction, str.join() builds a string from a list (the separator is the string you call join on):

tags = ["python", "lists", "tutorial"]
csv_like = ",".join(tags)
print(csv_like)

List equality: == vs is

Two lists are equal if their elements match in order, even if they are different objects in memory:

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)
print(a is b)

Use == for value comparisons. Use is only when you mean same object (aliases), which is rare in everyday list logic.

Stable sorting and tie-breakers

Python’s sort() and sorted() are stable: when two items compare equal, their relative order is preserved. That matters when you sort twice — for example, sort by last name, then sort by grade without shuffling names within the same grade. When you need multiple rules at once, prefer a tuple key so you sort by primary, then secondary:

rows = [
    ("Sam", "Lee", 88),
    ("Alex", "Kim", 92),
    ("Sam", "Pat", 88),
]

rows.sort(key=lambda r: (r[2], r[0], r[1]))
print(rows)

Here the primary key is the score r[2]; ties break on first name, then last name. This pattern shows up constantly in reports, leaderboards, and tables you export to CSV.

map(), filter(), and why comprehensions took over

Older tutorials (and some libraries) use map() and filter() with functions or lambdas. Both return iterators in Python 3, so you wrap them in list() when you need a real list:

nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, nums))
squares = list(map(lambda n: n * n, nums))
print(evens)
print(squares)

Today, list comprehensions and generator expressions are usually preferred because they read closer to English and keep the “what you are building” on the left. Still, you will see map/filter in the wild — especially in data pipelines — so it helps to recognize the pattern.

Nested lists — lists inside lists

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

print(matrix[0])
print(matrix[1])
print(matrix[0][0])
print(matrix[1][2])
print(matrix[2][1])

Student grade table:

students = [
    ["Alice", 88, 92, 79],
    ["Bob", 95, 87, 91],
    ["Carol", 72, 68, 75]
]

for student in students:
    name = student[0]
    scores_row = student[1:]
    average = sum(scores_row) / len(scores_row)
    print(f"{name}: avg = {average:.1f}")

A complete working program

# Student marks manager (stats after updating marks)
marks = [72, 88, 91, 65, 79, 95, 83, 70]
marks.append(88)
marks.remove(min(marks))  # removes 65

passed = [m for m in marks if m >= 70]
failed = [m for m in marks if m < 70]
distinction = [m for m in marks if m >= 85]

def get_grade(mark):
    if mark >= 85:
        return "Distinction"
    elif mark >= 70:
        return "Pass"
    else:
        return "Fail"

graded = [(m, get_grade(m)) for m in marks]

print(f"=== Marks Report ===")
print(f"Total students:  {len(marks)}")
print(f"Average score:   {sum(marks) / len(marks):.1f}")
print(f"Highest:         {max(marks)}")
print(f"Lowest:          {min(marks)}")
print(f"Passed:          {len(passed)} students")
print(f"Failed:          {len(failed)} students")
print(f"Distinctions:    {len(distinction)} students")
print(f"Ascending order: {sorted(marks)}")
print(f"Grade breakdown: {graded}")

Output:

=== Marks Report ===
Total students:  8
Average score:   83.3
Highest:         95
Lowest:          70
Passed:          8 students
Failed:          0 students
Distinctions:    4 students
Ascending order: [70, 72, 79, 83, 88, 88, 91, 95]
Grade breakdown: [(72, 'Pass'), (88, 'Pass'), (91, 'Distinction'), (79, 'Pass'), (95, 'Distinction'), (83, 'Pass'), (70, 'Pass'), (88, 'Pass')]

Five list mistakes every beginner makes

Mistake 1: capturing the return value of in-place methods

fruits = ["cherry", "apple", "banana"]

# sorted_fruits = fruits.sort()   # None — wrong

fruits.sort()
print(fruits)

sorted_fruits = sorted(fruits)
print(sorted_fruits)

append(), sort(), reverse(), insert(), remove(), extend(), and clear() return None.

Mistake 2: aliasing instead of copying

original = [1, 2, 3]
alias = original
alias.append(4)
print(original)

copy = original.copy()
copy.append(99)
print(original)

Mistake 3: modifying a list while iterating over it

numbers = [1, 2, 3, 4, 5, 6]

# Safer: iterate over a copy
for n in numbers[:]:
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)

# Cleaner: rebuild with a comprehension
numbers = [1, 2, 3, 4, 5, 6]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers)

Mistake 4: remove() when the value might be missing

items = ["apple", "banana"]
if "mango" in items:
    items.remove("mango")

Mistake 5: append() vs extend()

numbers = [1, 2, 3]
numbers.append([4, 5])
print(numbers)

numbers = [1, 2, 3]
numbers.extend([4, 5])
print(numbers)

Practice: list exercises

Exercise 1 — list builder: Create an empty list. Use append() for five city names, insert() for a sixth at index 2, print the list and len().

Exercise 2 — sort and filter: Given temperatures = [38, 22, 31, 19, 44, 27, 35, 16, 29], print ascending order without mutating the original, values above 30, hottest and coldest.

Exercise 3 — comprehensions: One comprehension each: squares 1–15; words longer than 4 chars from ["sky", "ocean", "river", "mountain", "lake", "valley"]; evens from 1–50.

Exercise 4 — grade calculator: Given scores = [85, 92, 78, 96, 61, 74, 88, 55, 90, 83], print class average, pass count (60+), highest, lowest, and scores descending.

Exercise 5 — safe operations: Five names; safely remove one that exists; guard removal of a missing name; copy and modify copy only.

See all Python list exercises with solutions

For all topics, see Python exercises.

What comes next — Day 6: tuples and sets

Lists are ordered and mutable. Day 6 introduces tuples (ordered, immutable) and sets (unordered, unique). Choosing the right collection is a core design skill.

Continue to Day 6: Tuples and Sets

Chapter navigation

Frequently asked questions: Lists and list methods

What is a list in Python?

A list is an ordered, mutable collection of items stored in a single variable using square brackets. Lists can hold any data type, allow duplicates, and can be modified after creation — items can be added, removed, or changed at any time. They are the most commonly used data structure in Python.

What is the difference between append() and extend() in Python lists?

append() adds its argument as a single item to the end of the list. If you append a list, that list becomes one nested item. extend() iterates over its argument and adds each element individually, keeping the result flat. Use append() for single items, extend() for merging collections.

What is the difference between sort() and sorted() in Python?

sort() is a list method that modifies the list in place and returns None. sorted() is a built-in function that leaves the original list unchanged and returns a new sorted list. Use sort() when you want to permanently reorder the list. Use sorted() when you need a sorted version but must preserve the original order.

What is a list comprehension in Python?

A list comprehension is a concise syntax for creating a new list by applying an expression to each item in an iterable, optionally filtering with a condition. [n ** 2 for n in range(10)] creates a list of squares in one line. They are more readable and often faster than equivalent for loops that build lists with append().

How do I copy a list in Python without the copy being affected by changes to the original?

Use list.copy(), slice notation original[:], or the list() constructor. All three create a shallow copy — a new list with the same items. Simple assignment (copy = original) does not copy the list — both variables then reference the same object in memory.

Why does sort() return None in Python?

Python follows a design principle called command-query separation — methods that modify an object (commands) return None, while methods that return data (queries) leave the object unchanged. sort() modifies the list in place (command), so it returns None. sorted() does not modify anything and returns the new list (query). This prevents the common mistake of chaining modifications and accidentally losing data.

What is the difference between remove() and pop() in Python?

remove() deletes the first occurrence of a specific value. pop() deletes an item at a specific index and returns that item. Use remove() when you know the value but not its position. Use pop() when you know the position or want to retrieve the item as you remove it — common in stack and queue patterns.

Can a Python list contain different data types?

Yes. A Python list can hold any mix of data types — strings, integers, floats, booleans, other lists, or any other Python object. In practice, most real-world lists contain one consistent type because mixed-type lists are harder to process reliably with loops and comprehensions.