Lists: Intermediate

Lyft's dispatch system matches riders to drivers in under 100 milliseconds, and at the heart of that speed is sorted list logic applied to real-time data. When a rider requests a trip, Python sorts a list of nearby available drivers using a key function that weighs distance, estimated arrival time, and driver rating simultaneously, then slices the top candidates for final selection. The list comprehensions and custom sort keys you will learn in this lesson are exactly the kind of operations powering that sub-second decision. Master these techniques and you will be able to rank, filter, and extract subsets of data with the same efficiency Lyft applies at millions of trips per day.

List Slicing

Daily Life
Interviews

Extract portions of a list efficiently

In the beginner lesson, you learned to access individual items using a single index like list[0] or list[-1]. Slicing extends this concept to extract multiple items at once. Instead of one index, you provide a range using the colon character. The result is a new list containing just the items in that range.

The basic slice syntax is list[start:end]. The start index is included in the result, but the end index is excluded. This might seem counterintuitive at first, but it has practical benefits: the number of items in the slice equals end minus start, and consecutive slices fit together without gaps or overlaps.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2
3# Extract from index 2 to 6 (exclusive)
4middle = numbers[2:6]
5print(middle)
6
7# Extract the first three items
8first_three = numbers[0:3]
9print(first_three)
10
11# Last three items (indices 7, 8, 9)
12last_three = numbers[7:10]
13print(last_three)
>>>Output
[2, 3, 4, 5]
[0, 1, 2]
[7, 8, 9]

The slice [2:6] gives us items at indices 2, 3, 4, and 5. Notice that index 6 is NOT included. The slice contains exactly 6 - 2 = 4 items. Similarly, [0:3] gives indices 0, 1, 2 (three items), and [7:10] gives indices 7, 8, 9 (three items).

Why Exclude the End Index?

The exclusive end index is a deliberate design choice with several advantages. First, the length of the slice equals end - start, making calculations easy. Second, you can split a list into non-overlapping parts: list[:n] and list[n:] together contain all items exactly once. Third, it matches how range() works, creating consistency throughout Python.

1data = [10, 20, 30, 40, 50, 60, 70, 80]
2
3# Split into two halves
4first_half = data[:4]
5second_half = data[4:]
6
7print("First half:", first_half)
8print("Second half:", second_half)
9print("Combined length:", len(first_half) + len(second_half))
10print("Original length:", len(data))
>>>Output
First half: [10, 20, 30, 40]
Second half: [50, 60, 70, 80]
Combined length: 8
Original length: 8
The two halves split cleanly at index 4. No item appears in both halves, and no item is missing. The combined length equals the original length. This clean division is possible because the end of one slice equals the start of the next.

Omitting Start or End

You can omit the start index, the end index, or both. Omitting start means "from the beginning." Omitting end means "to the end." This gives you convenient shortcuts for common operations.
1letters = ["a", "b", "c", "d", "e", "f", "g"]
2
3# From beginning to index 3 (first 3 items)
4print(letters[:3])
5
6# From index 4 to end
7print(letters[4:])
8
9# Everything (creates a copy)
10print(letters[:])
>>>Output
['a', 'b', 'c']
['e', 'f', 'g']
['a', 'b', 'c', 'd', 'e', 'f', 'g']

The expression letters[:3] means "from the start up to index 3." The expression letters[4:] means "from index 4 to the end." The expression letters[:] with both omitted means "from start to end," which creates a complete copy of the list.

list[:n]list[n:]list[-n:]list[:-n]list[:]
list[:n]
First n items
Start to index n, excluded
list[n:]
From n onward
Index n through the end
list[-n:]
Last n items
Tail of the list by count
list[:-n]
Drop last n
Everything except the tail
list[:]
Full copy
Shallow copy of all items

Negative Indices in Slices

