Lists vs Dictionaries vs Sets in Python
Lists, dictionaries, and sets are the three collections a Python beginner meets first, and picking between them is the decision that quietly shapes everything built on top. The choice is not about which is best. Each one answers a different question, and the work is deciding which question your data is actually asking.
This page holds the three against each other on the criteria that decide real cases: order, duplicates, lookup, and what is allowed inside. The recommendation comes at the bottom, and it is earned there rather than announced here.
What are lists, dictionaries, and sets?
A list is an ordered, changeable sequence written with square brackets. Every item has a position, positions are how you reach items, and the same value may appear as many times as you like.
A dictionary is a collection of key and value pairs written with braces. Every value is stored under a key you chose, keys are how you reach values, and each key appears at most once.
A set is an unordered collection of unique values, also written with braces but with no colons. There are no positions and no keys, membership is the only question it answers, and adding a value that is already present changes nothing.
arrivals = ["Ada", "Bo", "Ada"]
scores = {"Ada": 91, "Bo": 78}
attended = {"Ada", "Bo"}
print(arrivals, len(arrivals))
print(scores["Ada"])
print("Cleo" in attended) # Program output:
# ['Ada', 'Bo', 'Ada'] 3
# 91
# False Those three lines are the whole comparison in miniature. The list kept the repeat because arrivals repeat, the dictionary attached a number to a name, and the set answered a yes or no question about membership.
Quick Comparison
The behaviours below are what actually differ between the three types, stated as short answers so the table stays readable on a phone.
| Behaviour | List | Dictionary | Set |
|---|---|---|---|
| Keeps insertion order | Yes | Yes, since 3.7 | No |
| Allows duplicates | Yes | Values only | No |
| Reached by | Position | Key | Membership only |
| Membership search cost | Grows with length | Flat, on average | Flat, on average |
| Contents must be hashable | No | Keys only | Yes |
| Carries a payload | The item | Key and value | No payload |
| Literal for an empty one | Square brackets | Braces | Written set() |
Two rows need their scope stated. Insertion order in dictionaries is a language guarantee from Python 3.7 onward, not an accident of one interpreter. Flat membership cost is an average, and it depends on the contents hashing well.
Does the order carry meaning?
A list stores order and lets you use it. Positions are stable, slices take ranges, and sorting rearranges the sequence itself.
log = ["opened", "edited", "saved"]
print(log[0], log[-1])
print(log[1:]) # Program output:
# opened saved
# ['edited', 'saved'] A dictionary remembers the order keys were added, and that order is for iterating, not for reaching things. There is no scores[0] unless 0 happens to be a key you created.
A set stores no order at all. It may print in a repeatable order inside one run, and nothing in the language promises that order between runs or between versions. Sort a set before comparing it to anything a person will read.
attended = {"Cleo", "Ada", "Bo"}
print(sorted(attended)) # Program output:
# ['Ada', 'Bo', 'Cleo'] The question to ask is whether the sequence is part of the data. Events in time, steps in a recipe, and rows from a file all carry order. A roster of who has permission does not.
Do duplicates belong in the data?
This is the criterion people get wrong most often, because "no duplicates" is often a rule about the data rather than something the code enforces.
votes = ["yes", "no", "yes"]
tally = {"yes": 2, "no": 1}
options = {"yes", "no", "yes"}
print(len(votes), len(options))
print(tally["yes"]) # Program output:
# 3 2
# 2 A list keeps every repeat, which is correct when the repeat is a fact. Three votes are three votes, and collapsing them loses the count.
A set discards repeats on the way in, silently and immediately. That is exactly right for the set of options available and exactly wrong for the votes cast.
A dictionary sits between the two. Keys are unique, values are not, and the counting pattern turns a stream with repeats into a mapping from each distinct value to how often it appeared.
How will you look things up?
Lookup cost is where a wrong choice stops being a style question. A list has to walk its items to answer "is this in here?", so the work grows with the length. A set and a dictionary hash the value and go straight to where it would live, so the work does not grow with the size, on average.
import timeit
setup = "big_list = list(range(200_000)); big_set = set(big_list)"
list_time = timeit.timeit("199_999 in big_list", setup=setup, number=100)
set_time = timeit.timeit("199_999 in big_set", setup=setup, number=100)
print(f"list: {list_time:.4f}s")
print(f"set: {set_time:.6f}s") Run this on your machine. Exact timings and the ratio vary between runs and between machines, which is why no output is printed above. The shape is what carries over: a worst-case list search on two hundred thousand items ordinarily takes far longer than the set lookup, and the gap widens as the collection grows.
Two scopes belong on that claim. The flat cost is an average, and contents that hash poorly can degrade it. And the gap is invisible on small collections, so a list of twenty items is not worth converting.
The other half of lookup is what comes back. A set tells you only whether the value is there, while a dictionary tells you what is attached to it. When the answer to "is this in here?" is immediately followed by "and what is its price?", the dictionary was the right container.
What Is Allowed Inside
A list accepts anything, including other lists, because it never needs to hash what it holds. Sets and dictionary keys need hashable contents, which in practice means strings, numbers, booleans, and tuples of those.
pairs = [("London", "SW1"), ("Leeds", "LS1")]
lookup = {pair: index for index, pair in enumerate(pairs)}
unique = set(pairs)
print(lookup[("Leeds", "LS1")])
print(len(unique)) # Program output:
# 1
# 2 Reach for a list of lists and it works. Reach for a set of lists and you get TypeError: unhashable type: 'list', because a value that can change cannot be filed by its contents. Converting the inner lists to tuples is the usual fix.
This criterion decides more cases than it looks like it should. Compound keys, coordinates, and record identifiers all want tuples precisely so they can go inside a set or act as a dictionary key.
The Same Job in All Three
Here is one job done three ways so the difference shows in code rather than in adjectives. A door log records who entered, in order and with repeats, and three questions get asked of it.
entries = ["Ada", "Bo", "Ada", "Cleo", "Bo", "Ada"]
print(entries[0], entries[-1])
print(len(entries)) # Program output:
# Ada Ada
# 6 The list is the log itself. It answers "who was first?", "who was last?", and "how many entries were there?", and none of those questions survives de-duplication.
entries = ["Ada", "Bo", "Ada", "Cleo", "Bo", "Ada"]
visits = {}
for name in entries:
visits[name] = visits.get(name, 0) + 1
print(visits)
print(visits["Ada"]) # Program output:
# {'Ada': 3, 'Bo': 2, 'Cleo': 1}
# 3 The dictionary answers "how many times did each person come in?". It keeps one entry per person and attaches the count, which is the payload a set cannot carry.
entries = ["Ada", "Bo", "Ada", "Cleo", "Bo", "Ada"]
expected = {"Ada", "Bo", "Dev"}
present = set(entries)
print(sorted(present))
print(sorted(expected - present))
print(sorted(present & expected)) # Program output:
# ['Ada', 'Bo', 'Cleo']
# ['Dev']
# ['Ada', 'Bo'] The set answers "who showed up at all?", and then answers two questions neither of the others can express in one line. Who was expected and never arrived is a difference, and who was both expected and present is an intersection.
Same data, three containers, three genuinely different capabilities. Nothing here is a performance argument, which is the point: the containers differ in what they can say, not only in how fast they say it.
What Choosing Wrong Actually Costs
A List Doing a Set's Job
The most common wrong choice is a list used as a seen-already register. It works perfectly on test data and degrades quietly, because every membership test walks everything gathered so far.
incoming = ["a", "b", "a", "c", "b"]
seen = []
for item in incoming:
if item not in seen:
seen.append(item)
print(seen) # Program output:
# ['a', 'b', 'c'] Nothing is wrong with that output. The cost is that, in the worst case, processing a million distinct items does roughly half a trillion comparisons. The fix is one line: keep a set for the test and the list for the order. Undoing this mistake is cheap, which is why it survives so long in code that nobody profiled.
A Dictionary Doing a List's Job
A dictionary keyed by 0, 1, and 2 is a list wearing a disguise. It costs more memory, it cannot be sliced, sorting it means rebuilding it, and the keys can drift out of sequence when an entry is deleted.
This one is more expensive to undo than the first, because every read site in the code was written against key access and the keys may have been serialised into stored data. Catch it at design time by asking whether any key means something other than "the position of this item".
A Set Where Order Was Load-Bearing
The most expensive mistake of the three is de-duplicating through a set data that had order or repeats worth keeping. Both are discarded at the moment of conversion, and neither can be recovered from the set afterwards.
The damage shows up late, usually as an output whose sequence changes for no visible reason, and the repair is to go back to whatever produced the data. Convert to a set at the point where a question genuinely is about membership, not as a general-purpose tidy-up.
Which one should you use?
Every line below traces to a criterion above, so you can disagree with a recommendation only by disagreeing with something already shown.
Choose a list when:
- The sequence is part of the data, such as events, steps, or rows read from a file.
- Repeats are facts rather than noise, such as votes cast or entries in a log.
- You reach items by position, or take ranges of them with slices.
- The contents include things that cannot be hashed, such as other lists.
Choose a dictionary when:
- Each item has a natural name or identifier, and you reach it by that name.
- Something is attached to each unique thing, such as a count, a price, or a record.
- You are counting or grouping, which are the same shape with different payloads.
- The data arrived as JSON, where objects map onto dictionaries directly.
Choose a set when:
- The only question is whether a value is present.
- Duplicates are meaningless and should disappear on the way in.
- You want to compare two collections with union, intersection, or difference.
- Nothing needs to be attached to each member and no order needs keeping.
When two of those lists describe your case, use two containers. A list beside a set is the standard answer to "ordered, and de-duplicated", and it is a design rather than a compromise.
Conclusion
Ask what question the collection has to answer, not what the data happens to look like. Order and repeats point to a list, a name with something attached points to a dictionary, and a plain yes or no about membership points to a set.
Frequently Asked Questions
Can you use more than one of them together?
Yes, and it is a normal design rather than a workaround. Keeping a list for order alongside a set for membership is the standard way to de-duplicate a stream while preserving arrival order, at the cost of holding the values twice.
Is a set just a dictionary without values?
That is a fair mental model, not an implementation claim. Sets find members and dictionaries find keys by hashing; set members and dictionary keys must be hashable, and neither type keeps duplicate members or keys. They remain separate types with separate methods and implementations.
Are sets ordered in modern Python?
No. Dictionaries gained a guaranteed insertion order in Python 3.7 and sets did not. A set may print in a consistent order within one run, and nothing in the language promises that order across runs or across versions, so sort before comparing or displaying.
How do you convert between the three?
Pass one to another's constructor: set(items) drops duplicates, sorted(some_set) gives an ordered list, and dict(pairs) builds a mapping from two-item tuples. Converting a dictionary gives you its keys, so list(d) and set(d) both discard the values.
Which one uses the least memory?
On CPython, a list usually has the lowest container overhead for the same number of entries, because sets and dictionaries spend extra space on hash-table lookup. Note that sys.getsizeof compares shallow container allocations only, so count the objects they refer to separately when total memory matters.
Where do tuples fit in?
A tuple is an immutable sequence, and it is hashable when all of its contents are hashable. That is why tuples work as dictionary keys and set members, and why a fixed record such as a coordinate pair is usually a tuple rather than a list.
Related
- Python Lists - indexing, slicing, mutation, and copying in full.
- Python Dictionaries - safe key access, views, counting, grouping, and the JSON boundary.
- List and Dict Comprehensions in Python - build any of these from another iterable in one expression.
- Python - the language hub and what to learn next.
Sources
-
[1]
Data Structures(docs.python.org)
-
[2]
Set Types: set, frozenset(docs.python.org)
-
[3]
Glossary: hashable(docs.python.org)
-
[4]
Design and History FAQ(docs.python.org)
Read Next
A comprehension builds a list or a dictionary from an existing iterable in one expression. The syntax for both forms, filtering, conditional values, nesting, and the cases where a plain loop is the better answer.
A Python list keeps items in the order you put them and lets you change that order later. How to create one, index and slice it, add and remove items, copy it safely, and recognise the jobs a list is wrong for.
A beginner's guide to Python: data structures, scripting, and tutorials that start from plain-English syntax and build into real projects.