Skip to content

Choosing the Right Data Structure

Python’s four core collections, list, tuple, dict, and set, can all hold multiple values, which makes it hard to know which one to reach for. The trick is to start from the question you are asking, not from the type.

  • Need an ordered collection that changes? Use a list.
  • Need a fixed record where position has meaning? Use a tuple.
  • Need to look up values by a label? Use a dict.
  • Need unique items or fast “is it in here?” checks? Use a set.
StructureSyntaxMutableOrderedDuplicatesFind an item byCan be a dict key?
list[1, 2, 3]YesYesAllowedPosition (or scanning)No
tuple(1, 2, 3)NoYesAllowedPositionYes, if every item is hashable
dict{'a': 1}YesYes (insertion order)Keys are unique, values may repeatKeyNo
set{1, 2, 3}YesNoNot allowedMembership onlyNo
frozensetfrozenset({1, 2})NoNoNot allowedMembership onlyYes

I need to…Use
Keep items in order and add, remove, or sort themlist
Store a fixed record where each position has a meaningtuple
Return several values from a functiontuple
Look up a value by a name or iddict
Store labeled fields that may changedict (or a dataclass)
Keep only unique itemsset
Check “is X in here?” many timesset (or dict)
Compare two groups (what is shared or missing)set
Use a compound value as a dict key or set membertuple
Count how often things appearCounter
Group items by categorydefaultdict(list)
Process items first-in, first-outdeque
Remove duplicates but keep the orderlist(dict.fromkeys(items))
Store a fixed record with named fieldsnamedtuple or dataclass
  1. Do you need to look things up by a label or key? Use a dict.

  2. Do you only need unique items, or quick membership checks, with order irrelevant? Use a set.

  3. Is it a fixed record where position has meaning, or something that must be hashable? Use a tuple.

  4. Otherwise, is it a collection of similar items that can grow, shrink, or be reordered? Use a list.


Use a list when you have a collection of similar items that can grow, shrink, or be reordered.

SituationExample
Shopping cartcart = ['milk', 'eggs'] then cart.append('bread')
Todo itemstasks.remove('write report')
Lines from a filelines = f.readlines()
Search resultsresults = [r for r in items if r.matches]
Usernames onlineonline.append(user) and online.remove(user)
Scores to rankscores.sort(reverse=True)
Undo historyhistory.append(action) and history.pop()
list_examples.py
# Build a collection in a loop
squares = [n ** 2 for n in range(5)]
print(squares) # [0, 1, 4, 9, 16]
# Sort or reorder in place
scores = [72, 95, 88, 61]
scores.sort(reverse=True)
print(scores) # [95, 88, 72, 61]
# Use as a stack (last in, first out)
history = ['open', 'edit']
history.append('save')
print(history.pop()) # 'save'
# Filter and clean data
raw = [' Ada ', '', 'Alan', ' ', 'Grace']
print([s.strip() for s in raw if s.strip()]) # ['Ada', 'Alan', 'Grace']
# A grid whose cells change
board = [[' ' for _ in range(3)] for _ in range(3)]
board[1][1] = 'X'

For a large queue, use collections.deque, since removing from the front of a list shifts every other item. See Beyond the Core Four.


Use a tuple when the data is a fixed record and the position of each item has meaning.

SituationExample
Coordinatespoint = (3, 7)
RGB colorred = (255, 0, 0)
Database rowuser = (42, 'Ada', 'ada@example.com')
Host and portserver = ('localhost', 8080)
Date partsbirthday = (1990, 12, 25)
Fixed choicesSIZES = ('small', 'medium', 'large')
Min and max pairbounds = (0, 100)
tuple_examples.py
# Return multiple values, then unpack them
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 9, 1, 7])
print(low, high) # 1 9
# Use as dictionary keys (lists cannot be keys)
distances = {('London', 'Paris'): 344, ('Paris', 'Berlin'): 878}
print(distances[('London', 'Paris')]) # 344
# Use as set members
visited = {(2, 5), (3, 5)}
print((2, 5) in visited) # True
# Swap and unpack
a, b = 1, 2
a, b = b, a # a=2, b=1
name, age, *others = ('Ada', 36, 'London', 'UK') # others is ['London', 'UK']
# Protect constants from accidental changes
WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri')
WEEKDAYS[0] = 'Sun' # TypeError: 'tuple' object does not support item assignment