Just as you can use negative indices to access individual items from the end, you can use them in slices. This is extremely useful when you want to work with the tail of a list without knowing its exact length.
1scores = [85, 90, 78, 92, 88, 95, 87, 91]
2
3# Last 3 scores
4recent = scores[-3:]
5print("Last 3:", recent)
6
7# Everything except last 2
8most = scores[:-2]
9print("All but last 2:", most)
10
11# From 5th-last to 2nd-last
12window = scores[-5:-2]
13print("Window:", window)
>>>Output
Last 3: [95, 87, 91]
All but last 2: [85, 90, 78, 92, 88, 95]
Window: [88, 95, 87]

The slice [-3:] gives the last 3 items. The slice [:-2] gives everything except the last 2. The slice [-5:-2] gives a window from the 5th-last item up to (but not including) the 2nd-last item.

The Step Parameter

Slices can take a third parameter called the step, written as list[start:end:step]. The step determines how many positions to advance between each item. A step of 2 takes every other item, a step of 3 takes every third item, and so on.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2
3# Every other item (step of 2)
4evens = numbers[::2]
5print("Every other:", evens)
6
7# Every other, starting from index 1
8odds = numbers[1::2]
9print("Odd indices:", odds)
10
11# Every third item
12thirds = numbers[::3]
13print("Every third:", thirds)
>>>Output
Every other: [0, 2, 4, 6, 8]
Odd indices: [1, 3, 5, 7, 9]
Every third: [0, 3, 6, 9]

When you use a step, you can still omit start and end to mean "from beginning" and "to end." The expression [::2] means "from start to end, taking every second item." The expression [1::2] means "start at index 1, go to end, taking every second item."

Negative Step - Reversing

A negative step moves backward through the list. The most common use is step -1, which reverses the list. When using a negative step, the start index should be greater than the end index, since you are moving in reverse.
1letters = ["a", "b", "c", "d", "e"]
2
3# Reverse the entire list
4reversed_list = letters[::-1]
5print("Reversed:", reversed_list)
6
7# Reverse a portion (indices 1 through 3, backwards)
8portion = letters[3:0:-1]
9print("Portion reversed:", portion)
10
11# Every other item, reversed
12every_other_reversed = letters[::-2]
13print("Every other reversed:", every_other_reversed)
>>>Output
Reversed: ['e', 'd', 'c', 'b', 'a']
Portion reversed: ['d', 'c', 'b']
Every other reversed: ['e', 'c', 'a']

The idiom [::-1] is the standard Python way to reverse a sequence. It creates a new reversed list without modifying the original. This works on strings and tuples too, making it a universally useful pattern.

Experiment with different slice parameters to see how start, end, and step change the result. Try all four combinations to understand how slicing works.
Fill in the Blank

> You have a list [10, 20, 30, 40, 50, 60] and need to extract a portion of it. Pick a slice notation to see which elements come back.

nums = [10, 20, 30, 40, 50, 60]
result = nums[]
print(result)

Slices Create Copies

An important property of slicing is that it always creates a new list. The slice is a copy of the selected portion, not a view into the original. Modifying the slice does not affect the original list, and modifying the original does not affect the slice.
1original = [1, 2, 3, 4, 5]
2slice_copy = original[1:4]
3
4print("Original:", original)
5print("Slice:", slice_copy)
6
7# Modify the slice
8slice_copy[0] = 99
9print("After modifying slice:")
10print("Original:", original)
11print("Slice:", slice_copy)
>>>Output
Original: [1, 2, 3, 4, 5]
Slice: [2, 3, 4]
After modifying slice:
Original: [1, 2, 3, 4, 5]
Slice: [99, 3, 4]

When we changed slice_copy[0] to 99, the original list remained unchanged at [1, 2, 3, 4, 5]. This is because slicing created an independent copy. This behavior is called a shallow copy, which we will explore more in the advanced lesson.

TIP
Use list[:] to create a quick shallow copy of an entire list. This is equivalent to list.copy() but some developers prefer the slice notation for its brevity.

Combining Lists

Daily Life
Interviews

Merge multiple lists together

