Skip to content
  • Home
  • How To
  • M0
  • Module 1 - Python
  • Agents
  • M2
  • M3
  • M4
  • M5
  • M6
  • Progress
  • Contact
  • Pricing
  • Sign in
  • Future
  1. Module 1M1
  2. ›
    S4Collections
  3. ›
    Lesson 1 of 4Lists - Ordered, Mutable Collections
  4. ›
    S4›Lesson 1 of 4
  1. Module 1
  2. ›
  3. Section 4
  4. ›
  5. Lesson 1

Lists - Ordered, Mutable Collections

FREE·~1.5 h·2 labs·4 questions·1 assignment


Context

A list holds several values in a fixed order under one name. Items are reached by their position, whole stretches can be pulled out at once, and the contents can be changed after the list exists.

Order and changeability are exactly what tabular work needs. The rows read from a file arrive as a list, a column pulled out of those rows is a list, and the predictions a model returns line up with the true labels position by position, which only means anything because both keep their order. Building a filtered subset is a list that starts empty and grows as the loop finds rows worth keeping.

Positions are counted from zero, and that single fact causes most of the damage here. Asking for position one returns the second item, so a script that means to inspect the first record silently reports on the second, and a range taken to the wrong endpoint drops the final row of a dataset without complaint. Changeability adds its own trap, because two names can refer to one list and a change made through either is visible through both. The previous section built functions, this lesson gives them something substantial to be handed.


Goal

  • Build a list literal and a list from an existing iterable, and predict its contents before running.
  • Retrieve any single item by positive or negative index, and explain what an out-of-range index does.
  • Extract sub-sequences with start:stop:step slicing, including reversed and stepped slices.
  • Mutate a list in place with append, extend, insert, pop, remove, and sort, and distinguish in-place methods from ones that return a new value.
  • Measure the size of any collection with len and use it to guard index access.

Prerequisites

Neither is required and the lesson does not wait for them. Open one first if you would rather see lists moving before you read about them.

  • YouTube - "Python Tutorial for Beginners 4: Lists, Tuples, and Sets" by Corey Schafer. A visual walk through creating, indexing and slicing lists. Watch the first segment, up to where list methods begin at about 5:16.
  • Official docs - The Python Tutorial, "Lists". Read only that subsection, it shows indexing and slicing on a list literal.

Table of Contents

  • List
  • Indexing
  • Slicing
  • List Mutation (methods)
  • Length (len)

Theory

A single column of a dataset is, at bottom, a list of values. Everything you will later do to a column, selecting rows, reordering, appending new records, is built on the four operations in this lesson. Learn them on plain Python lists now, and the heavier tools come almost for free later.

List

What. A list is an ordered, mutable sequence of values. Ordered means every item has a fixed position counted from zero. Mutable means you can change the contents after the list exists without creating a new list. Why. You constantly need to hold many values under one name, a batch of feature values, a sequence of predictions, the rows you have read so far. A list gives you one name for the whole group plus the ability to grow, shrink, and reorder it as a program runs. When. Reach for a list whenever you have an ordered collection that may change in size, and whenever order itself carries meaning (the third sample is not the same as the first).

A list is written with square brackets and comma-separated items. The items can be any type, and they do not all have to be the same type, although in real data work you almost always keep one list to one type.

# 'feature_values' holds five floats in a fixed order.
feature_values = [0.12, 0.87, 0.43, 0.99, 0.05]

# A list can also start empty, ready to be filled later.
predictions = []

# Items need not share a type, but mixing types is rarely useful in data work.
mixed = [1, "setosa", 3.5, True]

Because a list is an iterable, you can loop over it directly with a for loop, which you already know. The list is the prototypical iterable, the thing every for loop in data work is most often walking across.

# A 'for' loop visits each item of the list in order.
feature_values = [0.12, 0.87, 0.43]
for value in feature_values:  # 'value' takes each item, first to last
    print(value)              # prints 0.12, then 0.87, then 0.43