zip(), enumerate(), and dict.items() all produce tuples, which you can unpack right in a for loop:

tuples_in_loops.py
names = ['Ada', 'Alan', 'Grace']
scores = [95, 88, 92]
for name, score in zip(names, scores):
print(name, score)
for i, name in enumerate(names):
print(i, name)
for key, value in {'a': 1, 'b': 2}.items():
print(key, value)

Use a dictionary when you need to find a value by a key, or when a record has named fields.

SituationExample
A record with named fieldsuser = {'name': 'Ada', 'age': 36}
Settings and configurationconfig = {'theme': 'dark', 'lang': 'en'}
Lookup tablesstatus = {200: 'OK', 404: 'Not Found'}
Countingcounts['apple'] += 1
Grouping by categorygroups['fruit'].append('apple')
Caching resultscache[n] = result
JSON-style data from an APIresponse = {'id': 1, 'tags': ['a', 'b']}
dict_examples.py
# A record with named fields
user = {'id': 42, 'name': 'Ada', 'email': 'ada@example.com'}
user['email'] = 'new@example.com' # Fields can change
print(user.get('phone', 'not provided')) # 'not provided', safe lookup
# A lookup table
status_names = {200: 'OK', 404: 'Not Found', 500: 'Server Error'}
code = 404
print(status_names.get(code, 'Unknown')) # 'Not Found'
# Caching previously computed results
cache = {}
def square(n):
if n not in cache:
cache[n] = n * n
return cache[n]
# Counting and grouping
from collections import Counter, defaultdict
print(Counter('banana').most_common(1)) # [('a', 3)]
groups = defaultdict(list)
for word in ['apple', 'kiwi', 'pear', 'banana']:
groups[len(word)].append(word)
print(dict(groups)) # {5: ['apple'], 4: ['kiwi', 'pear'], 6: ['banana']}

Use a set when you care about which items exist, not about their order or how many times they appear.

SituationExample
Remove duplicatesunique = set(items)
Fast membership checksif user_id in allowed_ids:
Unique tags or categoriestags = {'python', 'tips'}
Compare two groupsrequired - granted
Track visited itemsvisited.add(page)
Find shared itemslist_a & list_b
set_examples.py
# Remove duplicates (order is not preserved)
raw_ids = [101, 102, 102, 105, 108, 108, 110]
print(set(raw_ids)) # {101, 102, 105, 108, 110}
# Fast membership checks
allowed_users = {101, 105, 108}
print(105 in allowed_users) # True
# Compare two groups
required = {'read', 'write'}
granted = {'read', 'comment'}
print(required - granted) # {'write'}, what is missing
print(required & granted) # {'read'}, what is shared
print(required | granted) # {'read', 'write', 'comment'}, everything

The best structure depends on what you are modeling. Here is one scenario, a small user directory, represented with each.

user = (42, 'Ada', 'ada@example.com')
print(user[1]) # 'Ada', you must remember which position is which

A single, fixed record. Compact and read-only, but the meaning of each position lives in your head.


A record like (42, 'Ada', 'ada@example.com') is a good tuple as long as it never changes. If a field such as the email needs to change, a list is rarely the answer, because a list is meant for collections of similar items. Choose one of these instead:

SituationChooseExample
Fields never changetuple or namedtupleuser = (42, 'Ada', 'ada@example.com')
Fields change in placedict or a dataclassuser['email'] = 'new@example.com'
Keep the original, create an updated copynamedtuple with _replaceuser = user._replace(email='new@example.com')
One-off change to a plain tupleRebuild ituser = (user[0], user[1], 'new@example.com')
Many recordsA list of any of the aboveusers = [user1, user2]

Use a list only when you really mean “a collection that grows or shrinks”, such as a list of many users.


Python’s standard library offers specialized structures for common jobs.

NeedUseModule
Fixed record with named fieldsnamedtuplecollections
Record with named fields that can change@dataclassdataclasses
An immutable, hashable setfrozensetbuilt-in
Fast queue, adding and removing at both endsdequecollections
Counting itemsCountercollections
Grouping or defaulting missing keysdefaultdictcollections
Textstr (an immutable sequence of characters)built-in
Large numeric arraysNumPy arraysthird-party
specialized_structures.py
from collections import namedtuple, deque, Counter
from dataclasses import dataclass
# namedtuple: immutable record with names
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 7)
print(p.x, p.y) # 3 7
print(p._replace(x=10)) # Point(x=10, y=7)
# dataclass: a record that can change
@dataclass
class User:
id: int
name: str
email: str
user = User(42, 'Ada', 'ada@example.com')
user.email = 'new@example.com'
print(user) # User(id=42, name='Ada', email='new@example.com')
# deque: a fast queue
queue = deque(['a', 'b'])
queue.append('c')
print(queue.popleft()) # 'a'
# Counter: counting
print(Counter(['a', 'b', 'a'])) # Counter({'a': 2, 'b': 1})
# frozenset: usable as a dict key
groups = {frozenset({1, 2}): 'pair'}

Real programs nest structures inside one another. The outer structure answers “how do I find things?”, and the inner one answers “what does each thing look like?”

CombinationTypical use
List of tuplesA collection of fixed records, such as database rows
List of dictsA collection of labeled records, such as JSON from an API
Dict of listsItems grouped by category
Dict of setsUnique items per key, such as tags per post
Set of tuplesUnique coordinates or pairs
Dict keyed by tuplesValues indexed by compound keys, such as grid cells
combined_structures.py
# List of tuples: fixed records you can sort and loop over
employees = [
('Ada', 'Engineering', 120000),
('Alan', 'Research', 110000),
('Grace', 'Engineering', 130000),
]
employees.sort(key=lambda e: e[2], reverse=True)
for name, dept, salary in employees:
print(f'{name} ({dept}): {salary}')
# Grace (Engineering): 130000
# Ada (Engineering): 120000
# Alan (Research): 110000
# List of dicts: labeled records (like JSON)
users = [{'name': 'Ada', 'age': 36}, {'name': 'Alan', 'age': 41}]
print([u['name'] for u in users]) # ['Ada', 'Alan']
print(max(users, key=lambda u: u['age'])) # {'name': 'Alan', 'age': 41}
# Dict of lists: grouped items
by_dept = {'Engineering': ['Ada', 'Grace'], 'Research': ['Alan']}
by_dept['Research'].append('Edsger')
# Dict of sets: unique items per key
tags = {'post1': {'python', 'tips'}, 'post2': {'python'}}
print(tags['post1'] & tags['post2']) # {'python'}
# Set of tuples: unique coordinates
visited = {(0, 0), (0, 1)}
visited.add((0, 1)) # Already there, no change
print(len(visited)) # 2

When fields need names, upgrade the tuples in a list of tuples to namedtuple, and the rest of the code stays the same.

If your data will be saved or sent as JSON, remember how structures translate:

PythonJSON
dictobject
listarray
tuplearray (it comes back as a list)
setNot supported
json_conversion.py
import json
print(json.dumps({'a': (1, 2)})) # '{"a": [1, 2]}', the tuple became an array
json.dumps({'a': {1, 2}}) # TypeError: Object of type set is not JSON serializable
json.dumps({'a': sorted({1, 2})}) # Fix: convert the set to a list first