Often you need to combine data from multiple lists into one. Python provides several ways to do this, each with different behaviors. The extend() method adds items from one list to another, modifying the original. The + operator creates a new combined list without changing the originals. Understanding when to use each approach is essential for writing clear, efficient code.

The extend() Method

The .extend() method takes all items from another iterable (like a list) and adds them to the end of the current list. It modifies the list in place and returns None. The syntax is list1.extend(list2).

1team_a = ["Alice", "Bob", "Charlie"]
2team_b = ["Diana", "Eve"]
3
4print("Team A before:", team_a)
5print("Team B before:", team_b)
6
7team_a.extend(team_b)
8
9print("Team A after:", team_a)
10print("Team B after:", team_b)
>>>Output
Team A before: ['Alice', 'Bob', 'Charlie']
Team B before: ['Diana', 'Eve']
Team A after: ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']
Team B after: ['Diana', 'Eve']

Team A grew from 3 members to 5 members by adding everyone from Team B. Team B itself was not modified; its items were copied into Team A. The extend() method modified team_a directly rather than creating a new list.

extend() vs append()

One of the most common mistakes with lists is confusing extend() with append(). They behave very differently. The append() method adds its argument as a single item, even if that argument is a list. The extend() method unpacks its argument and adds each item individually.

1# Using append with a list
2list1 = [1, 2, 3]
3list1.append([4, 5, 6])
4print("After append:", list1)
5print("Length:", len(list1))
6
7# Using extend with a list
8list2 = [1, 2, 3]
9list2.extend([4, 5, 6])
10print("After extend:", list2)
11print("Length:", len(list2))
>>>Output
After append: [1, 2, 3, [4, 5, 6]]
Length: 4
After extend: [1, 2, 3, 4, 5, 6]
Length: 6

With append(), the entire list [4, 5, 6] became a single nested element at index 3. The result is a list of 4 items, where the last item is itself a list. With extend(), the items 4, 5, and 6 were added individually. The result is a flat list of 6 integers.

append()
  • Adds ONE item
  • Item can be any type
  • list.append([4,5]) adds [4,5] as one nested element
  • Result: [1, 2, 3, [4, 5]]
extend()
  • Adds MULTIPLE items
  • Unpacks the argument
  • list.extend([4,5]) adds 4 and 5 separately
  • Result: [1, 2, 3, 4, 5]
Choosing the wrong method when combining lists is one of the most frequent sources of nested-list bugs. Keep these rules in mind as you work with multi-source data.
When to Use Each Combination Method
  • Use append() only when adding a single item, never another list.
  • Use extend() when merging two flat lists into one.
  • Use the + operator when you need both original lists unchanged.
  • Use += as shorthand for extend() when you want in-place growth.

extend() with Any Iterable

The extend() method accepts any iterable, not just lists. An iterable is any object you can loop over: strings, tuples, ranges, and more. Each item from the iterable is added to the list.

1numbers = [1, 2, 3]
2
3# Extend with a tuple
4numbers.extend((4, 5))
5print("After tuple:", numbers)
6
7# Extend with a range
8numbers.extend(range(6, 9))
9print("After range:", numbers)
10
11letters = ["a", "b"]
12letters.extend("cd")
13print("After string:", letters)
>>>Output
After tuple: [1, 2, 3, 4, 5]
After range: [1, 2, 3, 4, 5, 6, 7, 8]
After string: ['a', 'b', 'c', 'd']
Notice what happened with the string "cd": each character became a separate item in the list. This is because strings are iterable, and iterating over a string yields individual characters. This behavior can be surprising if you expected the whole string to be added as one item.
TIP
If you want to add a string as a single item, use append("cd"). If you want to add each character separately, use extend("cd"). Choose based on what you actually need.

+ Operator: Concatenation

The + operator creates a new list by concatenating two lists together. Unlike extend(), it does not modify either original list. This is useful when you need to combine lists but want to preserve the originals, or when you want to chain multiple concatenations.