The visiting order is guaranteed, because a list is ordered.

You can also build a list from another iterable using the list() function, which is how you convert ranges or other sequences into a concrete list you can change.

# 'range(5)' is an iterable of 0..4, 'list()' turns it into a real list.
indices = list(range(5))  # 'indices' is now [0, 1, 2, 3, 4]

list() is needed because a range holds only its start, stop and step, as Lesson 1.2.3 showed, and list() walks the range and collects the values into a list you can change.

DON'T:

  • DON'T assume the items are stored sorted. A list keeps the order your code put the items in, not value order, until you explicitly sort it.
  • DON'T mix unrelated types in one list to "save space". A column with one consistent type is far easier to reason about and process.

Indexing

What. Indexing retrieves exactly one item by its position. Positions run from 0 for the first item upward, and from -1 for the last item downward. Why. You frequently need a specific element, not the whole collection, the first prediction, the last row read, the label at position i inside a loop. Indexing is the direct, constant-cost way to reach it. When. Use indexing whenever you want a single known position. When you want a contiguous range of positions instead, use slicing (the next subtitle).

Square brackets after the list name, with an integer inside, select one item. Counting starts at zero, so the first item is at index 0, not 1. This zero-based rule trips up every beginner once.

labels = ["setosa", "versicolor", "virginica"]

# Index 0 is the FIRST item, not the second.
first = labels[0]   # 'first' is "setosa"

# Index 1 is the second item.
second = labels[1]  # 'second' is "versicolor"

Negative indices count from the end, which saves you from computing len(...) - 1 just to reach the last item.

labels = ["setosa", "versicolor", "virginica"]

# -1 is the LAST item, -2 the second to last.
last = labels[-1]         # 'last' is "virginica"
second_last = labels[-2]  # 'second_last' is "versicolor"

Negative indexing is not a special trick, it is the normal way to address items relative to the end.

Asking for a position that does not exist raises an IndexError, which stops the program unless something catches it, the job of try in Lesson 1.6.2. There is no silent "return nothing", an out-of-range index is treated as a bug.

labels = ["setosa", "versicolor", "virginica"]

# Valid indices are 0 to 2, or -1 to -3 from the end. Index 3 does not exist.
value = labels[3]  # raises IndexError: list index out of range

You guard against this by checking len before indexing with a computed position, which the final subtitle shows.

DON'T:

  • DON'T start counting at one. my_list[1] is the SECOND item, this is the single most common indexing mistake.
  • DON'T index with a position you have not bounds-checked when it was computed at runtime. An out-of-range index is a crash, not a warning.

Slicing

What. Slicing extracts a sub-sequence using start:stop:step inside the brackets. It returns a brand new list containing the selected items, leaving the original untouched. Why. You routinely want a contiguous chunk, the first 100 rows, every other sample, the data in reverse. Slicing expresses all of these in one compact, readable expression instead of a loop. When. Use a slice whenever you want more than one item by position. Use plain indexing when you want exactly one. Remember that the result is always a new list, an instance of returning-new instead of changing-in-place.

The core rule is that stop is exclusive, the slice includes start but stops just before stop. So data[1:4] gives positions 1, 2, and 3, never 4. Both start and stop are optional and default to "beginning" and "end", and with a negative step, shown below, the two swap.

data = [10, 20, 30, 40, 50]

# Positions 1, 2, 3. Position 4 is EXCLUDED.
middle = data[1:4]  # 'middle' is [20, 30, 40]

# Omit 'start' to begin at the front.
head = data[:3]     # 'head' is [10, 20, 30]

# Omit 'stop' to run to the end.
tail = data[2:]     # 'tail' is [30, 40, 50]

The exclusivity of stop has one useful consequence worth memorizing: the length of a simple slice is exactly stop - start whenever 0 <= start <= stop <= len(data), so data[1:4] is three items without counting them.

The third number is step, the stride between selected positions. A step of 2 takes every other item. A negative step walks backward, which is the idiomatic way to reverse a sequence.

