Python Lists: Create, Slice, and Iterate

Published Updated

A Python list stores items in order and lets you add, remove, or change them. Use a list for a sequence such as names, scores, or records that you want to process one at a time.

Think of a shelf with numbered slots. Each slot has a position, but two slots can hold the same value. This guide covers the everyday operations and the copying mistakes worth catching early. For the wider learning path, see the Python guide.

What Is a Python List?

A Python list is an ordered, changeable collection of values. Ordered means each item has a position you can ask for. Changeable, or mutable in Python's own vocabulary, means you can replace, add, and remove items after the list exists.

An index is a position, counted from zero. A slice is a range of positions. Mutation is any change made to the list itself rather than to a copy of it.

A list holds references rather than the values themselves, which is why it accepts any mix of types and why copying needs a section of its own further down. Its closest neighbors are the tuple, an immutable sequence, and the dictionary, which stores items under names instead of positions.

How Do You Create a Python List?

Create a Python list with square brackets, or pass an iterable to list() to build a list from its items:

fruits = ["apple", "banana", "cherry"]
empty = []
letters = list("hello")
digits = list(range(5))

print(fruits)
print(letters)
print(digits)
# Program output:
# ['apple', 'banana', 'cherry']
# ['h', 'e', 'l', 'l', 'o']
# [0, 1, 2, 3, 4]

Multiplying a list by an integer repeats its contents, which is a convenient way to lay out a row of placeholders:

slots = [0] * 5
print(slots)
# Program output:
# [0, 0, 0, 0, 0]

That shortcut has one sharp edge involving nested lists, and it gets its own pitfall below. For simple values such as numbers and strings it behaves exactly as it reads.

How Do You Access Items in a Python List?

Access a Python list item with its index in square brackets. Indexes start at zero, so fruits[0] reads the first item and fruits[3] reads the fourth:

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

print(fruits[0])
print(fruits[3])
fruits[1] = "blueberry"
print(fruits)
# Program output:
# apple
# date
# ['apple', 'blueberry', 'cherry', 'date']

Counting Backwards

A negative index counts from the right-hand end of the shelf, so -1 is the last item. It saves writing the length arithmetic yourself:

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

print(fruits[-1])
print(fruits[-2])
# Program output:
# date
# cherry

Slices Take a Range

A slice is written [start:stop:step] and always returns a new list. The stop position is excluded, which is why [2:5] gives three items rather than four:

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(numbers[2:5])
print(numbers[:3])
print(numbers[7:])
print(numbers[::2])
print(numbers[::-1])
# Program output:
# [2, 3, 4]
# [0, 1, 2]
# [7, 8, 9]
# [0, 2, 4, 6, 8]
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Leaving a position out means "from the beginning" or "to the end". A step of -1 walks the shelf right to left, which is the shortest way to reverse a list without changing the original.

Slices are also forgiving where plain indexing is not. Asking for numbers[50:60] returns an empty list, while asking for numbers[50] raises an error.

How Do You Count, Find, and Loop Through List Items?

Use len() to count all items in a Python list, count() to count a particular value, and in to check whether a value is present. A for loop visits each item in order.

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

print(len(fruits))           # 3
print(fruits.count("apple")) # 2
print("banana" in fruits)    # True

for fruit in fruits:
    print(fruit)

Use fruits.index("banana") to find the first matching position. It returns 1 here and raises ValueError if the value is absent. Use enumerate(fruits) when a loop needs both the index and the item.

How Do You Add or Remove Python List Items?

Add Python list items with append(), extend(), or insert(). Remove an item by value with remove(), or by position with pop() or del.

Append, Extend, and Insert

Three methods add to a list, and the difference between the first two is the single most common mix-up in this area:

shelf = ["apple", "banana"]

shelf.append("cherry")
shelf.extend(["date", "elderberry"])
shelf.insert(1, "apricot")

print(shelf)
# Program output:
# ['apple', 'apricot', 'banana', 'cherry', 'date', 'elderberry']

The append() method adds exactly one item, even when that item is itself a list. The extend method loops over what you give it and adds each element separately:

a = [1, 2]
b = [1, 2]

a.append([3, 4])
b.extend([3, 4])

print(a)
print(b)
# Program output:
# [1, 2, [3, 4]]
# [1, 2, 3, 4]

The insert method takes a position first and the value second. Inserting at position 0 puts an item at the front, and every other item shifts one slot along, which is worth remembering when the list is long. The Python list insert() guide shows how the index picks the spot, including negative and out-of-range positions.

Remove, Pop, and Delete

Removal comes in three flavors depending on whether you know the value, the position, or neither:

shelf = ["apple", "banana", "cherry", "banana", "date"]

shelf.remove("banana")
taken = shelf.pop(1)
last = shelf.pop()
del shelf[0]

print(taken, last)
print(shelf)
# Program output:
# cherry date
# ['banana']

The remove method deletes the first matching value and raises ValueError when there is none. The pop method deletes by position and hands the item back, defaulting to the last slot. The del statement deletes without returning anything, and shelf.clear() empties the list entirely.

