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 is the container you will use most in Python. When you are not sure which data structure you need, start with a list. You can change it later.

You met indexing and slicing in Strings. Lists use the same syntax, with one big difference: you can change a list in place.


1. What a List Is

python
nums = [1, 2, 3, 4]                 # all the same type (the usual case)
mixed = [1, "two", 3.0, True, None] # mixed types, also allowed
empty = []                          # zero items

Three things to remember:

  • Ordered: items keep the order you put them in. nums[0] is always 1.
  • Mutable: you can change, add and remove items after you create the list.
  • Heterogeneous: items don't have to be the same type. Don't do this often, because a list of mixed types is harder to work with.

2. Creating Lists

python
a = [1, 2, 3]                       # written out directly (most common)
b = list("hello")                   # ['h', 'e', 'l', 'l', 'o']  (one item per character)
c = list(range(5))                  # [0, 1, 2, 3, 4]
d = [0] * 5                         # [0, 0, 0, 0, 0]  (one value repeated)

[x] * n is a quick way to make a list of a fixed size filled with an immutable value, such as 0. With a mutable value, such as a list, it causes a bug. 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']  (from 1 up to but not including 4)
print(letters[::2])         # ['a', 'c', 'e']  (every other item)
print(letters[::-1])        # ['e', 'd', 'c', 'b', 'a']  (a reversed copy)

The difference from strings: you can assign a new value to a position in a list.

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

letters[1:3] = ['Y', 'Z', 'Q']      # assigning to a slice can change the length
print(letters)              # ['X', 'Y', 'Z', 'Q', 'd', 'e']
+ setup added so this can run · re-runs 1 earlier block on this page first
letters = ["alpha", "beta", "gamma"]