data = [10, 20, 30, 40, 50]

# Every other item, starting at position 0.
evens = data[::2]           # 'evens' is [10, 30, 50]

# A negative step reverses the whole list.
reversed_data = data[::-1]  # 'reversed_data' is [50, 40, 30, 20, 10]

That [::-1] reversal is a standard idiom, and it is worth recognizing on sight rather than deriving each time.

Crucially, reading a slice never modifies the source. It always hands back a fresh list, so nothing you do to that list itself, replacing, adding or removing items, touches the original.

data = [10, 20, 30, 40, 50]
chunk = data[0:2]  # 'chunk' is a NEW list [10, 20]
chunk[0] = 999     # changing 'chunk' does not touch 'data'
# 'data' is still [10, 20, 30, 40, 50]

This "slice returns a new list" behavior bridges directly to the next lesson on immutability, returning new rather than mutating in place is a recurring distinction across Python.

DON'T:

  • DON'T expect stop to be included. data[0:3] gives three items at positions 0, 1, 2, not four.
  • DON'T think a slice edits the original. It returns a copy, so reading a slice never changes the source.

List Mutation (methods)

What. List mutation means changing a list in place through its methods, append, extend, insert, pop, remove, sort, reverse. The list object itself is altered, not replaced. Why. Real data collection is incremental. You read a row and append it, you find a bad record and remove it, you sort by a key before reporting. Mutating methods let one list evolve as the program runs without rebuilding it each time. When. Use a mutating method whenever you want the existing list to change. Be deliberate, because the most common mutation bug is expecting a method to return the new list when it actually returns None and changes the list in place.

append adds one item to the end. extend adds every item from another iterable. The two are different, append of a list nests that list as a single item, extend flattens it in.

predictions = [0.1, 0.4]

# 'append' adds ONE item to the end.
predictions.append(0.9)         # 'predictions' is now [0.1, 0.4, 0.9]

# 'extend' adds EACH item from another iterable.
predictions.extend([0.2, 0.7])  # now [0.1, 0.4, 0.9, 0.2, 0.7]

# 'append' of a list nests it as a single element.
predictions.append([0.0, 0.0])  # last item is the list [0.0, 0.0]

That last line is the classic append versus extend confusion, one call added a single item that happens to be a list, the other added the list's items.

insert places an item at a chosen index, shifting later items right. pop removes and returns an item by index (last item if no index given). remove deletes the first occurrence of a value.

labels = ["setosa", "virginica"]

# 'insert' puts a value at a position, shifting the rest right.
labels.insert(1, "versicolor")  # ["setosa", "versicolor", "virginica"]

# 'pop' removes AND returns an item, default is the last.
last = labels.pop()             # 'last' is "virginica", list loses it

# 'remove' deletes the first matching VALUE, not a position.
labels.remove("setosa")         # ["versicolor"] remains

The one to keep straight is remove, which takes a value and deletes the first match, while pop takes an index. Handing remove a number searches for that number as a value rather than treating it as a position.

The trap: every in-place list method this lesson names except pop returns None, not the list. sort and reverse rearrange the list itself and hand back nothing.

scores = [0.9, 0.1, 0.5]

# 'sort' rearranges in place and returns None.
result = scores.sort()                 # 'scores' is now [0.1, 0.5, 0.9], 'result' is None

# So NEVER write: scores = scores.sort()  -> 'scores' becomes None

# To get a NEW sorted list and keep the original, use sorted().
unsorted = [0.9, 0.1, 0.5]
ranked = sorted(unsorted)              # 'ranked' is [0.1, 0.5, 0.9], 'unsorted' unchanged

# 'reverse=True' puts the largest first, for '.sort()' and 'sorted()' alike.
desc = sorted(unsorted, reverse=True)  # 'desc' is [0.9, 0.5, 0.1]