1first = [1, 2, 3]
2second = [4, 5, 6]
3third = [7, 8, 9]
4
5# Concatenate two lists
6combined = first + second
7print("Combined:", combined)
8print("First unchanged:", first)
9
10# Chain multiple concatenations
11all_numbers = first + second + third
12print("All numbers:", all_numbers)
>>>Output
Combined: [1, 2, 3, 4, 5, 6]
First unchanged: [1, 2, 3]
All numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9]

The + operator returns a new list without changing first, second, or third. You can chain multiple + operators to combine many lists in one expression. The result is evaluated left to right: (first + second) + third.

extend()
  • Modifies list in place
  • Returns None
  • More memory efficient
  • Use when you want to grow a list
+ operator
  • Creates new list
  • Returns the combined list
  • Preserves originals
  • Use when you need both lists unchanged

The += Operator

The += operator combines assignment with concatenation. For lists, list1 += list2 is equivalent to list1.extend(list2). It modifies list1 in place rather than creating a new list.

1numbers = [1, 2, 3]
2print("Before:", numbers)
3print("ID before:", id(numbers))
4
5numbers += [4, 5, 6]
6print("After:", numbers)
7print("ID after:", id(numbers))
8print("Same object:", id(numbers) == id(numbers))
>>>Output
Before: [1, 2, 3]
ID before: 4376883328
After: [1, 2, 3, 4, 5, 6]
ID after: 4376883328
Same object: True

The id() function returns a unique identifier for an object in memory. Notice that the ID remains the same before and after the += operation, proving that we modified the same list object rather than creating a new one. This is more memory-efficient than using + and reassigning.

* Operator: Repetition

The * operator repeats a list a specified number of times. This is useful for initializing lists with default values or creating patterns.

1# Create a list of zeros
2zeros = [0] * 5
3print("Zeros:", zeros)
4
5# Create a pattern
6pattern = [1, 2] * 4
7print("Pattern:", pattern)
8
9# Initialize a grid row
10row_template = ["-"] * 3
11print("Row:", row_template)
>>>Output
Zeros: [0, 0, 0, 0, 0]
Pattern: [1, 2, 1, 2, 1, 2, 1, 2]
Row: ['-', '-', '-']

The expression [0] * 5 creates a new list with five zeros. The expression [1, 2] * 4 repeats the two-element pattern four times for eight total elements. This is a quick way to initialize lists of known sizes.

Python Quiz

> Combine two lists into a new one without modifying the originals. Pick the operator that joins lists, and the built-in that proves the original list was not changed.

a = [1, 2, 3]
b = [4, 5]
combined = a ___ b
print(len(combined))
print(___(a))
-
+
len
sum
*

The + operator always creates a new list, making it safe to use when you need to preserve both originals. Use extend() when you want to grow a list in place without creating a new object.

The * operator is a quick way to initialize lists with repeated values, like [0] * 10 for ten zeros. Be careful using * with nested mutable objects, since all copies share the same inner reference.

Slicing and combining operations give you the flexibility to reshape list data before passing it to functions, merging datasets, or preparing output for display.

Sorting Lists

Daily Life
Interviews

Arrange items in any custom order

Sorting is one of the most fundamental operations in programming. Python provides two ways to sort lists: the sort() method modifies the list in place, while the sorted() function returns a new sorted list. Both use the same underlying algorithm and accept the same customization options.

The sort() Method

The .sort() method sorts a list in place, meaning it modifies the original list and returns None. By default, it sorts in ascending order: smallest to largest for numbers, alphabetical for strings.

1numbers = [64, 34, 25, 12, 22, 11, 90]
2print("Before sort:", numbers)
3
4numbers.sort()
5print("After sort:", numbers)
6
7words = ["banana", "apple", "cherry", "date"]
8words.sort()
9print("Sorted words:", words)
>>>Output
Before sort: [64, 34, 25, 12, 22, 11, 90]
After sort: [11, 12, 22, 25, 34, 64, 90]
Sorted words: ['apple', 'banana', 'cherry', 'date']
Numbers are sorted from smallest to largest. Strings are sorted alphabetically based on Unicode code points, which means uppercase letters come before lowercase letters in a pure ASCII comparison. The original lists are modified directly.

