Python Lists: Create, Slice, and Iterate
Picture a shelf with numbered slots running left to right. You can read what is in slot 0, swap the thing in slot 2 for something else, push a new item onto the end, or shuffle the whole row into alphabetical order. Nothing stops you putting the same thing in two slots.
That shelf is the mental model for a Python list, and every rule below follows from it. Position is real and it is stored, the contents can change after the shelf is built, and duplicates are allowed because slots do not care what their neighbours hold.
What Is a Python List?
A 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.
Three terms carry the rest of this guide. 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 neighbours are the tuple, an immutable sequence, and the dictionary, which stores items under names instead of positions.
How do you create a list?
Square brackets are the usual way, and the list() built-in converts anything you can loop over into a fresh list:
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.
Indexing and Slicing
Indexes start at zero, so the first slot on the shelf is [0] and the fourth is [3]:
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.
Adding and Removing Items
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.
Remove, Pop, and Delete
Removal comes in three flavours 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.
Sorting and Reversing
Python offers a pair for each job: a method that rearranges the list you already have, and a built-in that leaves it alone and hands back a new one.
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 pattern. The reverse method flips the list in place, while list(reversed(words)) and the [::-1] slice each build a new one.
Why does copying a list catch people out?
Assignment gives a second name to the same shelf. It does not build a second shelf, so a change through either name is visible through both:
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 are 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 genuinely 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.
What List Operations Actually Cost
A list stores its references in one contiguous block with room to spare at the end. That single implementation detail explains which operations are cheap and which are not, in CPython, which is the interpreter almost everyone runs.
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.
Two practical consequences follow. 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
Here is the type doing a real job. 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'] Each step maps to a rule the shelf can express. Appending grows the roster in the order entries arrive, the membership test drops repeats, and sorting at the end reorders the whole shelf once rather than on every pass.
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'] Same output, different cost. The list still holds the order, and the set answers the "have I seen this?" question without walking anything. Keeping both is a normal move, not a hack.
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 instead, which states the intent directly and cannot skip anything:
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 genuinely optional.
When a List Is the Wrong Choice
A list is the right default for a sequence, and there are four cases where something else fits the data better.
- 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.dequeis built for that traffic, while a list pays a shifting cost at the front.
The comparison page works through the first two of those in detail, including the decision table and what going wrong actually costs.
Frequently Asked Questions
Are Python lists the same as arrays?
Not in the C or Java sense. A Python list holds references to objects of any type and resizes itself, while a typed array has a fixed element type. When you need a compact numeric array, the standard library offers array.array and NumPy offers ndarray.
When should you use a tuple instead of a list?
Use a tuple when the collection is fixed once built, such as a coordinate pair or a function returning several values. A tuple of hashable items can be a dictionary key or a set member, which a list can never be because a list is mutable.
Can a list hold different types at once?
Yes. A list stores references, so numbers, strings, other lists, and your own objects can sit side by side. Mixed lists usually mean the data wanted a dictionary or a small class instead, because reading position 3 tells you nothing about what lives there.
What is the difference between sort and sorted?
The sort method reorders the list in place and returns None. The sorted built-in leaves the original untouched and returns a new list. Both accept the same key and reverse arguments, and sorted also accepts any iterable, not only a list.
How do you remove duplicates from a list?
Wrap it in set and convert back with list, which is the shortest route but does not promise the original order. When order matters, dict.fromkeys keeps first appearance because dictionaries preserve insertion order in Python 3.7 and later.
Does a list have a maximum size?
There is no fixed limit in the language, and available memory is the practical ceiling since every element costs a pointer plus whatever the object itself holds. On a typical 64-bit build, sys.maxsize is 2**63 - 1, far beyond a practical list length.
Why does append return None?
Methods that change a list in place return None by convention, so nothing tempts you to treat the result as a new list. Writing items = items.append(x) therefore replaces your list with None, which is a common first-week bug.
Self-Check
- What does
[1, 2, 3, 4][1:3]evaluate to, and why is it two items rather than three? - After
a = [1, 2]anda.append([3, 4]), what islen(a)? - Which of
sortandsortedreturnsNone, and what does the other return? - Why does
rows = [[]] * 3followed byrows[0].append(1)change all three rows? - What does
numbers[50]raise on a ten-item list, and what doesnumbers[50:60]return? - Why is inserting at position 0 more expensive than appending?
- A function has
def tag(name, tags=[]). What goes wrong on the second call, and what is the fix?
Answers
[2, 3]. A slice includes the start position and excludes the stop position, so1:3covers indexes 1 and 2.- 3. The
appendmethod adds one item, and here that item happens to be a list, soais[1, 2, [3, 4]]. sortreturnsNone. It reorders the list in place, whilesortedleaves the original alone and returns a new list.- 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.
IndexError, and an empty list. Plain indexing must land on a real position, while slicing clamps to what exists.- 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.
- The second call sees the first call's list. The default is created once at definition time, so default to
Noneand build the list inside the function.
Where to Go Next
The numbered shelf holds up all the way down. Position is stored, so indexing and slicing are cheap and order survives every operation you do not ask to change. Contents are mutable, so methods rearrange the shelf you already have while built-ins hand back a new one.
The two habits worth taking away are appending rather than inserting at the front, and remembering that a shelf can be renamed without being duplicated. Nearly every surprising list bug is one of those two facts arriving late.
Related
- Python Dictionaries - store values under names you choose instead of numbered positions.
- Lists vs Dictionaries vs Sets in Python - the decision table for picking a collection, and the cost of picking wrong.
- List and Dict Comprehensions in Python - build a list from another iterable in one expression.
- Python - the language hub and what to learn next.
Sources
-
[1]
Data Structures: More on Lists(docs.python.org)
-
[2]
Sequence Types: list, tuple, range(docs.python.org)
-
[3]
copy: Shallow and deep copy operations(docs.python.org)
-
[4]
Design and History FAQ(docs.python.org)
Read Next
A Python dictionary stores values under names you choose instead of numbered positions. Creating one, reading keys safely, updating and merging, looping over views, counting and grouping, and the JSON boundary.
Three built-in collections answer three different questions: what came in what order, what is stored under this name, and have I seen this before. A criteria-based way to pick one, with the cost of picking wrong.
A beginner's guide to Python: data structures, scripting, and tutorials that start from plain-English syntax and build into real projects.