Python List – 15 Things You MUST Know

Python lists hold an ordered row of references you can read, change, and grow in place. I hit the classic alias surprise while verifying the samples for this page, I ran b = a followed by b.append(99) on Python 3.11.16 and saw a change too, so this page shows the copy that keeps the original intact, plus every indexing and slicing move that beginners mistype.

I tested every snippet below in a fresh virtual environment with no extra packages, and I kept the exact interpreter output for the alias, IndexError, and append-on-slice cases. In practice you will create a list, read a position, slice a run, add or remove an item, and loop over what remains, which means this page follows that same order.

What a Python List Is

A list is a mutable sequence of object references that keeps insertion order while allowing duplicates and None, and it grows or shrinks as you mutate the same object.

It differs from nearby types in one property each. A tuple is ordered but immutable, a set is mutable but unordered and unique, and a dict maps keys to values rather than positions.

fruits = ["Apple", "Banana", "Orange"]
print(fruits, len(fruits))
print(type(fruits).__name__)

The display shows three items and a length of three, because len counts references and not the size of the objects they point to.

empty = []
print(empty, len(empty))
print(list("abc"))
print(list((1, 2)))

An empty pair of brackets is an empty list. The list() constructor builds one from any iterable, so a string becomes a list of characters.

hetero = [1, "A", 2.5, True, (1, 2)]
print(hetero)
nested = [1, [2, 3], [4, 5, 6], 7]
print(nested)

Python lists are heterogeneous and store references, so one list can hold numbers, strings, and nested lists together.

What You Need Before You Start

You need Python 3.8 or newer. I ran everything here on Python 3.11.16, so every call below matches that runtime without extra installs.

Use either the REPL or a single script file for the samples, because the examples assume one file. You can copy and run them unchanged.

import sys
print(sys.version.split()[0])

That one-liner prints the version you are actually running. If it shows 3.11.16 or later, the syntax and methods below behave as described.

  • One Python install with no third-party libraries.
  • A folder for the demo script. The screenshots in this page used /home/ubuntu/python-list-demo.
  • The built-in copy module for the deep-copy comparison, which ships with Python.

How to Create and Read Lists

Creating and reading are the first two moves. This section builds lists three ways, then reads positions and slices.

Create with brackets, list(), and comprehension

fruits = ["Apple", "Banana", "Orange"]
empty = []
print(fruits, empty)
print(list(range(5)))
print([x*x for x in range(5)])

Range gives a quick numeric list, a comprehension builds one from an expression, and multiplication repeats a reference. For mutable inner objects prefer a comprehension over multiplication, because multiplication repeats the same object.

Read by index and negative index

vowels = ["a", "e", "i", "o", "u"]
print(vowels[0])
print(vowels[4])
print(vowels[-1])
print(vowels[-2])

Indices start at zero and negative indices count from the end. The last element is -1 and not len, which means vowels[len(vowels)] would fail.

vowels = ["a", "e", "i", "o", "u"]
try:
    print(vowels[40])
except IndexError as e:
    print("IndexError:", e)

I reproduced that IndexError on purpose and the interpreter reported list index out of range. The fix is a bounds check or using a slice that never raises.

Slice with start:stop:step

letters = ["a", "b", "c", "d", "e", "f"]
print(letters[1:4])
print(letters[:3])
print(letters[3:])
print(letters[-3:])
letters = ["a", "b", "c", "d", "e", "f"]
print(letters[::2])
print(letters[::-1])
print(letters[1:5:2])

A slice copies references into a new list and never raises for out-of-range bounds. That is why letters[1:40] returns what exists rather than throwing.

a = [1, 2, 3, 4, 5]
a[1:3] = [20, 30]
print(a)
a[1:4] = []
print(a)

Read nested elements

nested = [1, [2, 3], [4, 5, 6], 7]
print(nested[1][0])
print(nested[2][2])
print([1, (2, 3), (4, 5, 6), 7][-3][0])
Terminal output showing list indexing, negative indexing, slicing with step, and an IndexError for index 40
Indexing is zero based, slicing copies, and an out of range index raises IndexError.
OperationExampleResult type
indexa[0]element
slicea[1:4]new list
stepa[::2]new list

How to Change, Add, and Remove Elements

Mutation is why lists exist. The next three verbs cover the whole surface in order and they are change, add, and remove.

Change a single element

nums = [10, 20, 30]
nums[1] = 99
print(nums)
try:
    nums[10] = 1
except IndexError as e:
    print("IndexError:", e)

Assignment replaces the reference at that position immediately when you assign to an existing index. An index outside the current length raises.

Add with append, extend, and insert

m = [1, 2]
m.append(3)
print(m)
m.append([4, 5])
print(m)
n = [1, 2]
n.extend([4, 5])
print(n)
n = [1, 2, 3]
n.insert(1, 99)
print(n)

Append adds one object and can nest a list inside a list. Extend iterates the argument and adds each element flat, which is the branch most beginners meant.

Remove with remove, pop, clear, and del

r = [1, 2, 3, 2, 4]
r.remove(2)
print(r)
try:
    r.remove(99)
except ValueError as e:
    print("ValueError:", e)
p = [10, 20, 30, 40]
print(p.pop(), p)
print(p.pop(0), p)
try:
    [].pop()
except IndexError as e:
    print("IndexError:", e)
c = [1, 2, 3]
c.clear()
print(c)
d = [1, 2, 3, 4, 5]
del d[1]
print(d)
del d[1:3]
print(d)
del d[:]
print(d)
Terminal output showing append adding one item, extend flattening, insert at a position, and remove pop clear del results
Append adds one reference, extend adds many, and pop returns what it removes.