The sorted() Function

The built-in sorted() function takes any iterable and returns a new sorted list, leaving the original unchanged. This is useful when you need both the original order and a sorted version.

1original = [3, 1, 4, 1, 5, 9, 2, 6]
2sorted_copy = sorted(original)
3
4print("Original:", original)
5print("Sorted copy:", sorted_copy)
6
7# sorted() works on any iterable
8text = "python"
9sorted_letters = sorted(text)
10print("Sorted letters:", sorted_letters)
>>>Output
Original: [3, 1, 4, 1, 5, 9, 2, 6]
Sorted copy: [1, 1, 2, 3, 4, 5, 6, 9]
Original: [3, 1, 4, 1, 5, 9, 2, 6]
Sorted letters: ['h', 'n', 'o', 'p', 't', 'y']
The original list remains [3, 1, 4, 1, 5, 9, 2, 6] after calling sorted(). The sorted_copy is a new list containing the same values in sorted order. Notice that sorted() can work on strings too, returning a list of sorted characters.
sort()
  • Method on list objects
  • Modifies list in place
  • Returns None
  • Only works on lists
sorted()
  • Built-in function
  • Creates new sorted list
  • Returns the sorted list
  • Works on any iterable

Sorting in Reverse Order

Both sort() and sorted() accept a reverse=True parameter to sort in descending order instead of ascending.

1numbers = [3, 1, 4, 1, 5, 9, 2, 6]
2
3# Ascending (default)
4ascending = sorted(numbers)
5print("Ascending:", ascending)
6
7# Descending
8descending = sorted(numbers, reverse=True)
9print("Descending:", descending)
10
11# In-place reverse sort
12numbers.sort(reverse=True)
13print("In-place descending:", numbers)
>>>Output
Ascending: [1, 1, 2, 3, 4, 5, 6, 9]
Descending: [9, 6, 5, 4, 3, 2, 1, 1]
In-place descending: [9, 6, 5, 4, 3, 2, 1, 1]

Custom Sort Keys

The key parameter lets you customize how items are compared. You pass a function that takes an item and returns the value to use for comparison. This is extremely powerful for sorting complex data.

1# Sort strings by length
2words = ["elephant", "cat", "dog", "butterfly", "ant"]
3by_length = sorted(words, key=len)
4print("By length:", by_length)
5
6# Sort strings case-insensitively
7mixed = ["Banana", "apple", "Cherry", "date"]
8case_insensitive = sorted(mixed, key=str.lower)
9print("Case insensitive:", case_insensitive)
10
11# Sort by absolute value
12numbers = [-5, 2, -8, 1, -3, 7]
13by_abs = sorted(numbers, key=abs)
14print("By absolute value:", by_abs)
>>>Output
By length: ['cat', 'dog', 'ant', 'elephant', 'butterfly']
Case insensitive: ['apple', 'Banana', 'Cherry', 'date']
By absolute value: [1, 2, -3, -5, 7, -8]

The key=len sorts strings by their length. The key=str.lower converts each string to lowercase for comparison, achieving case-insensitive sorting. The key=abs sorts numbers by their distance from zero, ignoring the sign.

Sorting complex objects like dictionaries or tuples:
1students = [
2 ("Alice", 85),
3 ("Bob", 92),
4 ("Charlie", 78),
5 ("Diana", 92)
6]
7
8# Sort by score (second element of each tuple)
9by_score = sorted(students, key=lambda x: x[1])
10print("By score:", by_score)
11
12# Sort by score descending
13by_score_desc = sorted(students, key=lambda x: x[1], reverse=True)
14print("By score (desc):", by_score_desc)
>>>Output
By score: [('Charlie', 78), ('Alice', 85), ('Bob', 92), ('Diana', 92)]
By score (desc): [('Bob', 92), ('Diana', 92), ('Alice', 85), ('Charlie', 78)]