FromToHow
listtupletuple(items)
tuplelistlist(t)
listsetset(items)
setsorted listsorted(s)
listlist without duplicates, in orderlist(dict.fromkeys(items))
list of pairsdictdict(pairs)
two listsdictdict(zip(keys, values))
dictlist of keys, values, or pairslist(d), list(d.values()), list(d.items())
converting_structures.py
numbers = [3, 1, 2, 3, 1]
print(tuple(numbers)) # (3, 1, 2, 3, 1)
print(set(numbers)) # {1, 2, 3}
print(sorted(set(numbers))) # [1, 2, 3], a set cannot be sorted in place
print(list(dict.fromkeys(numbers))) # [3, 1, 2], duplicates removed, order kept
pairs = [('a', 1), ('b', 2)]
d = dict(pairs) # {'a': 1, 'b': 2}
print(list(d)) # ['a', 'b']
print(list(d.values())) # [1, 2]
print(list(d.items())) # [('a', 1), ('b', 2)]
# A list cannot be a key, but its tuple version can
cache = {}
cache[tuple(['a', 'b'])] = 'result'

Only hashable values can be dictionary keys or set members. In practice, hashable means immutable.

TypeHashableCan be a key or set member
int, float, str, bool, NoneYesYes
tupleOnly if every item is hashableSometimes
frozensetYesYes
list, dict, setNoNo
hashability_pitfalls.py
hash((1, 2)) # Works
hash((1, [2, 3])) # TypeError: unhashable type: 'list'
record = ('Ada', ['python', 'rust'])
record[1].append('go')
print(record) # ('Ada', ['python', 'rust', 'go']), the inner list changed

O(1) means the cost stays constant however large the collection is. O(n) means it grows with the number of items, because Python may scan all of them. These are average-case figures.

Operationlisttupledictset
Get by indexO(1)O(1)n/an/a
Get by keyn/an/aO(1)n/a
Check membership (x in c)O(n)O(n)O(1) for keysO(1)
Add to the endO(1)n/aO(1)O(1)
Insert at the front or middleO(n)n/an/an/a
Remove an itemO(n)n/aO(1)O(1)
Loop over all itemsO(n)O(n)O(n)O(n)
SortO(n log n)sorted() gives a listsorted() gives a listsorted() gives a list

MistakeProblemFix
Using a list as a dictionary keyTypeError: unhashable type: 'list'Convert with tuple(items)
Using x in big_list repeatedlyEach check scans the whole listConvert to a set once
{} for an empty setThat is an empty dictionaryset()
Relying on the order of a setOrder is arbitraryUse a list, or sorted(s)
Using a tuple when you need to add itemsTuples have no appendUse a list, or build a new tuple
x = (5) for a one-item tupleThat is just the number 5x = (5,)
Parallel lists (names[i], ages[i])Easy to get out of syncA list of dicts or tuples, or dict(zip(...))
A list or tuple of many positional fieldsrow[7] is unreadabledict, namedtuple, or dataclass
Using a list for a fixed record, like [x, y]Works, but hides the intentUse a tuple
A dict where a simple list would doKeys that are just 0, 1, 2... add nothingUse a list
set(items) when order mattersOrder is lostlist(dict.fromkeys(items))
Putting a set in JSONTypeErrorConvert with sorted(s) or list(s)
  1. Choose by meaning: what question will you ask of the data?
  2. Default to a list for collections, and reach for another structure when you notice a specific need.
  3. Use a tuple for fixed records and multiple return values, and a dict when fields have names or may change.
  4. Use a set for uniqueness, fast membership checks, and comparing groups.
  5. Use tuples (not lists) for compound dictionary keys and set members.
  6. Move to namedtuple or dataclass once a record has more than two or three fields.
  7. Use Counter, defaultdict, and deque instead of rebuilding them by hand.
  8. Convert freely between structures, but remember that sets lose order and JSON has no sets.