The commented line is the one that costs people real data. scores = scores.sort() sorts the list correctly, then throws it away and binds scores to the None that sort returned, so the list is gone and nothing raised. Use sorted() when you want a sorted copy and the original intact, and call .sort() on a line by itself when you do not.

Which end of a sorted list is the best end depends on the metric, never on the list. reverse=True puts the best accuracy first and the worst loss first, and both lists look equally right.

DON'T:

  • DON'T write my_list = my_list.sort(). sort returns None, so you would erase your list. Call my_list.sort() on its own line, or use sorted(my_list).
  • DON'T rank every metric with reverse=True. Largest first is best first only where a higher value wins, so a list of validation losses (each run's loss on data held back from training) ranked that way puts its worst run on top, and nothing raises.
  • DON'T confuse append with extend. append([1, 2]) adds one nested list, extend([1, 2]) adds two separate items.

Length (len)

What. len is a built-in function that returns the number of items in a collection. It works on lists, strings, and the other containers you will meet this section. Why. You constantly need the count, to report how many rows you read, to compute an average, to build a valid range of indices, to guard against out-of-range access. len is the single, uniform way to ask "how many?". When. Use len whenever the size matters: bounds-checking before indexing, driving an index-based loop, or sanity-checking that a dataset has the rows you expect.

len takes the collection as its argument and returns an integer. It counts top-level items only, a nested list counts as one item, regardless of what is inside it.

feature_values = [0.12, 0.87, 0.43, 0.99, 0.05]

# 'len' returns the item count as an int.
n = len(feature_values)  # 'n' is 5

# Nested lists count as ONE top-level item each.
nested = [[1, 2], [3, 4, 5]]
n_rows = len(nested)     # 'n_rows' is 2, not 5

To count the inner items you would have to len each sub-list, len never recurses.

The most important use is guarding index access so a computed position never overruns the list. The last valid index of a non-empty list is always len(...) - 1.

data = [10, 20, 30]
i = 5                            # imagine 'i' was computed at runtime

# Check both bounds BEFORE indexing to avoid IndexError.
if 0 <= i < len(data):           # is 'i' between 0 and the last index?
    print(data[i])
else:
    print("index out of range")  # safe fallback instead of a crash

Pairing len with index access this way is the standard defensive pattern for any position you did not write by hand.

DON'T:

  • DON'T call len on a single number or boolean. len(5) raises TypeError, len is for collections and strings only.
  • DON'T assume len counts nested items. It counts top-level items, so a list of lists reports the number of inner lists, not the grand total.

Lab Exercises

Lab Exercise 1: Build, Index, and Carve a Feature Vector

Create a list named feature_vector holding the six floats 0.5, 0.2, 0.9, 0.1, 0.7, 0.3. Using indexing only, print the first feature, the last feature with a negative index, and the third feature.

Then, using slicing and without changing feature_vector, build first_half holding the first three values, every_other holding every second value from the front, and backwards holding the whole vector reversed. Print all three, then print feature_vector again to show it survived untouched.

Lab Exercise 2: Mutate a Prediction Log, and Keep the Original

Start with predictions = [0.91, 0.12, 0.45, 0.78, 0.33, 0.66].

Mutate it in place and print the list after each step: append 0.5, insert 0.0 at the front, and remove the value 0.12. Then report how many predictions remain with len.

Now build ranked, a new list of the same values sorted from highest to lowest, in a way that leaves predictions in its current order. Take a separate copy of predictions, pop the last item off that copy, and print the popped value, the copy's new length, and predictions itself, to prove neither the ranking nor the pop reached the original.

Finally, given an index i that you set to 9, print predictions[i] only when that position actually exists, and print a clear message when it does not.


Solution 1: Build, Index, and Carve a Feature Vector
# Build the list literal with six floats in a fixed order.
feature_vector = [0.5, 0.2, 0.9, 0.1, 0.7, 0.3]

print(feature_vector[0])           # 0.5
print(feature_vector[-1])          # 0.3
print(feature_vector[2])           # 0.9