The lambda x: x[1] is a small anonymous function that returns the second element of each tuple (the score). Lambda functions are useful when you need a simple key function without defining a named function.

A very common bug is assigning the result of sort() and expecting a sorted list. Since sort() returns None, the variable ends up empty. Fix the code below by removing the offending tiles.

Debug Challenge

> This code assigns the result of .sort() to scores, but sort() modifies the list in place and returns None. The variable becomes None.

TypeError: scores is None because sort() returns None

The in-place vs return-value distinction applies to many Python methods. sort() and reverse() modify in place and return None, while sorted() and reversed() return new objects without touching the original.

Use sort() when you do not need to preserve the original order, saving memory. Use sorted() when you need both the original and a sorted version available at the same time.

Custom sorting with the key= argument is one of the most powerful features Python offers. Any function that maps an item to a comparable value can serve as a key, giving you precise control over sort order.

Reversing Lists

Daily Life
Interviews

Flip list order for different traversals

Python provides multiple ways to reverse a list, each suited for different situations. The reverse() method modifies in place, the reversed() function returns an iterator, and slice notation creates a reversed copy.

The reverse() Method

The .reverse() method reverses a list in place and returns None. The original list is modified.

1letters = ["a", "b", "c", "d", "e"]
2print("Before:", letters)
3
4letters.reverse()
5print("After:", letters)
>>>Output
Before: ['a', 'b', 'c', 'd', 'e']
After: ['e', 'd', 'c', 'b', 'a']

The reversed() Function

The built-in reversed() function returns an iterator that yields items in reverse order. To get a list, you must wrap it with list().

1original = [1, 2, 3, 4, 5]
2
3rev_iterator = reversed(original)
4print("Iterator:", rev_iterator)
5
6# Convert to list
7rev_list = list(reversed(original))
8print("Reversed list:", rev_list)
9print("Original unchanged:", original)
>>>Output
Iterator: <list_reverseiterator object at 0x...>
Reversed list: [5, 4, 3, 2, 1]
Original unchanged: [1, 2, 3, 4, 5]

The reversed() function is memory-efficient because it does not create a new list immediately. It creates an iterator that generates items on demand. This is useful when you only need to iterate through the reversed order once.

Slice Reversal

As you learned earlier, the slice [::-1] creates a reversed copy. This is often the most concise approach when you need a reversed list.

1original = [1, 2, 3, 4, 5]
2reversed_copy = original[::-1]
3
4print("Original:", original)
5print("Reversed copy:", reversed_copy)
>>>Output
Original: [1, 2, 3, 4, 5]
Reversed copy: [5, 4, 3, 2, 1]
list.reverse()
list.reverse()
Modifies the list in place and returns None. The original order is lost.
reversed(list)
reversed(list)
Returns a memory-efficient iterator. Wrap with list() to get a list.
list[::-1]
list[::-1]
Creates a reversed copy in one expression. The original stays unchanged.

Searching in Lists

Daily Life
Interviews

Find items and check membership quickly

Beyond checking if an item exists with the "in" operator (covered in the beginner lesson), Python provides methods to find where items are located and how many times they appear. These are essential for data analysis and processing.

The index() Method

The .index(value) method returns the index of the first occurrence of a value. If the value is not found, it raises a ValueError.

1fruits = ["apple", "banana", "cherry", "banana", "date"]
2
3# Find the first occurrence
4banana_index = fruits.index("banana")
5print("First banana at index:", banana_index)
6
7cherry_index = fruits.index("cherry")
8print("Cherry at index:", cherry_index)
>>>Output
First banana at index: 1
Cherry at index: 2

Even though "banana" appears twice (at indices 1 and 3), index() returns only the first occurrence at index 1. The method scans from the beginning and stops as soon as it finds a match.

Search from Any Position