# Earlier blocks on this page that this one builds on, re-run quietly first.
_pm_earlier = [
    "letters = ['a', 'b', 'c', 'd', 'e']\n\nprint(letters[0])           # 'a'    (first)\nprint(letters[-1])          # 'e'    (last)\nprint(letters[1:4])         # ['b', 'c', 'd']  (from 1 up to but not including 4)\nprint(letters[::2])         # ['a', 'c', 'e']  (every other item)\nprint(letters[::-1])        # ['e', 'd', 'c', 'b', 'a']  (a reversed copy)",
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

On a string, both of those lines would raise a TypeError. On a list, they work.


4. Mutating Methods

These methods change the list in place and return None (except pop(), which returns the item it removed). They don't return a new list.

python
nums = [1, 2, 3]

nums.append(4)              # [1, 2, 3, 4]            (adds one item to the end)
nums.extend([5, 6])         # [1, 2, 3, 4, 5, 6]      (adds every item from another list)
nums.insert(0, 0)           # [0, 1, 2, 3, 4, 5, 6]   (inserts 0 at index 0)
nums.remove(3)              # [0, 1, 2, 4, 5, 6]      (removes the first 3 it finds)
last = nums.pop()           # last == 6, nums == [0, 1, 2, 4, 5]   (removes and returns the last item)
first = nums.pop(0)         # first == 0, nums == [1, 2, 4, 5]     (removes and returns the item at index 0)
nums.clear()                # []                       (empties the list)

Beginners often confuse append(x) and extend(iter). See Common Mistakes.


5. Sorting

There are two ways to sort. Choose the one you need.

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

nums.sort()                 # sorts in place, so nums is now [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True)     # sorts in place, largest first

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

The key= argument is the most useful part. It takes a function, and Python calls that function on each item to get the value to sort by.

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

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

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

key= calls your function once for each item, not once for each comparison, so it is fast.


6. Reverse: Two Ways

python
nums = [1, 2, 3]

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

original = [1, 2, 3]
for x in reversed(original):
    print(x)                # 3, 2, 1  (but the original list is unchanged)

reversed() returns an iterator. That is fine in a for loop, 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  (the first index where "cherry" appears)

.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

Beginners often spend hours on this bug.

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() makes a shallow copy: if the list contains other lists, those inner lists are still shared. For deeply nested structures, use copy.deepcopy() from the standard library.


10. The Immutable Version: 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. Tuples are also hashable, so they can be dictionary keys (as long as the items inside them are immutable too). Lists can't be dictionary keys. The Tuples lesson covers them in full.


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. It is one line, it is faster, and it is easy to read once you are used to it:

python
squares = [n * n for n in nums]
evens   = [n for n in nums if n % 2 == 0]
+ setup added so this can run · re-runs 6 earlier blocks on this page first
nums = [3, -1, 4, -1, 5]

# Earlier blocks on this page that this one builds on, re-run quietly first.
_pm_earlier = [
    'nums = [1, 2, 3, 4]                 # all the same type (the usual case)\nmixed = [1, "two", 3.0, True, None] # mixed types, also allowed\nempty = []                          # zero items',
    'nums = [1, 2, 3]\n\nnums.append(4)              # [1, 2, 3, 4]            (adds one item to the end)\nnums.extend([5, 6])         # [1, 2, 3, 4, 5, 6]      (adds every item from another list)\nnums.insert(0, 0)           # [0, 1, 2, 3, 4, 5, 6]   (inserts 0 at index 0)\nnums.remove(3)              # [0, 1, 2, 4, 5, 6]      (removes the first 3 it finds)\nlast = nums.pop()           # last == 6, nums == [0, 1, 2, 4, 5]   (removes and returns the last item)\nfirst = nums.pop(0)         # first == 0, nums == [1, 2, 4, 5]     (removes and returns the item at index 0)\nnums.clear()                # []                       (empties the list)',
    'nums = [3, 1, 4, 1, 5, 9, 2, 6]\n\nnums.sort()                 # sorts in place, so nums is now [1, 1, 2, 3, 4, 5, 6, 9]\nnums.sort(reverse=True)     # sorts in place, largest first\n\nsnapshot = [3, 1, 4, 1, 5]\nnew = sorted(snapshot)      # returns a new list, and snapshot is unchanged\nprint(snapshot, new)        # [3, 1, 4, 1, 5] [1, 1, 3, 4, 5]',
    'nums = [1, 2, 3]\n\nnums.reverse()              # reverses in place, so nums is now [3, 2, 1]\n\noriginal = [1, 2, 3]\nfor x in reversed(original):\n    print(x)                # 3, 2, 1  (but the original list is unchanged)',
    'nums = [4, 1, 7, 3, 9]\n\nprint(len(nums))            # 5\nprint(min(nums))            # 1\nprint(max(nums))            # 9\nprint(sum(nums))            # 24',
    'nums = [1, 2, 3, 4, 5]\nsquares = []\nfor n in nums:\n    squares.append(n * n)\nprint(squares)              # [1, 4, 9, 16, 25]',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

Read it left-to-right: give me n*n, for each n in nums. A later lesson covers comprehensions in detail.


Common Mistakes

1. [x] * n with a mutable item: every slot shares 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)]   # the comprehension makes a new list on each loop

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 and gives the 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, taken out of the list)

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, because sort() changed nums 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 a complicated solution. 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, because 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([]))                       # []

Only two lines do the work. The comprehension removes the zeros, and the concatenation adds them back at the end. The function returns a new list instead of changing the input. This is usually the safer choice, because the caller still has the original list.

You can also write an in-place version (move the non-zero items forward, then fill the end with zeros). It is a common interview answer when memory use 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. It is Python's default container.
  • Build with [], list(iterable), or [x] * n (careful with mutables).
  • Indexing and slicing work as they do on strings, but you can also assign to an index or to a slice.
  • Mutating methods (append, extend, insert, remove, clear) return None, but pop returns the item it removed. They all change the list in place.
  • .sort() mutates, but 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 version of lists. Use them when the data shouldn't change.
  • List comprehensions are the idiomatic way to transform a list into another list.

Next: Conditionals for making decisions, then Loops for working through your lists.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code did, as well as whether a test passed.

Found this useful? Share it with a friend who’s learning Python.