# Every slice below returns a NEW list, none of them touch the source.
first_half = feature_vector[:3]    # start omitted, so from the front
every_other = feature_vector[::2]  # step 2, positions 0, 2, 4
backwards = feature_vector[::-1]   # negative step walks end to start

print(first_half)                  # [0.5, 0.2, 0.9]
print(every_other)                 # [0.5, 0.9, 0.7]
print(backwards)                   # [0.3, 0.7, 0.1, 0.9, 0.2, 0.5]
print(feature_vector)              # [0.5, 0.2, 0.9, 0.1, 0.7, 0.3]

Index 2 is the third feature, not the second, because the count starts at zero. That single off-by-one is the reason the printed value is 0.9 rather than 0.2.

DON'T:

  • DON'T use feature_vector[3] for "the third feature", that is the fourth item, index 2 is the third.
  • DON'T compute the last item with feature_vector[len(feature_vector) - 1] when feature_vector[-1] says the same thing more clearly.
Solution 2: Mutate a Prediction Log, and Keep the Original
predictions = [0.91, 0.12, 0.45, 0.78, 0.33, 0.66]

# 'append' adds ONE item to the end, in place, and returns None.
predictions.append(0.5)
print(predictions)       # [0.91, 0.12, 0.45, 0.78, 0.33, 0.66, 0.5]

# 'insert' at index 0 puts the value at the FRONT, shifting the rest right.
predictions.insert(0, 0.0)
print(predictions)       # [0.0, 0.91, 0.12, 0.45, 0.78, 0.33, 0.66, 0.5]

# 'remove' deletes the first matching VALUE, not a position.
predictions.remove(0.12)
print(predictions)       # [0.0, 0.91, 0.45, 0.78, 0.33, 0.66, 0.5]
print(len(predictions))  # 7
# 'sorted' returns a NEW list. '.sort()' would have reordered the original.
ranked = sorted(predictions, reverse=True)  # reverse=True gives high to low

# A full slice is the copy idiom: a new list holding the same values.
working = predictions[:]                    # NOT 'working = predictions', that would be one list
popped = working.pop()                      # removes and returns the last item of the COPY

print(ranked)                               # [0.91, 0.78, 0.66, 0.5, 0.45, 0.33, 0.0]
print(popped, len(working))                 # 0.5 6
print(predictions)                          # [0.0, 0.91, 0.45, 0.78, 0.33, 0.66, 0.5]

i = 9                                       # imagine this was computed rather than typed
if 0 <= i < len(predictions):               # check the bound BEFORE indexing
    print(predictions[i])
else:
    print("index out of range")             # safe branch instead of an IndexError

working = predictions[:] is doing the real work in the second block. Without the [:] both names would refer to one list, and working.pop() would have removed an item from predictions as well, with no error to say so.

DON'T:

  • DON'T pass an index to remove, it takes a value, use pop(index) when you mean a position.
  • DON'T index with a runtime position before bounds-checking it, plain indexing raises IndexError out of range.

Theory Assessment

Q1: Counting From Zero

Given labels = ["a", "b", "c", "d"], which index gives the second item, and which single index gives the last item without using len? State both values. Then explain why labels[1] is the second element rather than the first, and say exactly what labels[4] does.

Hint: The first valid index and the count of items are two different numbers, and negative indices address positions relative to the end.

Q2: Where a Slice Stops, and What It Hands Back

For data = [10, 20, 30, 40, 50], what does data[1:4] return, and how many items does a slice data[a:b] hold when 0 <= a <= b <= len(data)? What does data[::-1] produce? Then explain why replacing an item in the result of a slice never changes the list it came from.

Hint: The stop bound marks a boundary the slice stops before rather than an item it includes. For the last part, ask how many list objects exist after the slice runs.

Q3: What Comes Back, and What Went In

Explain why ranked = scores.sort() leaves ranked equal to None, and what to write instead when you want a sorted list while keeping scores unchanged. Then, starting from xs = [1, 2] each time, give the contents after xs.append([3, 4]) and after xs.extend([3, 4]), and state the difference in one sentence.

