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.
At a Glance
Section titled “At a Glance”| Structure | Syntax | Mutable | Ordered | Duplicates | Find an item by | Can be a dict key? |
|---|---|---|---|---|---|---|
list | [1, 2, 3] | Yes | Yes | Allowed | Position (or scanning) | No |
tuple | (1, 2, 3) | No | Yes | Allowed | Position | Yes, if every item is hashable |
dict | {'a': 1} | Yes | Yes (insertion order) | Keys are unique, values may repeat | Key | No |
set | {1, 2, 3} | Yes | No | Not allowed | Membership only | No |
frozenset | frozenset({1, 2}) | No | No | Not allowed | Membership only | Yes |
Start with Your Question
Section titled “Start with Your Question”| I need to… | Use |
|---|---|
| Keep items in order and add, remove, or sort them | list |
| Store a fixed record where each position has a meaning | tuple |
| Return several values from a function | tuple |
| Look up a value by a name or id | dict |
| Store labeled fields that may change | dict (or a dataclass) |
| Keep only unique items | set |
| Check “is X in here?” many times | set (or dict) |
| Compare two groups (what is shared or missing) | set |
| Use a compound value as a dict key or set member | tuple |
| Count how often things appear | Counter |
| Group items by category | defaultdict(list) |
| Process items first-in, first-out | deque |
| Remove duplicates but keep the order | list(dict.fromkeys(items)) |
| Store a fixed record with named fields | namedtuple or dataclass |
A Decision Flow
Section titled “A Decision Flow”-
Do you need to look things up by a label or key? Use a dict.
-
Do you only need unique items, or quick membership checks, with order irrelevant? Use a set.
-
Is it a fixed record where position has meaning, or something that must be hashable? Use a tuple.
-
Otherwise, is it a collection of similar items that can grow, shrink, or be reordered? Use a list.
Lists: Changing Collections
Section titled “Lists: Changing Collections”Use a list when you have a collection of similar items that can grow, shrink, or be reordered.
| Situation | Example |
|---|---|
| Shopping cart | cart = ['milk', 'eggs'] then cart.append('bread') |
| Todo items | tasks.remove('write report') |
| Lines from a file | lines = f.readlines() |
| Search results | results = [r for r in items if r.matches] |
| Usernames online | online.append(user) and online.remove(user) |
| Scores to rank | scores.sort(reverse=True) |
| Undo history | history.append(action) and history.pop() |
# Build a collection in a loopsquares = [n ** 2 for n in range(5)]print(squares) # [0, 1, 4, 9, 16]
# Sort or reorder in placescores = [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 dataraw = [' Ada ', '', 'Alan', ' ', 'Grace']print([s.strip() for s in raw if s.strip()]) # ['Ada', 'Alan', 'Grace']
# A grid whose cells changeboard = [[' ' 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.
Tuples: Fixed Records
Section titled “Tuples: Fixed Records”Use a tuple when the data is a fixed record and the position of each item has meaning.
| Situation | Example |
|---|---|
| Coordinates | point = (3, 7) |
| RGB color | red = (255, 0, 0) |
| Database row | user = (42, 'Ada', 'ada@example.com') |
| Host and port | server = ('localhost', 8080) |
| Date parts | birthday = (1990, 12, 25) |
| Fixed choices | SIZES = ('small', 'medium', 'large') |
| Min and max pair | bounds = (0, 100) |
# Return multiple values, then unpack themdef 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 membersvisited = {(2, 5), (3, 5)}print((2, 5) in visited) # True
# Swap and unpacka, b = 1, 2a, b = b, a # a=2, b=1name, age, *others = ('Ada', 36, 'London', 'UK') # others is ['London', 'UK']
# Protect constants from accidental changesWEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri')WEEKDAYS[0] = 'Sun' # TypeError: 'tuple' object does not support item assignmentzip(), enumerate(), and dict.items() all produce tuples, which you can unpack right in a for loop:
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)Dictionaries: Lookup by Label
Section titled “Dictionaries: Lookup by Label”Use a dictionary when you need to find a value by a key, or when a record has named fields.
| Situation | Example |
|---|---|
| A record with named fields | user = {'name': 'Ada', 'age': 36} |
| Settings and configuration | config = {'theme': 'dark', 'lang': 'en'} |
| Lookup tables | status = {200: 'OK', 404: 'Not Found'} |
| Counting | counts['apple'] += 1 |
| Grouping by category | groups['fruit'].append('apple') |
| Caching results | cache[n] = result |
| JSON-style data from an API | response = {'id': 1, 'tags': ['a', 'b']} |
# A record with named fieldsuser = {'id': 42, 'name': 'Ada', 'email': 'ada@example.com'}user['email'] = 'new@example.com' # Fields can changeprint(user.get('phone', 'not provided')) # 'not provided', safe lookup
# A lookup tablestatus_names = {200: 'OK', 404: 'Not Found', 500: 'Server Error'}code = 404print(status_names.get(code, 'Unknown')) # 'Not Found'
# Caching previously computed resultscache = {}
def square(n): if n not in cache: cache[n] = n * n return cache[n]
# Counting and groupingfrom 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']}Sets: Uniqueness and Membership
Section titled “Sets: Uniqueness and Membership”Use a set when you care about which items exist, not about their order or how many times they appear.
| Situation | Example |
|---|---|
| Remove duplicates | unique = set(items) |
| Fast membership checks | if user_id in allowed_ids: |
| Unique tags or categories | tags = {'python', 'tips'} |
| Compare two groups | required - granted |
| Track visited items | visited.add(page) |
| Find shared items | list_a & list_b |
# 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 checksallowed_users = {101, 105, 108}print(105 in allowed_users) # True
# Compare two groupsrequired = {'read', 'write'}granted = {'read', 'comment'}print(required - granted) # {'write'}, what is missingprint(required & granted) # {'read'}, what is sharedprint(required | granted) # {'read', 'write', 'comment'}, everythingThe Same Data, Four Ways
Section titled “The Same Data, Four Ways”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 whichA single, fixed record. Compact and read-only, but the meaning of each position lives in your head.
users = ['Ada', 'Alan', 'Grace']
users.append('Edsger')print(users[0]) # 'Ada'An ordered collection of similar things that grows and shrinks.
user = {'id': 42, 'name': 'Ada', 'email': 'ada@example.com'}
print(user['name']) # 'Ada'user['email'] = 'new@example.com' # Fields can changeA record with labeled fields, or a collection you look up by key.
emails = {'ada@example.com', 'alan@example.com'}
print('ada@example.com' in emails) # True, fast checkemails.add('ada@example.com') # No effect, already thereUnique values with fast membership checks, and no order.
When a Record’s Fields Can Change
Section titled “When a Record’s Fields Can Change”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:
| Situation | Choose | Example |
|---|---|---|
| Fields never change | tuple or namedtuple | user = (42, 'Ada', 'ada@example.com') |
| Fields change in place | dict or a dataclass | user['email'] = 'new@example.com' |
| Keep the original, create an updated copy | namedtuple with _replace | user = user._replace(email='new@example.com') |
| One-off change to a plain tuple | Rebuild it | user = (user[0], user[1], 'new@example.com') |
| Many records | A list of any of the above | users = [user1, user2] |
Use a list only when you really mean “a collection that grows or shrinks”, such as a list of many users.
Beyond the Core Four
Section titled “Beyond the Core Four”Python’s standard library offers specialized structures for common jobs.
| Need | Use | Module |
|---|---|---|
| Fixed record with named fields | namedtuple | collections |
| Record with named fields that can change | @dataclass | dataclasses |
| An immutable, hashable set | frozenset | built-in |
| Fast queue, adding and removing at both ends | deque | collections |
| Counting items | Counter | collections |
| Grouping or defaulting missing keys | defaultdict | collections |
| Text | str (an immutable sequence of characters) | built-in |
| Large numeric arrays | NumPy arrays | third-party |
from collections import namedtuple, deque, Counterfrom dataclasses import dataclass
# namedtuple: immutable record with namesPoint = namedtuple('Point', ['x', 'y'])p = Point(3, 7)print(p.x, p.y) # 3 7print(p._replace(x=10)) # Point(x=10, y=7)
# dataclass: a record that can change@dataclassclass 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 queuequeue = deque(['a', 'b'])queue.append('c')print(queue.popleft()) # 'a'
# Counter: countingprint(Counter(['a', 'b', 'a'])) # Counter({'a': 2, 'b': 1})
# frozenset: usable as a dict keygroups = {frozenset({1, 2}): 'pair'}Combining Structures
Section titled “Combining Structures”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?”
| Combination | Typical use |
|---|---|
| List of tuples | A collection of fixed records, such as database rows |
| List of dicts | A collection of labeled records, such as JSON from an API |
| Dict of lists | Items grouped by category |
| Dict of sets | Unique items per key, such as tags per post |
| Set of tuples | Unique coordinates or pairs |
| Dict keyed by tuples | Values indexed by compound keys, such as grid cells |
# List of tuples: fixed records you can sort and loop overemployees = [ ('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 itemsby_dept = {'Engineering': ['Ada', 'Grace'], 'Research': ['Alan']}by_dept['Research'].append('Edsger')
# Dict of sets: unique items per keytags = {'post1': {'python', 'tips'}, 'post2': {'python'}}print(tags['post1'] & tags['post2']) # {'python'}
# Set of tuples: unique coordinatesvisited = {(0, 0), (0, 1)}visited.add((0, 1)) # Already there, no changeprint(len(visited)) # 2When fields need names, upgrade the tuples in a list of tuples to namedtuple, and the rest of the code stays the same.
JSON Compatibility
Section titled “JSON Compatibility”If your data will be saved or sent as JSON, remember how structures translate:
| Python | JSON |
|---|---|
dict | object |
list | array |
tuple | array (it comes back as a list) |
set | Not supported |
import json
print(json.dumps({'a': (1, 2)})) # '{"a": [1, 2]}', the tuple became an arrayjson.dumps({'a': {1, 2}}) # TypeError: Object of type set is not JSON serializablejson.dumps({'a': sorted({1, 2})}) # Fix: convert the set to a list firstConverting Between Structures
Section titled “Converting Between Structures”| From | To | How |
|---|---|---|
| list | tuple | tuple(items) |
| tuple | list | list(t) |
| list | set | set(items) |
| set | sorted list | sorted(s) |
| list | list without duplicates, in order | list(dict.fromkeys(items)) |
| list of pairs | dict | dict(pairs) |
| two lists | dict | dict(zip(keys, values)) |
| dict | list of keys, values, or pairs | list(d), list(d.values()), list(d.items()) |
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 placeprint(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 cancache = {}cache[tuple(['a', 'b'])] = 'result'Mutability and Hashability
Section titled “Mutability and Hashability”Only hashable values can be dictionary keys or set members. In practice, hashable means immutable.
| Type | Hashable | Can be a key or set member |
|---|---|---|
int, float, str, bool, None | Yes | Yes |
tuple | Only if every item is hashable | Sometimes |
frozenset | Yes | Yes |
list, dict, set | No | No |
hash((1, 2)) # Workshash((1, [2, 3])) # TypeError: unhashable type: 'list'
record = ('Ada', ['python', 'rust'])record[1].append('go')print(record) # ('Ada', ['python', 'rust', 'go']), the inner list changedPerformance Cheat Sheet
Section titled “Performance Cheat Sheet”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.
| Operation | list | tuple | dict | set |
|---|---|---|---|---|
| Get by index | O(1) | O(1) | n/a | n/a |
| Get by key | n/a | n/a | O(1) | n/a |
Check membership (x in c) | O(n) | O(n) | O(1) for keys | O(1) |
| Add to the end | O(1) | n/a | O(1) | O(1) |
| Insert at the front or middle | O(n) | n/a | n/a | n/a |
| Remove an item | O(n) | n/a | O(1) | O(1) |
| Loop over all items | O(n) | O(n) | O(n) | O(n) |
| Sort | O(n log n) | sorted() gives a list | sorted() gives a list | sorted() gives a list |
Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Fix |
|---|---|---|
| Using a list as a dictionary key | TypeError: unhashable type: 'list' | Convert with tuple(items) |
Using x in big_list repeatedly | Each check scans the whole list | Convert to a set once |
{} for an empty set | That is an empty dictionary | set() |
| Relying on the order of a set | Order is arbitrary | Use a list, or sorted(s) |
| Using a tuple when you need to add items | Tuples have no append | Use a list, or build a new tuple |
x = (5) for a one-item tuple | That is just the number 5 | x = (5,) |
Parallel lists (names[i], ages[i]) | Easy to get out of sync | A list of dicts or tuples, or dict(zip(...)) |
| A list or tuple of many positional fields | row[7] is unreadable | dict, namedtuple, or dataclass |
Using a list for a fixed record, like [x, y] | Works, but hides the intent | Use a tuple |
| A dict where a simple list would do | Keys that are just 0, 1, 2... add nothing | Use a list |
set(items) when order matters | Order is lost | list(dict.fromkeys(items)) |
| Putting a set in JSON | TypeError | Convert with sorted(s) or list(s) |
Best Practices Checklist
Section titled “Best Practices Checklist”- Choose by meaning: what question will you ask of the data?
- Default to a list for collections, and reach for another structure when you notice a specific need.
- Use a tuple for fixed records and multiple return values, and a dict when fields have names or may change.
- Use a set for uniqueness, fast membership checks, and comparing groups.
- Use tuples (not lists) for compound dictionary keys and set members.
- Move to
namedtupleordataclassonce a record has more than two or three fields. - Use
Counter,defaultdict, anddequeinstead of rebuilding them by hand. - Convert freely between structures, but remember that sets lose order and JSON has no sets.