datarekha

Lists

Python's workhorse collection — append, slice, sort, and the shared-reference gotcha that bites every programmer exactly once.

9 min read Beginner Python Lesson 6 of 41

What you'll learn

  • The methods that actually matter — append, extend, insert, pop, remove
  • sort vs sorted, and why key= is the most useful argument in the language
  • Why a = b never copies a list, and the copy that fixes the aliasing bug
  • When a list is the wrong tool, and a set is right

Before you start

If Python had a mascot, it would be the list. Lists are ordered, they are mutable — changeable after they are made — and they will hold anything you put in them: numbers, strings, dictionaries, even other lists. You will build one, change one, or walk through one in very nearly every program you ever write, so it is worth knowing them deeply rather than by habit.

Creating and growing a list

A list starts empty and grows. The two ways to add catch people out, so watch the difference between adding one item and adding several:

leaderboard = []                       # empty

leaderboard.append("alice")            # add one item to the end
leaderboard.append("bob")
leaderboard.extend(["chen", "dani"])   # add EACH element of an iterable
leaderboard.insert(0, "winner")        # insert at a specific index
print(leaderboard)

leaderboard.remove("bob")              # remove the first matching value
last = leaderboard.pop()               # remove AND return the last item
print("removed:", last)
print(leaderboard)
['winner', 'alice', 'bob', 'chen', 'dani']
removed: dani
['winner', 'alice', 'chen']

The distinction to fix in memory: append adds its argument as a single item, while extend adds every element of an iterable. The classic mistake is calling append with a list — lb.append([1, 2]) gives you a list nested inside your list, which is almost never what you meant. When you want to merge in several items, reach for extend.

Indexing and slicing

Lists use exactly the [start:stop:step] slicing you met with strings — same rules, same exclusive stop, same negative indices counting from the right:

scores = [42, 58, 71, 89, 95, 100]

print(scores[0])        # first
print(scores[-1])       # last
print(scores[1:4])      # positions 1, 2, 3
print(scores[:3])       # first three
print(scores[-2:])      # last two
print(scores[::2])      # every other element
print(scores[::-1])     # a reversed copy
42
100
[58, 71, 89]
[42, 58, 71]
[95, 100]
[42, 71, 95]
[100, 95, 89, 71, 58, 42]

One quiet but important fact: slicing returns a new list and never disturbs the original. That is why scores[::-1] is a tidy one-liner for “give me a reversed copy” without touching scores itself.

Sorting — sort versus sorted

There are two ways to order a list, and the difference between them is exactly the difference between changing a thing and making a new thing:

  • list.sort() sorts the list in place and returns None.
  • sorted(iterable) returns a new sorted list and leaves its input untouched.
results = [
    ("alice", 87),
    ("bob", 42),
    ("chen", 95),
    ("dani", 71),
]

# Sort by score, highest first. The key= argument is what makes this sing.
ranked = sorted(results, key=lambda r: r[1], reverse=True)
for name, score in ranked:
    print(f"{name:<8} {score}")

print()

# Sort the original in place, this time by name.
results.sort(key=lambda r: r[0])
print(results)
chen     95
alice    87
dani     71
bob      42

[('alice', 87), ('bob', 42), ('chen', 95), ('dani', 71)]

The star of that example is key=. It is a function applied to each element to produce the value Python should compare — sort by a tuple’s second field, by the length of a string, by a date pulled out of a record, by anything you can compute. It is, with little exaggeration, the most useful single argument in the language.

The aliasing gotcha — a = b does not copy

Here is the bug that bites everyone exactly once. You want a snapshot of a list before you change it, so you write season_2 = season_1 — and then watch your “snapshot” change underneath you:

season_1 = ["alice", "bob", "chen"]

# WRONG — this does not copy. season_2 is just a second NAME for the same list.
season_2 = season_1
season_2.append("dani")

print("season_1:", season_1)
print("season_2:", season_2)
print("same object?", season_1 is season_2)
season_1: ['alice', 'bob', 'chen', 'dani']
season_2: ['alice', 'bob', 'chen', 'dani']
same object? True

Appending through season_2 changed season_1, because there was only ever one list with two names pointing at it. To get a genuinely separate list, you must ask for a copy — and the picture below shows precisely what changes when you do:

season_2 = season_1season_1season_2[alice, bob,chen, dani]one list, two names —append leaked into bothseason_2 = season_1.copy()season_1season_2[alice, bob, chen][alice, bob, chen,dani]
Plain assignment makes a second name for one list; .copy() makes a second list. Mutation only stays separate on the right.

So to take a real snapshot, copy explicitly — and pick the depth you need:

season_2 = season_1.copy()           # shallow copy
season_2 = list(season_1)            # also a shallow copy
season_2 = season_1[:]               # slice copy — works, older style
import copy
season_2 = copy.deepcopy(season_1)   # recursive — for nested structures

A first taste of comprehensions

The most Pythonic way to build a new list from an existing one is the list comprehension. A whole lesson is coming on these, but you will see them so often that a first taste belongs here:

scores = [42, 58, 71, 89, 95, 100]