Hint: Some methods change the object and hand back nothing, while some functions leave the object alone and hand back a new one. For the second half, count how many items each call adds.

Q4: Two Appends, One List

Read the following without running it, then state what each print shows.

scores = [0.9, 0.1, 0.5]
alias = scores    # a second name
copy = scores[:]  # a full slice

alias.append(0.7)
copy.append(0.3)
ranked = scores.sort()

print(scores)
print(copy)
print(ranked)

Two values were appended, and only one of them shows up in scores. Explain why, and explain what ranked holds and how it got that way.

Hint: One of the two extra names was built by an operation that constructs something new, and the other was not. Then recall what .sort() hands back to whoever assigns from it.


Answer 1:

The second item is at index 1, which is "b". The last item is at index -1, which is "d".

labels[1] is the second element because the first sits at index 0. The first valid index (0) and the count of items (4) are different numbers, which is the whole of zero-based indexing, and it means the largest valid index of a non-empty list is always len - 1 rather than len.

labels[4] raises IndexError, because four items have the indices 0 to 3, or -1 to -4 from the end, and 4 is neither. There is no silent empty result, an out-of-range index is treated as a bug and stops the program.

DON'T assume the largest valid index equals the length, it is one less.

Answer 2:

data[1:4] returns [20, 30, 40], positions 1, 2 and 3, because stop is exclusive. When 0 <= a <= b <= len(data), data[a:b] holds b - a items. data[::-1] returns [50, 40, 30, 20, 10], the whole list reversed by a step of -1.

Replacing an item in a slice cannot reach the original because the slice built a brand new list. After chunk = data[0:2] there are two separate list objects holding equal values, so chunk[0] = 999 writes into one of them and the other never learns about it. This is the "return a new thing rather than change the old one" behavior, and it is the same instinct behind the immutability of the next lesson.

DON'T expect data[1:4] to include position 4, the stop bound is a boundary the slice stops before.

Answer 3:

scores.sort() sorts the list in place and returns None, so ranked = scores.sort() binds ranked to None rather than to a list. Use the built-in sorted(scores) instead, which leaves scores alone and hands back a new sorted list.

xs.append([3, 4]) gives [1, 2, [3, 4]], three items, the last of which is itself a list. xs.extend([3, 4]) gives [1, 2, 3, 4], four items. append adds its argument as exactly one element whatever that argument is, while extend walks the argument and adds each of its items separately.

DON'T write scores = scores.sort(), it replaces your list with None and nothing raises to tell you.

Answer 4:

scores prints [0.1, 0.5, 0.7, 0.9]. copy prints [0.9, 0.1, 0.5, 0.3]. ranked prints None.

alias = scores does not build anything. It attaches a second name to the list that already exists, so alias and scores are two names for one object, and alias.append(0.7) puts 0.7 into that one list. Reading scores afterwards shows the change, because there was never a second list to be out of step with.

copy = scores[:] is different, because a slice constructs a new list. copy holds equal values in a separate object, so copy.append(0.3) lands in the copy alone and scores never sees 0.3. That is why only one of the two appended values appears in scores.

ranked is None because .sort() reorders the list in place and returns nothing. The sorting itself did happen, which is why scores prints in ascending order with 0.7 included, but the return value assigned to ranked is that None.

The trap worth naming is that alias = scores and copy = scores[:] look almost identical and behave completely differently, and neither one announces which you wrote. Two names for one list is not a bug in itself, it is only a bug when you thought you had two lists.

DON'T read alias = scores as making a copy, only an operation that constructs a new list does that, and plain assignment never constructs anything.


Assignment

Assignment: An In-Place Cleanup That Reports Its Own Damage

Given readings = [0.5, 0.2, 0.9, 0.2, 0.7, 0.9, 0.1], do all of the following on the SAME list, mutating in place where sensible, printing the list after each step.