How to Copy, Loop, Sort, and Check Membership

Copying and looping decide whether the next change stays isolated. This section also covers sorting and the in check.

Copy without aliasing

a = [1, 2, 3]
b = a
b.append(99)
print(a, b is a)
a = [1, 2, 3]
b = a.copy()
b.append(99)
print(a, b, b is a)

a = [1, 2, 3]
b = a[:]
print(a, b)

a = [1, 2, 3]
b = list(a)
print(a, b)

Assignment shares the object and so every mutation shows through both names. I verified that copy, slice, and list() each produced a new outer list where appending to the copy left the original at [1, 2, 3].

import copy
nested = [[1, 2], [3, 4]]
shallow = nested.copy()
shallow[0].append(99)
print(nested, shallow, nested[0] is shallow[0])

nested = [[1, 2], [3, 4]]
deep = copy.deepcopy(nested)
deep[0].append(99)
print(nested, deep)
Terminal output showing alias mutation leaking to the original, copy keeping the original intact, and shallow vs deep copy on nested lists
Assignment shares one object, copy shares only the outer list, and deepcopy separates everything.

Loop and comprehension

lst = [10, 20, 30]
for x in lst:
    print(x)
print(list(enumerate(lst)))
print([x*2 for x in lst])
q = [1, 2, 3]
x, y, z = q
print(x, y, z)

A for loop visits each reference in order and covers the basic iteration case, while enumerate adds the index and a comprehension builds a new list. Unpacking binds positions to names.

Sort, reverse, and keep the original

s = [3, 1, 4, 1, 5]
print(sorted(s), s)
s.sort()
print(s)
s = ["banana", "Apple", "cherry"]
s.sort(key=str.lower)
print(s)
r = [3, 1, 2]
r.reverse()
print(r)

Sorted returns a new list and leaves the source alone. Sort mutates in place and returns None, so assigning its result loses the list.

Membership, length, and operators

print([1, 2] + [3, 4])
print([0]*3)
print(2 in [1, 2, 3])
print(99 not in [1, 2, 3])
print(len([1, 2, 3]))
x = [1, 2, 3, 2, 1]
print(x.index(2))
print(x.index(2, 2))
print(x.count(2))
try:
    x.index(99)
except ValueError as e:
    print("ValueError:", e)
MethodWhat it doesReturn value
append(x)adds one objectNone
extend(iterable)adds each elementNone
insert(i, x)inserts before iNone
remove(x)removes first xNone
pop([i])removes and returnsthe item
clear()empties the listNone
copy()shallow copynew list
sort()sorts in placeNone
reverse()reverses in placeNone

When Lists Break With IndexError, Aliasing, and Slicing Gotchas

Two failures cover most reports from beginners who are new to mutable sequences. An index past the end raises and an alias leaks.

IndexError from a bad index

Access uses a position that must exist. Slicing uses bounds that may not, which is why one raises and the other does not.

a = ["a", "e", "i"]
print(a[2])
print(a[-1])
print(a[10:20])
try:
    print(a[10])
except IndexError as e:
    print("IndexError:", e)

Append on a slice does not grow the original

a = [1, 2, 3, 4, 5]
a[1:].append(a[0])
print(a)

a = [1, 2, 3, 4, 5]
s = a[1:]
s.append(a[0])
print(a, s)

I ran a[1:].append(a[0]) exactly as the Stack Overflow report wrote it and the original stayed at [1, 2, 3, 4, 5] because the slice is a new list. Build the rotated list with concatenation or append to the intended target instead.

Mutating while iterating

orig = [1, 2, 3, 4]
evens = [x for x in orig if x % 2 == 0]
print(evens)
print(max(orig), min(orig), sum(orig))

Removing or inserting while a for loop runs skips elements or repeats them. Filter into a new list with a comprehension, or iterate over a snapshot like list(orig) when you must.

What You Can Do Next

You now have the full edit loop: create, read, slice, change, add, remove, copy, loop, sort, and check. The one habit worth keeping is copying before you mutate when the source must survive.

  • Use copy or a[:] slice for a flat list and copy.deepcopy for nested ones.
  • Prefer extend when the argument is a sequence you want flattened.
  • Reach for list comprehension for one-pass transforms, and for the list of tuples when each row carries two fields.

Frequently Asked Questions

What is a Python list?

A Python list is a mutable ordered sequence of object references created with brackets []. It allows duplicates and None, grows dynamically, and supports indexing, slicing, and in-place mutation.

How do you create a list in Python?

Use brackets with comma separated values, the list() constructor from any iterable, or a comprehension. Examples: [] for empty, [1, 2, 3] for literals, list(range(5)) for numbers, and [x*x for x in range(5)] for computed values.

What is the difference between append and extend?

Append adds one object, which can nest a list inside a list. Extend iterates the argument and adds each element to the end, so extend([4, 5]) on [1, 2] gives [1, 2, 4, 5] while append([4, 5]) gives [1, 2, [4, 5]].

Why does modifying a copied list change the original?

Assignment shares the same object, so b = a followed by b.append changes a. Use b = a.copy(), b = a[:], or b = list(a) for a flat copy. For nested lists use copy.deepcopy, because a shallow copy still shares inner objects.

How do I fix IndexError: list index out of range?

The index you used does not exist. Check that 0 <= index < len(list) before access, use negative indices for counting from the end, or use a slice which never raises for out-of-range bounds.

Pankaj Kumar
Pankaj Kumar

I have been working on Python programming for more than 12 years. At AskPython, I share my learning on Python with other fellow developers.

Articles: 256