FreeCampus Python

Lists: Keeping Values in Order

Build and maintain ordered Python lists while distinguishing positions, slices, mutation, method return values, aliases, and separate outer copies.
python-foundations collections-iteration lists sequences
Open in Colab
  • Level: Python Foundations · Unit 3
  • Estimated time: 3.5–5 hours
  • You will learn: Create, inspect, change, search, combine, and copy lists while explaining order, duplicates, mutation, and method returns.
  • Practice in: Google Colab, JupyterLab, or a local editor

1. Pack an adventure in a deliberate order

An explorer’s backpack is more than a group of names. The first item is easiest to reach, the last was packed most recently, and two water bottles are genuinely two supplies. A list represents those facts:

backpack = ["map", "water", "rope", "water"]

print(backpack)
print(type(backpack).__name__)
print(len(backpack))

Python creates one list containing four string references. Lists preserve insertion order, allow duplicates, and are mutable: their contents can change without binding the name to a different list.

An empty list can be written either way:

found_items = []
also_empty = list()

assert found_items == also_empty

A list may legally mix types:

raw_observation = ["north gate", 3, True, None]

That flexibility is useful for an intentionally positional record, but most lists are easier to use when every item has the same role: a list of stop names, scores, or player records. Use a tuple or dictionary when each position has a different fixed meaning.

Questions this lesson will answer

  • How can you read one item without confusing its position with its human count?
  • Which operations change the same list, and which create a new one?
  • Why does append() sometimes produce an unexpected list inside a list?
  • What do removal methods return, and how do missing items fail?
  • When is a copy separate, and where does a shallow copy stop?

2. Positions begin at zero

Python numbers sequence positions from zero:

backpack = ["map", "water", "rope", "torch"]

print(backpack[0])
print(backpack[1])
print(backpack[3])

The output is map, water, and torch. Human item number one is Python index zero. An index is an offset from the beginning, not an ordinal label.

Negative indexes count back from the end:

print(backpack[-1])
print(backpack[-2])

-1 selects the last item and -2 the item before it. This remains useful when the list length changes.

An absent position raises IndexError

short_route = ["dock", "garden"]
print(short_route[2])

The list has valid indexes 0 and 1. Asking for index 2 raises IndexError: list index out of range. Read the traceback together with len(short_route): the largest valid positive index is one less than the length.

Membership asks a different question:

route = ["dock", "garden", "vault"]

print("garden" in route)
print("tower" not in route)

Indexing asks “what is at this position?” Membership asks “does an equal value occur anywhere?” A list normally answers membership by scanning values until it finds a match or reaches the end.

3. Slices select a new list

A slice uses start:stop; the start is included and the stop is excluded:

route = ["dock", "garden", "bridge", "tower", "vault"]

middle = route[1:4]
first_two = route[:2]
from_bridge = route[2:]

print(middle)
print(first_two)
print(from_bridge)

The results are new outer lists. middle contains positions 1, 2, and 3, but not position 4. Omitting a bound means “from the beginning” or “through the end.”

A third slice part is the step:

every_other = route[::2]
reversed_copy = route[::-1]

print(every_other)
print(reversed_copy)
print(route)

The original route remains unchanged. A negative step visits positions in the opposite direction.

Unlike one-item indexing, an oversized slice is safe:

print(route[:100])
print(route[100:200])

The first expression returns every available item; the second returns an empty list. Slices describe a range of available positions rather than demanding one exact position.

Checkpoint: positions and slices

4. Replace one item or an entire slice

Assigning through an index mutates the existing list:

backpack = ["map", "water", "rope"]
backpack[1] = "canteen"

print(backpack)

Only position 1 changes. The list still has three items.

Slice assignment can replace, grow, or shrink a region:

supplies = ["map", "water", "rope", "torch"]

supplies[1:3] = ["canteen", "cord"]
print(supplies)

supplies[1:2] = ["snack", "compass", "blanket"]
print(supplies)

supplies[1:4] = ["provisions"]
print(supplies)

The values on the right are spliced into the selected region. Because a list can change length, the replacement does not need the same number of items.

One surprising use inserts without removing anything:

route = ["dock", "vault"]
route[1:1] = ["garden", "tower"]
print(route)

The empty slice at position 1 becomes the insertion point.

5. Add one object or add its contents