Sort it ascending. Remove the first occurrence of the value 0.2. Pop the largest value and store it. Then insert the popped value back at the exact middle index, computing that index with integer floor division //, which you met in Lesson 1.1.4, and print the index you used. Then report the position of the re-inserted value with list.index(), and say in a comment why .index() reports that position, and when it would report one other than the index you inserted at.

Now demonstrate the two ways this goes wrong, rather than describing them.

First, build a fresh list, data = [0.5, 0.2, 0.9], and write the mistake data = data.sort() on it, then print data and its type. Do not guard against the result, print what actually happens next when you call len on it, catching the error so the script keeps running.

Second, build alias as a plain second name for a list and copy as a full slice of the same list, append a different value through each, and print all three names. Add a comment stating which append reached the original and why the two lines that created the names are not equivalent.

Hints:

  • After sorting, the largest value is at the end, so pop() with no argument retrieves it, and the middle index of a list of length n is n // 2, which floor division keeps a whole number so it is usable as an index.
  • .index(v) scans left to right and stops at the FIRST match, so if the value you re-inserted also appears earlier in the sorted list, the position it reports will not be the position you inserted at.

Solution:
readings = [0.5, 0.2, 0.9, 0.2, 0.7, 0.9, 0.1]

# Sort ascending IN PLACE, on its own line because it returns None.
readings.sort()
print(readings)                 # [0.1, 0.2, 0.2, 0.5, 0.7, 0.9, 0.9]

# 'remove' deletes the FIRST occurrence of the value 0.2.
readings.remove(0.2)
print(readings)                 # [0.1, 0.2, 0.5, 0.7, 0.9, 0.9]

# Sorted, so the largest sits at the end and pop() returns it.
largest = readings.pop()
print(readings, largest)        # [0.1, 0.2, 0.5, 0.7, 0.9] 0.9

# Floor division '//' keeps the index a whole number, '/' would give 2.5.
mid = len(readings) // 2        # 5 // 2 -> 2
readings.insert(mid, largest)
print(readings, "mid:", mid)    # [0.1, 0.2, 0.9, 0.5, 0.7, 0.9] mid: 2

# '.index(v)' returns the FIRST position holding v, scanning left to right.
print(readings.index(largest))  # 2
# It reports 2 here because the inserted 0.9 is the leftmost 0.9. Had the
# insert landed to the RIGHT of the surviving 0.9, this would report that
# other one's position instead, and it would still be a correct answer to a
# different question than the one you meant to ask.
# FAILURE 1: assigning from an in-place method.
data = [0.5, 0.2, 0.9]
data = data.sort()       # sorts correctly, then throws the list away
print(data, type(data))  # None <class 'NoneType'>

try:
    print(len(data))     # 'data' is not a list any more
except TypeError as e:   # bind the error object so it can be printed
    print("len(data) failed:", e)

# FAILURE 2: a second NAME is not a second LIST.
original = [1, 2, 3]
alias = original         # binds another name to the SAME list
copy = original[:]       # a slice, so this builds a NEW list

alias.append(99)
copy.append(77)

print(original)          # [1, 2, 3, 99]
print(alias)             # [1, 2, 3, 99]
print(copy)              # [1, 2, 3, 77]
# The append through 'alias' reached the original, because 'alias = original'
# constructed nothing. 'copy = original[:]' ran a slice, and a slice always
# builds a new list, so 77 landed somewhere 'original' cannot see.

Running the second block prints:

None <class 'NoneType'>
len(data) failed: object of type 'NoneType' has no len()
[1, 2, 3, 99]
[1, 2, 3, 99]
[1, 2, 3, 77]

The two failures fail at different distances from their cause. data = data.sort() destroys the list on the line it is written, but nothing raises there, and the program carries on holding None under a name that reads like a list. The error arrives at the next line that needs a list, a TypeError from len or an AttributeError from a method such as .append, which in a longer script could be pages away and in a different function, and its message talks about NoneType rather than about sorting.