# Boost everyone by 5, capped at 100.
boosted = [min(s + 5, 100) for s in scores]
print(boosted)

# Keep only the passing scores while you are at it.
passing = [s for s in scores if s >= 60]
print(passing)

# Clean a messy column of names in one pass.
raw = ["  Alice ", "BOB", "Chen\n", "  dani"]
clean = [name.strip().lower() for name in raw]
print(clean)
[47, 63, 76, 94, 100, 100]
[71, 89, 95, 100]
['alice', 'bob', 'chen', 'dani']

Read a comprehension [f(x) for x in xs if cond(x)] straight across, like a sentence: “f of x, for each x in xs, where the condition holds.” That left-to-right reading is the whole trick.

Methods you will use often

MethodWhat it does
lst.append(x)add x to the end
lst.extend(iterable)add every element of an iterable
lst.insert(i, x)insert x at index i
lst.remove(x)remove the first occurrence of x
lst.pop(i=-1)remove and return the element at i
lst.index(x)find the index of the first x
lst.count(x)how many times x appears
lst.reverse()reverse in place
lst.sort(key=, reverse=)sort in place
lst.clear()empty the list
len(lst)number of elements
x in lstmembership test — scans every element, so O(n)

That last row hides a real performance lesson. A membership test x in lst walks the list from the front until it finds a match, so on a list of thousands it is slow when you do it repeatedly. If you keep asking “is this in here?”, a set answers in roughly constant time because it uses a hash table — and that is a lesson coming up shortly.

In one breath

  • Lists are ordered and mutable; append adds one item, extend adds each element of an iterable.
  • Slice with [start:stop:step]; slicing returns a new list and never mutates the original.
  • sort() reorders in place and returns None; sorted() returns a new list — key= orders by anything you can compute.
  • a = b binds a second name to one list; copy with .copy(), list(a), a[:], or copy.deepcopy for nesting.
  • Repeated x in lst is O(n) — switch to a set when membership is the hot path.

Practice

Quick check

0/3
Q1After `xs = [1, 2, 3]; ys = xs; ys.append(4)`, what is `xs`?
Q2Which sorts a list of dicts by their `score` field, highest first?
Q3What is the difference between `list.sort()` and `sorted(list)`?

What’s next

Lists are for collections that change. When you want a fixed, hashable record instead — an (x, y) coordinate, a row pulled from a table — the right tool is the tuple, and that is next.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
When and how do you use enumerate() and zip() in Python, and what are common mistakes when using them together?

enumerate() pairs each element with its index without maintaining a manual counter. zip() pairs elements from multiple iterables together. Both return lazy iterators, so they compose efficiently. The key trap with zip() is silent truncation when iterables differ in length.

Write a function that returns all duplicate values in a list. What is the optimal time complexity?

The optimal solution is O(n) using a set to track seen elements and a second set to collect duplicates, avoiding any nested iteration. A Counter-based approach is equally O(n) and often more readable.

How do you flatten a nested list in Python, and how does the approach differ for one level deep versus arbitrarily deep nesting?

For a single level of nesting, a list comprehension or itertools.chain.from_iterable is idiomatic and O(n). For arbitrary depth, recursion or an explicit stack is required. The right choice depends on whether the structure is known at write time.

How does Python list slicing work, including step and negative indices?

Slicing uses the syntax `seq[start:stop:step]` and returns a new list containing elements from index `start` up to but not including `stop`, stepping by `step`. Negative indices count from the end; a negative step reverses direction. Omitted parts default to the beginning, end, or step of 1.

When would you use a Python list versus a NumPy array, and what are the performance trade-offs?

Python lists are heterogeneous, pointer-based, and general-purpose. NumPy arrays are homogeneous, stored as contiguous typed memory, and support vectorised operations that run at C speed. For numerical work on more than a few hundred elements, NumPy is almost always faster and more memory-efficient.

What is the difference between a list and a tuple, and when should you use each?

Lists are mutable sequences; tuples are immutable. Use a tuple when the collection of items is fixed by meaning — coordinates, RGB values, function return values — and a list when the collection will grow, shrink, or be modified in place. Immutability also makes tuples hashable, so they can serve as dict keys or set members.

How do you reverse a list and remove duplicates in Python, and what are the performance implications of each approach?

Reversing a list is O(n) whether you use slice notation or list.reverse(). Deduplication is O(n) with a set conversion but O(n²) if you check membership against a list. Understanding when order must be preserved changes which tool to reach for.

What is the difference between a shallow copy and a deep copy, and when does it matter?

A shallow copy creates a new container but populates it with references to the same inner objects. A deep copy creates a new container and recursively copies every nested object. The difference only matters when the data structure contains mutable nested objects — for flat structures of immutables, shallow copy is sufficient and faster.

How do you sort a list of dictionaries by a specific key in Python, and what is the difference between sorted() and list.sort()?

Use sorted() with a key= lambda to produce a new sorted list, or list.sort() to sort in place. Both use Timsort and run in O(n log n). sorted() works on any iterable and returns a new list; list.sort() operates in place and returns None.

Related lessons

Explore further

Skip to content