PythonMastery
beginner 16 min read · lesson 9 of 19 in Python Fundamentals

Lists & Sequences

1 · The lesson

read

A list is an ordered, mutable collection of anything. It's the workhorse container in Python — when you don't know which data structure to reach for, you reach for a list and refactor later.

You met indexing and slicing in Strings. Lists use the same syntax but with one enormous difference: you can change them in place.


1. What a List Is

python
nums = [1, 2, 3, 4]                 # all the same type — common
mixed = [1, "two", 3.0, True, None] # heterogeneous — also legal
empty = []                          # zero items

Three things to internalise:

  • Ordered — items keep the order you put them in. nums[0] is always 1.
  • Mutable — you can change, add, and remove items after creation.
  • Heterogeneous — items don't have to share a type. Use this sparingly; mixed lists are harder to reason about.

2. Creating Lists

python
a = [1, 2, 3]                       # literal — most common
b = list("hello")                   # ['h', 'e', 'l', 'l', 'o']  — from any iterable
c = list(range(5))                  # [0, 1, 2, 3, 4]
d = [0] * 5                         # [0, 0, 0, 0, 0]  — repeat a value

The [x] * n trick is great for fixed-size buffers of immutable values — but it has a sharp edge with mutable values. See Common Mistakes below.


3. Indexing & Slicing

Indexing and slicing work exactly as they do for strings.

python
letters = ['a', 'b', 'c', 'd', 'e']

print(letters[0])           # 'a'    — first
print(letters[-1])          # 'e'    — last
print(letters[1:4])         # ['b', 'c', 'd']  — slice [start:stop)
print(letters[::2])         # ['a', 'c', 'e']  — every other
print(letters[::-1])        # ['e', 'd', 'c', 'b', 'a']  — reversed copy

The difference from strings: slots are writable.

python
letters[0] = 'X'
print(letters)              # ['X', 'b', 'c', 'd', 'e']

letters[1:3] = ['Y', 'Z', 'Q']      # slice assignment can resize
print(letters)              # ['X', 'Y', 'Z', 'Q', 'd', 'e']
+ setup added so this can run · defines letters
letters = ["alpha", "beta", "gamma"]

Strings would throw TypeError on either of those. Lists welcome it.


4. Mutating Methods

These methods change the list in place and return None — they don't give you back a new list.

python
nums = [1, 2, 3]

nums.append(4)              # [1, 2, 3, 4]            — add one item to the end
nums.extend([5, 6])         # [1, 2, 3, 4, 5, 6]      — add every item from an iterable
nums.insert(0, 0)           # [0, 1, 2, 3, 4, 5, 6]   — insert at index i
nums.remove(3)              # [0, 1, 2, 4, 5, 6]      — remove first occurrence of value 3
last = nums.pop()           # last == 6, nums == [0, 1, 2, 4, 5]   — remove + return last
first = nums.pop(0)         # first == 0, nums == [1, 2, 4, 5]     — remove + return at index
nums.clear()                # []                       — empty the list

append(x) vs extend(iter) trips up beginners — see Common Mistakes.


5. Sorting

Two ways. Pick deliberately.

python
nums = [3, 1, 4, 1, 5, 9, 2, 6]

nums.sort()                 # in-place — nums is now [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True)     # in-place descending

snapshot = [3, 1, 4, 1, 5]
new = sorted(snapshot)      # returns a new list — snapshot unchanged
print(snapshot, new)        # [3, 1, 4, 1, 5] [1, 1, 3, 4, 5]

The key= argument is the real power. It takes a function that produces the sort key for each item.

python
words = ["banana", "fig", "cherry", "apple"]

print(sorted(words, key=len))       # ['fig', 'apple', 'banana', 'cherry']
print(sorted(words, key=str.lower)) # case-insensitive alphabetical

people = [("Ada", 36), ("Linus", 54), ("Grace", 85)]
print(sorted(people, key=lambda p: p[1]))   # sort by age

key= is lazy — it calls your function once per item, not once per comparison. Cheap.


6. Reverse — Two Flavours

python
nums = [1, 2, 3]

nums.reverse()              # in-place — nums is now [3, 2, 1]

original = [1, 2, 3]
for x in reversed(original):
    print(x)                # 3, 2, 1 — but original is untouched

reversed() returns an iterator — fine for looping, but wrap it in list(...) if you need a list.


7. Built-ins That Take a List

python
nums = [4, 1, 7, 3, 9]

print(len(nums))            # 5
print(min(nums))            # 1
print(max(nums))            # 9
print(sum(nums))            # 24

min and max accept a key= argument too, mirroring sorted.


8. Membership and Searching

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

print("apple" in fruits)        # True
print("kiwi" not in fruits)     # True

print(fruits.count("banana"))   # 2
print(fruits.index("cherry"))   # 2  — first index where it lives

.index(x) raises ValueError if x isn't there. If you're not sure, check with in first or use try/except.


9. Copying — The Aliasing Trap

This is where beginners lose a weekend.

python
a = [1, 2, 3]
b = a                       # NOT a copy — b is another name for the same list
b.append(4)
print(a)                    # [1, 2, 3, 4]  — a changed too

b = a binds the name b to the same list object. Two names, one list. To get an independent copy:

python
a = [1, 2, 3]
b = a.copy()                # or list(a), or a[:]
b.append(4)
print(a, b)                 # [1, 2, 3] [1, 2, 3, 4]

.copy() is a shallow copy — nested lists are still shared. For deeply nested structures, use copy.deepcopy() from the standard library.


10. The Immutable Cousin — Tuples

A tuple is a list that can't change.

python
point = (3, 4)              # parentheses, comma-separated
print(point[0])             # 3
# point[0] = 9              # TypeError: 'tuple' object does not support item assignment

Use tuples for fixed-shape records (a coordinate, an RGB colour, a database row) where mutation would be a bug. They're also hashable, which means they can be dictionary keys — lists can't. Full treatment in Tuples.


11. A Quick Taste of Comprehensions

You'll write a lot of "take a list, transform every item, get a new list" code. The verbose way:

python
nums = [1, 2, 3, 4, 5]
squares = []
for n in nums:
    squares.append(n * n)
print(squares)              # [1, 4, 9, 16, 25]

The Pythonic way is a list comprehension — one line, faster, more readable once your eyes adjust:

python
squares = [n * n for n in nums]
evens   = [n for n in nums if n % 2 == 0]
+ setup added so this can run · defines nums
nums = [3, -1, 4, -1, 5]

Read it left-to-right: give me n*n, for each n in nums. We'll dig in properly in a later lesson.


Common Mistakes

1. [x] * n of a mutable item — they all share the same object.

python
grid = [[]] * 3             # looks like three empty lists
grid[0].append("oops")
print(grid)                 # [['oops'], ['oops'], ['oops']]  — all three changed!

All three slots point to the same inner list. To get three independent lists:
python
grid = [[] for _ in range(3)]   # comprehension creates a fresh list each iteration

2. Mutating a list while iterating over it.

python
nums = [1, 2, 3, 4, 5]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)      # skips items, gives wrong result

Iterate over a copy (for n in nums[:]:) or build a new list with a comprehension.

3. .append([1, 2]) vs .extend([1, 2]).

python
a = [0]
a.append([1, 2])            # [0, [1, 2]]   — one new item, which is a list
a = [0]
a.extend([1, 2])            # [0, 1, 2]     — two new items, unpacked

append adds one thing. extend consumes an iterable.

4. Expecting .sort() to return the sorted list.

python
nums = [3, 1, 2]
result = nums.sort()        # result is None — nums was sorted in place
print(result)               # None

Use sorted(nums) when you want the return value. Use nums.sort() when you want to mutate.


🎯 Your Turn — Move Zeros to the End

Write a function move_zeros(lst) that returns a new list with every 0 moved to the end, preserving the original order of the non-zero elements.

python
[1, 0, 3, 0, 5, 0, 7]   →   [1, 3, 5, 7, 0, 0, 0]
[0, 0, 1]               →   [1, 0, 0]
[1, 2, 3]               →   [1, 2, 3]
[]                      →   []

Skeleton:

python
def move_zeros(lst):
    # TODO 1: collect non-zero items in their original order
    # TODO 2: count how many zeros there were
    # TODO 3: return non-zeros followed by that many zeros
    ...

print(move_zeros([1, 0, 3, 0, 5, 0, 7]))    # [1, 3, 5, 7, 0, 0, 0]
Hint 1 — Two passes is fine You don't need anything clever. One pass to collect non-zero items, then append the right number of zeros. A list comprehension with a filter handles the first pass in one line: [x for x in lst if x != 0].
Hint 2 — Count what's missing The number of zeros is len(lst) - len(non_zeros), or just lst.count(0). Then non_zeros + [0] * zeros gives you the answer — list concatenation builds a new list.
Show full solution
python
def move_zeros(lst):
    non_zeros = [x for x in lst if x != 0]
    zeros = len(lst) - len(non_zeros)
    return non_zeros + [0] * zeros

# Sanity checks
print(move_zeros([1, 0, 3, 0, 5, 0, 7]))    # [1, 3, 5, 7, 0, 0, 0]
print(move_zeros([0, 0, 1]))                # [1, 0, 0]
print(move_zeros([1, 2, 3]))                # [1, 2, 3]
print(move_zeros([]))                       # []

Two lines of real work. The comprehension filters; concatenation rebuilds. We return a fresh list rather than mutating the input — generally the safer default, since the caller still has their original.

A purely in-place version is possible (swap non-zeros forward, fill the tail with zeros) and is the classic interview answer when memory matters. For everyday code, the version above reads better and runs in linear time too.


What You Learned

  • A list is an ordered, mutable, heterogeneous sequence — Python's default container.
  • Build with [], list(iterable), or [x] * n (careful with mutables).
  • Indexing and slicing match strings, but list slots are writable — including via slice assignment.
  • Mutating methods (append, extend, insert, remove, pop, clear) return None. They change the list in place.
  • .sort() mutates; sorted() returns a new list. Both accept key= and reverse=.
  • b = a is aliasing, not copying. Use .copy(), list(a), or a[:] for an independent list.
  • Tuples are the immutable, hashable cousin — reach for them when data shouldn't change.
  • List comprehensions are the idiomatic way to transform a list into another list.

Next: Conditionals — making decisions, then Loops to put your lists to work.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.