You can provide a starting index to begin the search from a specific position: list.index(value, start). You can also provide an end index: list.index(value, start, end).

1numbers = [5, 10, 15, 10, 20, 10, 25]
2
3# Find first 10
4first_ten = numbers.index(10)
5print("First 10 at:", first_ten)
6
7# Find 10 starting after index 2
8second_ten = numbers.index(10, 2)
9print("Second 10 at:", second_ten)
10
11# Find 10 starting after index 4
12third_ten = numbers.index(10, 5)
13print("Third 10 at:", third_ten)
>>>Output
First 10 at: 1
Second 10 at: 3
Third 10 at: 5
By starting the search at different positions, we can find each successive occurrence of the value 10. This is useful when you need to process all occurrences of a value.

Handling Not Found Errors

If the value is not in the list, index() raises a ValueError. You should handle this case either by checking with in first, or by using try/except.

1fruits = ["apple", "banana", "cherry"]
2search_item = "mango"
3
4# Method 1: Check first
5if search_item in fruits:
6 position = fruits.index(search_item)
7 print(f"Found at index {position}")
8else:
9 print(f"{search_item} not in list")
10
11# Method 2: Try/except
12try:
13 position = fruits.index("grape")
14 print(f"Found grape at {position}")
15except ValueError:
16 print("Grape not found")
>>>Output
mango not in list
Grape not found

The count() Method

The .count(value) method returns the number of times a value appears in the list. If the value is not found, it returns 0 rather than raising an error.

1votes = ["A", "B", "A", "A", "C", "B", "A", "C", "A"]
2
3a_count = votes.count("A")
4b_count = votes.count("B")
5c_count = votes.count("C")
6d_count = votes.count("D")
7
8print(f"A votes: {a_count}")
9print(f"B votes: {b_count}")
10print(f"C votes: {c_count}")
11print(f"D votes: {d_count}")
>>>Output
A votes: 5
B votes: 2
C votes: 2
D votes: 0

The count() method scans the entire list and counts matches. "D" appears 0 times, which is returned as 0 rather than causing an error. This makes count() safe to use without checking if the value exists first.

index()
  • Returns position of first match
  • Raises ValueError if not found
  • Can specify start/end positions
  • Use when you need location
count()
  • Returns total occurrences
  • Returns 0 if not found
  • Always scans entire list
  • Use when you need frequency

Finding All Indices

Sometimes you need to find all positions where a value appears, not just the first. You can combine index() with a loop, or use a list comprehension with enumerate().

1letters = ["a", "b", "a", "c", "a", "d", "a"]
2
3# Find all indices of "a" using enumeration
4all_a_indices = [i for i, letter in enumerate(letters) if letter == "a"]
5print("All 'a' positions:", all_a_indices)
6
7# Count confirms the result
8print("Total 'a' count:", letters.count("a"))
>>>Output
All 'a' positions: [0, 2, 4, 6]
Total 'a' count: 4

The enumerate() function yields pairs of (index, value) as you iterate. The list comprehension keeps only those indices where the letter equals "a". This is more efficient than calling index() repeatedly.

TIP
For frequent lookups in large lists, consider using a dictionary or set instead. The in operator, index(), and count() all have O(n) complexity, meaning they slow down as the list grows.
Knowing which method to reach for in different situations is key to writing clean, efficient list code. Here is a quick reference for the patterns covered in this lesson.
Do
  • Use sorted() to keep originals
  • Use extend() for flat merging
  • Use [::-1] for quick reversal
  • Use key= for custom sorting
Don't
  • Assign result of sort()
  • Use append() to merge lists
  • Mutate list during iteration
  • Forget end index is exclusive
01
Slice
Extract portions with [start:end:step]; end is always excluded
02
Combine
Use extend() to merge in place or + to create a new combined list
03
Sort
sort() changes in place (returns None); sorted() returns a new list
04
Reverse
reverse() in place, reversed() iterator, or [::-1] for a copy
05
Search
index() finds position (may raise); count() tallies occurrences safely
Python Quiz

