Python Lists, Tuples, and Dictionaries: A Complete Guide (2026)
Python lists, tuples, and dictionaries are where the mutability rules from the last article stop being theoretical. A shopping cart function does cart_backup = cart before running a risky operation, expecting an independent snapshot. It isn't one. Both names point to the exact same list, so "backing it up" changed nothing — modifying cart silently modifies cart_backup too, because they were never two separate lists to begin with. This guide covers lists, tuples, and dictionaries in full: indexing, slicing, methods, and the copying rules that actually prevent this bug.
Quick Answer: Lists, Tuples, and Dictionaries
A list ([]) is an ordered, mutable collection — items can be added, removed, or changed after creation. A tuple (()) is ordered but immutable — fixed once created. A dictionary ({}) stores key-value pairs instead of positions, so values are looked up by name rather than index. Plain assignment between two list variables never copies the list — both names end up pointing to the same object, which is the single most common structural bug in this area.
The Backup That Wasn't
A function needs to try a risky bulk update on a shopping cart, with a fallback to the original if anything goes wrong. The instinct is to "save a backup" first:
cart = ["shirt", "shoes"] cart_backup = cart # looks like a backup — it isn't one cart.append("hat") cart.remove("shirt") print(cart_backup) # ['shoes', 'hat'] — the "backup" changed too
Nothing errors. Nothing warns. cart_backup = cart doesn't create a second list — it creates a second name pointing at the exact same list object in memory. Every mutation through cart is visible through cart_backup, because as far as Python is concerned, there was only ever one list. This is a direct consequence of the mutability covered in the previous article — and the actual fix is covered in the copying section below.
Lists: The Mutable Workhorse
A list holds an ordered collection of items, and unlike a tuple, it can grow, shrink, and change after creation.
fruits = ["apple", "banana", "cherry"] fruits.append("date") # add to the end fruits.insert(0, "apricot") # insert at a specific position fruits.remove("banana") # remove by value fruits.sort() # sort in place, alphabetically print(len(fruits)) # 4
A list can hold mixed types — [1, "two", 3.0, True] is completely valid — though in practice, data science code usually keeps a list's contents uniform, since that's what most operations downstream expect.
Indexing and Slicing
Individual items are accessed by position, starting at 0; slices pull out a sub-range without modifying the original.
numbers = [10, 20, 30, 40, 50] print(numbers[0]) # 10 — first item print(numbers[-1]) # 50 — last item, negative indexing print(numbers[1:3]) # [20, 30] — index 1 up to (not including) 3 print(numbers[:2]) # [10, 20] — from the start print(numbers[::-1]) # reversed copy of the whole list
The slice's end index is always exclusive — numbers[1:3] stops before index 3, returning exactly two items. This trips up beginners coming from languages with inclusive ranges, and it's worth memorizing early since it's consistent across lists, tuples, and strings alike.
Tuples: Fixed and Hashable
A tuple looks like a list but can never be changed after creation — no append, no remove, no in-place sort.
coordinates = (40.7128, -74.0060) rgb_red = (255, 0, 0) # coordinates.append(1) # AttributeError — tuples have no append # unpacking works the same as with lists lat, lon = coordinates
Because tuples are immutable, they're hashable — usable as dictionary keys or set members, which a list can never be. Reach for a tuple whenever a value is genuinely fixed by nature: coordinates, an RGB triple, or a function returning more than one value.
Dictionaries: Key-Value Pairs
A dictionary looks up values by name (a key) instead of by numeric position — the standard structure for labeled, structured data.
customer = {
"name": "Maria",
"age": 28,
"active": True
}
print(customer["name"]) # "Maria" — raises KeyError if missing
print(customer.get("email")) # None — no error, key doesn't exist
print(customer.get("email", "n/a")) # "n/a" — with a default
for key, value in customer.items():
print(key, value)
dict[key] raises a KeyError the moment a key doesn't exist; .get() returns None (or a default you specify) instead, silently. Use square brackets when a missing key is genuinely a bug you want surfaced; use .get() when a missing key is a normal, expected case.
Copying Correctly: The Actual Fix
This is what resolves the hook at the top of this article — creating an independent copy instead of a second name for the same object.
cart = ["shirt", "shoes"] cart_backup = cart.copy() # a genuine, independent copy now cart.append("hat") cart.remove("shirt") print(cart_backup) # ['shirt', 'shoes'] — untouched, exactly as intended
.copy(), list(cart), and the slice cart[:] all create a genuine independent copy. None of these go deep, though — if the list contains other lists or dictionaries nested inside it, the inner objects are still shared between the original and the copy. For fully independent nested structures, copy.deepcopy() from the standard library is the correct tool.
Choosing the Right Structure
| Need | Structure | Why |
|---|---|---|
| A collection that will grow or shrink | list | Mutable, ordered |
| A fixed set of values, or a dict key | tuple | Immutable, hashable |
| Values that need a name, not a position | dict | Key-based lookup |
Mistakes That Give Away a Beginner
- Assuming assignment copies a list. The exact trap from this article's opening — it creates a second reference, not a second list.
- Using square-bracket access when a key might be missing, causing an unhandled KeyError instead of using .get() deliberately.
- Trying to use a list as a dictionary key and hitting a TypeError — lists aren't hashable; use a tuple instead.
- Forgetting slice ends are exclusive, off-by-one errors on the last element of a slice.
- Reaching for a shallow .copy() on nested data and being surprised the inner lists are still shared — deepcopy is needed for that.
Practice: Fix These Three Yourself
Fix the backup bug
Rewrite the shopping-cart example so cart_backup genuinely stays unchanged after cart is modified.
Build a lookup dictionary
Given a list of (name, score) tuples, build a dictionary mapping each name to its score.
Slice practice
Given a list of 10 numbers, return the first 3, the last 3, and the list reversed, using slicing only.
Frequently Asked Questions
What is the difference between a list and a tuple in Python?
A list is mutable, meaning items can be added, removed, or changed after creation, and uses square brackets. A tuple is immutable, meaning it cannot be changed after creation, and uses parentheses. Tuples are typically used for fixed collections of values, while lists are used when the collection needs to grow, shrink, or change.
Why did changing one list also change another list in Python?
This happens when two variables point to the same underlying list object rather than two separate copies. Writing new_list = old_list does not create a copy — it creates a second name pointing to the same list in memory, so a change through either name is visible through both. Use old_list.copy() or list(old_list) to create an independent copy.
Can a dictionary key be a list in Python?
No. Dictionary keys must be hashable, and lists are mutable, which makes them unhashable. Attempting to use a list as a dictionary key raises a TypeError. Tuples, which are immutable, can be used as dictionary keys instead.
How do you copy a list in Python without changing the original?
Use the .copy() method, the list() constructor, or a slice with [:], for example new_list = old_list.copy(). Any of these creates a new, independent list object, so changes to the copy do not affect the original. For nested lists containing other lists, a deep copy from the copy module may be needed instead.
When should I use a tuple instead of a list in Python?
Use a tuple when the collection of values is fixed and shouldn't change, such as coordinates, RGB color values, or a function returning multiple values. Tuples are also required when a fixed collection needs to be used as a dictionary key or stored in a set, since lists cannot be used in either case.
What is the difference between dict.get() and dict[] in Python?
Accessing a dictionary with square brackets, like my_dict['key'], raises a KeyError if the key doesn't exist. dict.get('key') returns None instead of raising an error, and optionally accepts a default value to return, such as dict.get('key', 0), making it the safer choice when a key might be missing.
Conclusion: Know What You're Actually Copying
Lists, tuples, and dictionaries cover almost every data-shape need in everyday Python: an ordered, changeable collection; a fixed, hashable one; or a labeled lookup. The habit worth carrying forward from this article is the one that fixes the opening bug — plain assignment between two variables never copies a mutable object, it just adds a second name to the same one. Reach for .copy(), list(), or deepcopy() the moment independence actually matters.
Practice the three exercises above, then move to the next article in this cluster, which covers control flow — if statements and loops — the logic that actually operates on these structures.
Next Up: Python Control Flow
If statements, loops, and the logic that ties these data structures together.
▶ Read the Control Flow GuideKhalid Hussain
Founder of Review Publically. MSc holder and Google Advanced Data Analytics certified. Teaches Python and SQL data analysis with a focus on current, correct, production-ready code rather than outdated conventions.
Related Articles