The aliasing failure never raises at all. original and alias are two names for one list, so both print identically and both are correct, and the only evidence that alias.append(99) was not what you wanted is that original changed when you did not intend it to. Nothing in the language distinguishes a deliberate second name from an accidental one, which is why [:] is worth writing whenever you mean a copy.

DON'T assign from .sort(), .append(), .insert() or any other in-place list method this lesson names but .pop(), they return None by design, and don't reach for plain assignment when you want an independent list, only an operation that constructs one gives you that.


Additional Resources

Optional, and nothing ahead depends on either. Worth a look only if you want more repetitions on slicing and the method table.

  • YouTube - "Learn Python - Full Course for Beginners [Tutorial]" by freeCodeCamp.org. Watch the lists portion for slicing and methods in motion.
  • Official docs - The Python Tutorial, "More on Lists", with every list method, index, count and copy included.

Summary

A list is the first real container, and almost everything that follows leans on it. The shift this lesson asks you to make is from thinking about one value at a time to thinking about an ordered group under a single name, and then about the handful of moves that let you reach into that group, carve pieces out of it, and reshape it as a program runs. Indexing reaches one item, slicing carves out a run of them, the mutating methods grow and shrink and reorder the list itself, and len tells you how big it has become.

Two distinctions are worth carrying forward more than the syntax. The first is zero-based, stop-exclusive counting, which feels arbitrary until it becomes invisible, the first item is at index zero, and a slice stops just before its end bound. The second is the quiet but constant difference between changing a thing in place and building a new thing from it. A slice always hands you a new list. sort rearranges the existing one and returns nothing. Confusing those two is the source of a surprising share of beginner bugs, and noticing which one you want is a habit that will keep paying off.

That same in-place-versus-new tension is the doorway to the next lesson. You have just seen that a slice refuses to touch its source. Tuples take that idea to its conclusion: a value that cannot be changed at all, so that any "edit" must produce something new. Once you have felt how freely a list mutates, the reasons you might deliberately give that freedom up start to make sense.


Cheat Sheet

Part A - Reference Table

Operation Syntax Returns Notes
Build list [a, b, c] or list(iterable) new list ordered, mutable
Index xs[i], xs[-1] one item zero-based, -1 is last, out-of-range raises
Slice xs[start:stop:step] new list stop excluded, [::-1] reverses
Append one xs.append(v) None adds single item in place
Add many xs.extend(iter) None adds each item in place
Insert xs.insert(i, v) None shifts rest right
Remove by value xs.remove(v) None first match only
Remove by index xs.pop(i) / xs.pop() removed item default last
Sort in place xs.sort() None mutates, use reverse=True for desc
Sorted copy sorted(xs) new list original unchanged
Count items len(xs) int top-level only

Part B - Compact Code Block

xs = [10, 20, 30, 40]        # ordered, mutable
first, last = xs[0], xs[-1]  # index, negative index
mid = xs[1:3]                # slice [20, 30], stop excluded
rev = xs[::-1]               # reversed copy
xs.append(50)                # add one in place
xs.extend([60, 70])          # add many in place
xs.insert(0, 5)              # shift rest right
xs.pop()                     # remove + return last
xs.remove(20)                # delete first matching value
xs.sort()                    # in place, returns None
ranked = sorted(xs)          # new list, original kept
n = len(xs)                  # item count
if 0 <= 2 < len(xs):         # guard before indexing
    print(xs[2])
←Previous: Section 3, Lesson 4
Next: Lesson 2→

Questions about this lesson: #module-1 on Discord

Found an error in this lesson? Report it

Your progress
Current streak
🔥 0
Lessons marked today
0 / 3
Longest streak
0

Sign in

The bootcamp

  • Curriculum
  • How To
  • Pricing
  • Changelog
  • Future

The community

  • Discord
  • Reviews
  • Contact

Follow

  • Instagram
  • X
  • TikTok
  • Reddit

Legal

  • Terms
  • Privacy
  • Refunds

Coming soon on November 16, 2026

Theme