The shape of the right-hand value determines which operation expresses the job.

append() adds exactly one object

backpack = ["map", "rope"]
backpack.append("torch")

print(backpack)

The string becomes one new final item. If the argument is itself a list, that list remains one object:

backpack = ["map", "rope"]
emergency_kit = ["bandage", "whistle"]

backpack.append(emergency_kit)
print(backpack)
print(len(backpack))

The length is three; position 2 is the entire nested kit.

extend() adds values supplied by another iterable

backpack = ["map", "rope"]
emergency_kit = ["bandage", "whistle"]

backpack.extend(emergency_kit)
print(backpack)

Now the list is flat and has four strings. Be careful when extending with a string: strings supply characters one at a time.

letters = ["N"]
letters.extend("OVA")
print(letters)

This produces ['N', 'O', 'V', 'A']. Use append("OVA") if the complete word should be one item.

insert() chooses a position

route = ["dock", "tower", "vault"]
route.insert(1, "garden")
print(route)

The former position 1 and everything after it move right. Inserting repeatedly near the beginning requires shifting existing positions; frequent front insertion may suggest a different design later.

+ creates a new list

day_route = ["dock", "garden"]
night_route = ["tower", "vault"]
complete_route = day_route + night_route

print(day_route)
print(night_route)
print(complete_route)

Neither input changes. Concatenation is useful when the program needs to preserve both sources and produce a third sequence.

6. Remove by value or by position

Python offers several removal operations because they answer different questions.

items = ["map", "water", "rope", "water"]

items.remove("water")
print(items)

removed = items.pop()
print(removed)
print(items)

remove(value) deletes the first equal value and returns None. pop() removes and returns the final item by default.

Choose a position for pop(index):

route = ["dock", "garden", "tower", "vault"]
skipped_stop = route.pop(1)

print(skipped_stop)
print(route)

Use del when no removed value is needed:

route = ["dock", "garden", "tower", "vault"]
del route[1]
del route[1:2]
print(route)

clear() removes every item but keeps the same list object:

temporary_clues = ["rune", "key", "star"]
result = temporary_clues.clear()

print(temporary_clues)
print(result)

The result is None. Methods that mutate a list generally communicate their effect through the changed list, not by returning that list.

Missing removals expose different boundaries

items = ["map", "rope"]
items.remove("torch")

This raises ValueError because the requested equal value is absent.

items = ["map", "rope"]
items.pop(5)

This raises IndexError because the position is absent. Decide whether the task identifies an item by equality or by position before choosing a method.

7. Search and count duplicates deliberately

index() returns the first matching position:

items = ["map", "water", "rope", "water"]

print(items.index("water"))
print(items.count("water"))
print(items.count("torch"))

The outputs are 1, 2, and 0. count() has a natural zero result for an absent value; index() raises ValueError when no match exists.

You can constrain the index() search with start and stop positions:

items = ["map", "water", "rope", "water"]
second_water = items.index("water", 2)
print(second_water)

This returns index 3 because the search begins at position 2. It still finds only the first match within the chosen range.

NoteOperation cost as a practical choice

Reading items[5] uses a known position directly. Checking target in items, calling index(), count(), or remove() may inspect many items. That is often fine for a short sequence. If a program repeatedly looks up thousands of values by stable IDs, Lesson 3’s dictionary may fit the question better.

Checkpoint: mutation and return values

8. An alias is not a backup

Assignment does not copy a list:

backpack = ["map", "rope"]
backup = backpack

backup.append("torch")

print(backpack)
print(backup)

Both names show the added torch because both names reach the same list object.

The diagram distinguishes a second name for the same list from a separate outer list copy.

flowchart LR
    A["backpack name"] --> B["one list object"]
    C["backup name"] --> B
    D["snapshot name"] --> E["separate outer list"]

Create a separate outer list with .copy(), list(...), or a full slice:

backpack = ["map", "rope"]

copy_method = backpack.copy()
copy_constructor = list(backpack)
copy_slice = backpack[:]

copy_method.append("torch")

assert backpack == ["map", "rope"]
assert copy_method == ["map", "rope", "torch"]
assert copy_constructor == backpack
assert copy_slice == backpack

Equality tells you the values currently compare the same; changing one outer list proves whether its top-level structure is separate.