The remove() vs pop() guide covers the errors each one raises and the cases where remove() deletes an item you didn't expect.

How Do You Sort or Reverse a Python List?

Sort a Python list in place with sort(), or create a sorted copy with sorted(). To reverse the current order without sorting, use reverse() in place or [::-1] for a reversed copy.

scores = [3, 1, 4, 1, 5]

ordered = sorted(scores)
scores.sort(reverse=True)

print(ordered)
print(scores)
# Program output:
# [1, 1, 3, 4, 5]
# [5, 4, 3, 1, 1]

Both accept a key function that says what to sort on, which is how you sort words by length or records by one field:

words = ["kiwi", "banana", "fig", "plum", "apple"]

print(sorted(words, key=len))
# Program output:
# ['fig', 'kiwi', 'plum', 'apple', 'banana']

Python's sort is stable, which means items the key rates as equal keep the order they already had. Here kiwi stays ahead of plum because both are four letters long and kiwi appeared first in the input.

Reversing follows the same in-place or new-list pattern. The reverse method flips the list in place, while list(reversed(words)) and the [::-1] slice each build a new one.

Why Can Changing a Copied List Affect the Original?

Assigning a Python list to another variable does not copy it. Both names refer to the same list, so changes appear through either name. Even a shallow copy still shares nested objects, as the second example below shows:

original = [1, 2, 3]
alias = original
alias.append(4)

print(original)
# Program output:
# [1, 2, 3, 4]

Three spellings make a genuine copy of the outer list, and they behave identically for values such as numbers and strings:

original = [1, 2, 3]

first = original.copy()
second = list(original)
third = original[:]

first.append(4)
print(original, first)
# Program output:
# [1, 2, 3] [1, 2, 3, 4]

All three of those spellings make shallow copies. The new list is independent, but the objects inside it are still shared, so a nested list is reachable from both copies:

import copy

grid = [[1, 2], [3, 4]]
shallow = grid.copy()
deep = copy.deepcopy(grid)

shallow[0].append(99)

print(grid)
print(deep)
# Program output:
# [[1, 2, 99], [3, 4]]
# [[1, 2], [3, 4]]

Reach for copy.deepcopy only when the nested objects need to be independent. It walks the whole structure, so it costs more than a slice, and on flat lists of numbers or strings it buys you nothing.

Which Python List Operations Are Fast?

In CPython, reading an item by index and appending at the end are generally fast. Inserting near the front or searching for a value takes more work as the list grows. CPython stores list references together with spare room for new items at the end.

Reading or writing by index is a direct jump, so its cost does not grow with the list. Appending is cheap for the same reason, apart from the occasional resize when the spare room runs out.

Inserting or deleting anywhere except the end has to shift every later element along by one slot, so the work grows with how much of the list sits after that point. Searching with in or index walks the list until it finds a match, so it grows the same way.

Those costs lead to two practical habits. Build lists by appending rather than by inserting at position 0, and when you need to test membership repeatedly against a large collection, build a set once and test against that instead. For a queue where items leave from the front, collections.deque is designed for exactly that shape.

A Worked Example: Tidying a Signup List

Names arrive from a form with stray spacing, inconsistent capitals, blanks, and repeats, and the page needs a clean alphabetical roster.

raw = ["  Ada ", "bob", "", "ADA", "Cleo", "bob "]

names = []
for entry in raw:
    cleaned = entry.strip().title()
    if cleaned and cleaned not in names:
        names.append(cleaned)

names.sort()
print(names)
# Program output:
# ['Ada', 'Bob', 'Cleo']

The loop trims spaces, fixes capitalization, and skips empty or repeated names. It appends each accepted name, then sorts the finished list once.

The membership test is the line to watch as the data grows. On a signup form of a few hundred names it is invisible, and on a mailing list of a million it is the slowest thing in the script, because each test walks everything gathered so far.

raw = ["  Ada ", "bob", "", "ADA", "Cleo", "bob "]

seen = set()
names = []
for entry in raw:
    cleaned = entry.strip().title()
    if cleaned and cleaned not in seen:
        seen.add(cleaned)
        names.append(cleaned)

print(sorted(names))
# Program output:
# ['Ada', 'Bob', 'Cleo']

This version prints the same names, and once many different names pile up it usually runs much faster. The list still holds the order, and the set answers whether a name has already been seen with a hash lookup instead of a scan. Keeping a list and a set side by side like this is a normal, everyday pattern.

Pitfalls and Debugging

Removing Items While Looping

Deleting from a list you are currently looping over skips elements, because the loop's internal position keeps advancing while the items shuffle down to meet it:

numbers = [1, 2, 4, 6, 7]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)

print(numbers)
# Program output:
# [1, 4, 7]

The 4 survives because removing 2 moved it into the slot the loop had already passed. Nothing raises an error, which is what makes this one expensive to find.

Build a new list with a list comprehension instead, keeping only the values you want:

numbers = [1, 2, 4, 6, 7]
numbers = [n for n in numbers if n % 2 != 0]

print(numbers)
# Program output:
# [1, 7]

The Repeated Empty List Trap

