Execute Program

Python for Programmers: List Slicing

Welcome to the List Slicing lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We often want just a part of a list, like "only the first 4 elements", or "only elements 7 through 11", or "only the last 6 elements". Python's list slicing operator lets us do all of those and more.

  • some_list[2:] gives us a list containing every element whose index is greater than or equal to 2.

  • >
    visitors = ["Amir", "Betty", "Cindy", "Dalili"]
    visitors[2:]
    Result:
    ['Cindy', 'Dalili']Pass Icon
  • If we put a number after the :, we get every element up to, but not including, that index. :2 gives us indexes 0 and 1.

  • >
    visitors = ["Amir", "Betty", "Cindy", "Dalili"]
    visitors[:2]
    Result:
    ['Amir', 'Betty']Pass Icon
  • We can also specify the start and end index together. some_list[1:3] starts at some_list[1] and goes to some_list[3], but doesn't include some_list[3]. That is, it only includes indexes 1 and 2.

  • >
    visitors = ["Amir", "Betty", "Cindy", "Dalili"]
    visitors[1:3]
    Result:
    ['Betty', 'Cindy']Pass Icon
  • Slicing is useful when combined with negative indexing. some_list[:-2] returns all list elements except for the last 2. We can think of it as "all elements up to, but not including, the last 2 elements".

  • >
    winning_ids = [10, 5, 3, 2, 1]
    winning_ids[:-2]
    Result:
    [10, 5, 3]Pass Icon
  • If we slice outside of a list's bounds, we get an empty list.

  • >
    some_list = [1, 2, 3]
    some_list[5:]
    Result:
    []Pass Icon
  • If the sliced range only partially overlaps with the actual list indexes, we get the part of the list that falls within the range.

  • >
    some_list = [1, 2, 3]
    some_list[2:20]
    Result:
    [3]Pass Icon
  • This slicing behavior is a bit unusual for Python. In most other situations, Python is conservative about list indexes. For example, it raises an exception when we access indexes that don't exist. That makes sense for simple list indexing like some_list[some_index]: if we accidentally index past the end of the list, that's probably a bug that we want to know about.

  • With list slicing, we'd have to do some tedious math to make sure that every index in our slice is actually within the bounds of the list. That would defeat the purpose of Python's terse slice syntax. Python simply skips the bounds check when we slice, which keeps our code simple.

  • We often use slices to split lists into two parts. For example, imagine that we're writing a "worker process" that handles tasks stored in a list. A worker can handle up to 3 tasks at a time, so we want to remove up to 3 from our list at once. We need the list of those tasks, so we can assign them to the worker. But we also need to remove those tasks from the list of remaining tasks, since they're now assigned.

  • >
    remaining_task_ids = [10, 20, 30, 40, 50, 60]

    # Take a bundle of tasks to be assigned to this worker.
    task_capacity = 3
    tasks_to_assign = remaining_task_ids[:task_capacity]

    # Remove these tasks from the remaining_task_id list.
    remaining_task_ids = remaining_task_ids[task_capacity:]
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    tasks_to_assign
    Result:
    [10, 20, 30]Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    remaining_task_ids
    Result:
    [40, 50, 60]Pass Icon
  • That example uses a common pattern for slicing a list into two parts at a certain index:

    • some_list[:index] gives us everything before some_list[index].
    • some_list[index:] gives us everything starting at some_list[index].
  • Together, these two expressions let us quickly partition a list without worrying about edge cases.

  • >
    numbers = [0, 1, 2, 3, 4]
    chunks = []
    chunk_size = 2

    while len(numbers) > 0:
    chunks.append(numbers[:chunk_size])
    numbers = numbers[chunk_size:]

    chunks
    Result:
    [[0, 1], [2, 3], [4]]Pass Icon
  • We've seen that the slice syntax can contain a start index ([2:]), an end index ([:3]), or both ([2:3]). It can also contain neither index! The slice some_list[:] gives us every element in the list.

  • >
    original_list = [1, 2, 3]
    new_list = original_list[:]
    new_list
    Result:
    [1, 2, 3]Pass Icon
  • Although the lists contain the same elements, they're independent lists, stored in different places in memory. Changes to one list won't affect the other.

  • >
    original_list = [1, 2]
    new_list = original_list[:]

    new_list.append(3)
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    original_list
    Result:
    [1, 2]Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new_list
    Result:
    [1, 2, 3]Pass Icon
  • This is also true if we only slice a part of the list. Slicing always creates a fresh list.

  • >
    original_list = [1, 2, 3]
    new_list = original_list[:2]
    new_list.append(4)
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    original_list
    Result:
    [1, 2, 3]Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    new_list
    Result:
    [1, 2, 4]Pass Icon
  • What if we slice a list that contains mutable elements, like lists or dictionaries? The elements themselves aren't copied. The original list and the new sliced list both reference the same element values, stored at the same places in memory.

  • In the example below, we copy a list that contains dictionaries. When we modify one of the dictionaries in one copy of the list, our changes are also visible in the other copy.

  • >
    original_list = [{"name": "Amir"}, {"name": "Betty"}]
    copy = original_list[:]

    original_list[0]["age"] = 36
    copy[0]
    Result:
    {'name': 'Amir', 'age': 36}Pass Icon
  • This is a subtle but important point: slicing a list gives us a new list, independent from the original list. But it doesn't copy the individual list elements! In other words, slicing can make a "shallow copy" of the list, but it can't make a "deep copy" of the list and all of its elements.

  • Slices also work when assigning. In the simplest case, some_list[:] = another_list replaces the entire contents of some_list with another_list.

  • >
    some_list = [1, 2, 3]
    some_list[:] = ["a", "b", "c"]
    some_list
    Result:
    ['a', 'b', 'c']Pass Icon
  • When we assign to a slice, we replace part of a list with another list. some_list[1:3] = another_list replaces all values from some_list[1:3] with the values of another_list That is, it replaces indexes 1 and 2.

  • >
    some_list = [1, 2, 3, 4, 5]
    some_list[1:3] = [6, 7]
    some_list
    Result:
    [1, 6, 7, 4, 5]Pass Icon
  • When replacing in this way, the replacement doesn't need to be the same size as the section that it's replacing. For example, we can replace a 2-element section of a list with 5 new elements. The resulting list will be 3 elements longer than the original.

  • >
    some_list = ["a", "b", "c", "d", "e"]
    some_list[1:3] = ["x", "y", "z"]
    some_list
    Result:
  • >
    some_list = ["a", "b", "c", "d", "e"]
    some_list[2:3] = ["x", "y"]
    some_list
    Result:
    ['a', 'b', 'x', 'y', 'd', 'e']Pass Icon
  • That even works with an empty slice. For example, some_list[2:2] = another_list inserts another_list into some_list starting at index 2. The slice 2:2 means "every index that is at least 2, but is also less than 2."

  • There are no indexes that are at least 2, but also less than 2, so the slice matches no indexes. When we assign to that slice, we're saying "insert this other list's elements, starting at index 2." It only inserts elements; none are removed.

  • >
    some_list = [1, 2, 3, 4, 5]
    some_list[2:2] = [600, 700]
    some_list
    Result:
  • >
    some_list = [1, 2, 3, 4, 5]
    some_list[4:4] = [800]
    some_list
    Result:
    [1, 2, 3, 4, 800, 5]Pass Icon
  • Finally, here's a helpful way to think about slice assignment. We can think of some_list[start:end] = another_list as happening in two steps.

    1. We remove all the elements found within some_list[start:end].
    2. We insert all the elements from another_list into some_list, starting at some_list[start].
  • Here's a code problem:

    Write a function, remove_outliers, that takes two arguments: a sorted list (the_list) and a number (n). It should return the list with the first n and last n elements removed. For example, remove_outliers([1, 2, 3, 4, 5, 6], 2) should return [3, 4]. Use Python's slice syntax to remove the elements.

    (The solution already contains some code to handle the edge case where n == 0.)

    def remove_outliers(the_list, n):
    if n == 0:
    return the_list
    else:
    return the_list[n:-n]
    assert remove_outliers([1, 2, 3, 4, 5, 6], 2) == [3, 4]
    assert remove_outliers([2, 5, 7, 8, 10, 12], 2) == [7, 8]
    assert remove_outliers([1, 4, 7, 11, 15], 1) == [4, 7, 11]
    assert remove_outliers([1, 4, 5], 0) == [1, 4, 5]
    Goal:
    None
    Yours:
    NonePass Icon