An outer copy does not duplicate nested values

packs = [["map"], ["rope"]]
outer_copy = packs.copy()

outer_copy[0].append("torch")

print(packs)
print(outer_copy)

Both displays contain the torch inside the first nested list. The outer lists are separate, but their positions still refer to the same nested list objects. This is a shallow copy. Unit 6 develops the complete identity and nested-copy model; for now, state whether your task needs only a separate outer sequence or fully independent nested state.

9. Visit items without changing the list

A straightforward for loop receives list values in order:

route = ["dock", "garden", "vault"]

for stop in route:
    print(f"Scanning {stop}")

assert route == ["dock", "garden", "vault"]

The target name stop is rebound for each iteration. Assigning a new value to that name does not replace the list position:

items = ["map", "rope"]

for item in items:
    item = item.upper()

print(items)

items is unchanged. You would need to assign through a position or build a new list to preserve transformed strings. Lesson 5 examines positions with enumerate() and the risks of mutating while traversing. Unit 4 develops filtering and accumulation patterns.

10. Build the explorer backpack

Complete this lab without opening the support first. The source manifest is evidence and must remain unchanged.

source_manifest = ["map", "water", "rope", "water", "torch"]

working_pack = None
first_item = None
last_two = None
water_count = None
removed_item = None
emergency_kit = ["bandage", "whistle"]
final_pack = None

Your program must:

  1. create a separate outer working list;
  2. replace the second item with "canteen";
  3. insert "compass" immediately after "map";
  4. remove and preserve the rope by its position;
  5. add both emergency-kit values as separate items;
  6. add "sealed rations" as one final item;
  7. derive the first item, final two items, and original water count; and
  8. preserve source_manifest and emergency_kit exactly.

Run these checks:

assert source_manifest == ["map", "water", "rope", "water", "torch"]
assert emergency_kit == ["bandage", "whistle"]
assert working_pack is not source_manifest
assert removed_item == "rope"
assert first_item == "map"
assert water_count == 2
assert last_two == ["whistle", "sealed rations"]
assert final_pack == [
    "map",
    "compass",
    "canteen",
    "water",
    "torch",
    "bandage",
    "whistle",
    "sealed rations",
]
assert working_pack == final_pack

Then modify the source so it begins with two maps and ends with no torch. Update only the assertions whose promised facts genuinely change.

Hint: choose an operation from the shape of each requirement

Use .copy() before mutation. Assignment through index replaces one item; .insert() chooses a position; .pop(index) preserves the removed item; .extend() adds the kit’s contents; and .append() adds the final string as one item. Calculate facts from the appropriate source before changing it.

Show one complete solution after attempting the lab
source_manifest = ["map", "water", "rope", "water", "torch"]
emergency_kit = ["bandage", "whistle"]

first_item = source_manifest[0]
water_count = source_manifest.count("water")

working_pack = source_manifest.copy()
working_pack[1] = "canteen"
working_pack.insert(1, "compass")
removed_item = working_pack.pop(3)
working_pack.extend(emergency_kit)
working_pack.append("sealed rations")

last_two = working_pack[-2:]
final_pack = working_pack.copy()

The solution takes facts about the untouched manifest before mutation. The final copy is not required for the mechanics, but it makes the delivered artifact explicit and keeps later edits to working_pack from changing its outer list.

Checkpoint: aliases and copy shape

11. Explain the choices, not only the result

Before leaving the lesson, answer in your own words:

  1. Why does indexing beyond the end fail while an oversized slice succeeds?
  2. When should another list be passed to append() rather than extend()?
  3. Which removal operation should you use when the removed value is needed?
  4. Why does assigning the result of append() destroy the useful list name?
  5. What independence does a shallow list copy guarantee, and what does it not guarantee?

Key points

TipKey points
  • Lists preserve order, allow duplicates, and can change length and contents.
  • Indexing selects one exact position; slicing returns a new outer list over an available range.
  • append() adds one object, extend() adds supplied items, insert() chooses a position, and + produces a new list.
  • remove() deletes the first equal value; pop() removes and returns by position; del removes without returning the item.
  • In-place methods such as append(), extend(), remove(), and clear() return None.
  • Assignment can create an alias. .copy(), list(...), and [:] create a separate outer list but do not recursively copy nested objects.

References

Back to top