The repetition shortcut copies references, not objects. With numbers that is invisible, and with nested lists it produces three names for one list:

rows = [[]] * 3
rows[0].append(1)
print(rows)

rows = [[] for _ in range(3)]
rows[0].append(1)
print(rows)
# Program output:
# [[1], [1], [1]]
# [[1], [], []]

The comprehension runs [] once per row, so each row is a separate list. Use the multiplication form for immutable fillers such as 0 or None, and the comprehension form whenever the filler is something you intend to change.

A Mutable Default Argument

Default arguments are evaluated once, when the function is defined. A list used as a default is therefore shared by every call that does not pass its own:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))
print(add_item("b"))
# Program output:
# ['a']
# ['a', 'b']

The fix is to default to None and create the list inside the body, so each call that needs one gets a fresh list:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("a"))
print(add_item("b"))
# Program output:
# ['a']
# ['b']

IndexError and ValueError

Two named errors come up constantly, and their messages say which mistake you made. Reading past the end of a list raises IndexError: list index out of range, which usually means a loop counter or an off-by-one calculation went past the length.

Calling remove or index with a value the list does not hold raises ValueError: list.remove(x): x not in list or a similar message for index. Test with in first when the value is optional.

When Should You Use Something Other Than a Python List?

Use a Python dictionary for lookups by key, a set for unique values and repeated membership checks, a tuple for a fixed sequence, or a deque for frequent changes at both ends. Keep a list when positions and a changeable sequence are what you need.

  • When items are looked up by name rather than by position, a dictionary stores the name with the value instead of making you remember which slot it went into.
  • When the only question is "have I seen this?", a set answers it without walking the collection and refuses duplicates for you.
  • When the collection must not change after it is built, a tuple says so in the type, and a tuple of hashable items can also be a dictionary key.
  • When items are added and removed at both ends, collections.deque is built for that traffic, while a list pays a shifting cost at the front.

Our list, dictionary, and set comparison shows which collection fits each job.

Frequently Asked Questions

Are Python lists the same as arrays?

Python lists can hold different types and grow as items are added, while typed arrays restrict their elements to one type. For compact numeric storage, Python offers array.array in the standard library; NumPy provides ndarray for numerical work.

When should you use a Python tuple instead of a list?

Use a Python tuple instead of a list when the sequence should stay fixed, such as a coordinate pair. A tuple containing only hashable objects can also be a dictionary key or set member. Nested mutable objects inside a tuple can still change.

Can a Python list hold different types at once?

A Python list can hold numbers, strings, other lists, and custom objects together. If each position represents a named field, a dictionary or small class may make the data easier to understand.

What is the difference between Python list.sort() and sorted()?

Python list.sort() reorders an existing list and returns None. The sorted() function returns a new sorted list without changing the original. Both accept key and reverse arguments, and sorted() can take any iterable.

How do you remove duplicates from a Python list?

For a Python list of hashable items, list(dict.fromkeys(items)) removes duplicates while keeping their first appearance. list(set(items)) also removes duplicates but does not preserve the original order. For unhashable items such as nested lists, build a new list and check whether each item is already present.

Does a Python list have a maximum size?

A Python list is limited by the interpreter and available memory. On a typical 64-bit build, sys.maxsize is 2**63 - 1, but a machine will run out of memory long before a list reaches that length.

Why does Python list.append() return None?

Python list.append() changes the existing list and returns None rather than a new list. Call items.append(x) on its own. Writing items = items.append(x) replaces the variable with None.

Self-Check

  1. What does [1, 2, 3, 4][1:3] evaluate to, and why is it two items rather than three?
  2. After a = [1, 2] and a.append([3, 4]), what is len(a)?
  3. Which of sort and sorted returns None, and what does the other return?
  4. Why does rows = [[]] * 3 followed by rows[0].append(1) change all three rows?
  5. What does numbers[50] raise on a ten-item list, and what does numbers[50:60] return?
  6. Why is inserting at position 0 more expensive than appending?
  7. A function has def tag(name, tags=[]). What goes wrong on the second call, and what is the fix?

Answers

  1. [2, 3]. A slice includes the start position and excludes the stop position, so 1:3 covers indexes 1 and 2.
  2. 3. The append method adds one item, and here that item happens to be a list, so a is [1, 2, [3, 4]].
  3. sort returns None. It reorders the list in place, while sorted leaves the original alone and returns a new list.
  4. Because the repetition copies the reference three times. All three positions name one list, so appending through any of them is visible from all of them.
  5. IndexError, and an empty list. Plain indexing must land on a real position, while slicing clamps to what exists.
  6. Every later element shifts one slot along. Appending is amortized constant time because it usually writes into spare room at the end, with occasional resizes.
  7. The second call sees the first call's list. The default is created once at definition time, so default to None and build the list inside the function.

Where to Go Next

Practice choosing between changing a list and making a copy. Then try the signup example with your own data. The difference between append() and extend(), and between assignment and copying, will come up often.

Sources

  1. [1]
  2. [2]
  3. [3]
  4. [4]
    Design and History FAQ
    (docs.python.org)