> Find the position of an item and count how many times another item appears. Pick the method that locates the first occurrence, and the one that tallies all occurrences.

letters = ["a", "b", "c", "b", "a"]
pos = letters.___("c")
total = letters.___("b")
print(pos)
print(total)
find
index
len
count

index() raises a ValueError if the item is not found, so wrap it in a try-except block or check with in first when the item might be absent.

count() is a safe alternative to index() when you just need to know whether duplicates exist. A count of 0 means the item is absent; any positive number tells you exactly how many times it appears.

Efficient list manipulation is at the heart of most data processing tasks. Put these techniques to the test with hands-on challenges in the Python Builder.
PUTTING IT ALL TOGETHER

> You are a data engineer at Datadog processing a day's worth of hourly CPU metrics: you slice the list to isolate peak hours, sort readings to surface anomalies, combine lists from multiple hosts, reverse the sequence for a most-recent-first report, and search for the index of a threshold-crossing value.

Slicing with [start:end] extracts only the peak-hour window from the full 24-entry hourly metrics list.
.sort() reorders all readings in place so the highest CPU spikes appear at the top of the anomaly report.
.extend() merges the metrics lists from multiple monitored hosts into a single unified dataset for cross-host analysis.
.index() locates the exact position of the first reading that crosses the critical threshold so the timestamp can be flagged.
KEY TAKEAWAYS
Slicing uses [start:end:step] - start included, end excluded
Omit start for beginning, end for rest: [:3], [3:], [:]
[::-1] reverses a list; all slices create copies
.extend() adds each item from an iterable; .append() adds one item
The + operator creates a new combined list; += modifies in place
.sort() modifies in place; sorted() returns a new list
Use reverse=True for descending order; key=func for custom comparisons
.index(x) finds first position (raises if not found)
.count(x) counts occurrences (returns 0 if not found)

Slice, combine, and organize your data

Category
Python
Difficulty
intermediate
Duration
43 minutes
Challenges
0 hands-on challenges

Topics covered: List Slicing, Combining Lists, Sorting Lists, Reversing Lists, Searching in Lists

Lesson Sections

  1. List Slicing

    Why Exclude the End Index? The two halves split cleanly at index 4. No item appears in both halves, and no item is missing. The combined length equals the original length. This clean division is possible because the end of one slice equals the start of the next. Omitting Start or End You can omit the start index, the end index, or both. Omitting start means "from the beginning." Omitting end means "to the end." This gives you convenient shortcuts for common operations. Negative Indices in Slices

  2. Combining Lists

    The extend() Method extend() vs append() Choosing the wrong method when combining lists is one of the most frequent sources of nested-list bugs. Keep these rules in mind as you work with multi-source data. extend() with Any Iterable Notice what happened with the string "cd": each character became a separate item in the list. This is because strings are iterable, and iterating over a string yields individual characters. This behavior can be surprising if you expected the whole string to be added

  3. Sorting Lists (concepts: pyListSort)

    The sort() Method Numbers are sorted from smallest to largest. Strings are sorted alphabetically based on Unicode code points, which means uppercase letters come before lowercase letters in a pure ASCII comparison. The original lists are modified directly. The sorted() Function The original list remains [3, 1, 4, 1, 5, 9, 2, 6] after calling sorted(). The sorted_copy is a new list containing the same values in sorted order. Notice that sorted() can work on strings too, returning a list of sorted

  4. Reversing Lists

    The reverse() Method The reversed() Function Slice Reversal

  5. Searching in Lists (concepts: pyListSearch)

    Beyond checking if an item exists with the "in" operator (covered in the beginner lesson), Python provides methods to find where items are located and how many times they appear. These are essential for data analysis and processing. The index() Method Search from Any Position By starting the search at different positions, we can find each successive occurrence of the value 10. This is useful when you need to process all occurrences of a value. Handling Not Found Errors The count() Method Finding