2025, Oct 07 03:00

How to Use csv.DictReader to Build Player Score Dictionaries and Avoid KeyError and Unpacking Errors

Learn how to parse CSV game scores with Python csv.DictReader, build clean per-player dictionaries, and avoid KeyError and unpacking errors. Robust patterns

Turning a CSV of game scores into a clean Python dictionary sounds simple until you hit KeyError surprises and unpacking issues. The core confusion usually comes from mixing up how csv.DictReader works and what a valid nested structure in Python looks like. Let’s walk through the pitfalls and land on robust patterns that handle any number of game columns with changing names.

The setup and the stumbling blocks

Suppose you have a CSV with a header row and a variable set of game columns:

name,Game_1, Game_2, Game_3
James,3,7,4
Charles,2,3,8
Bob,6,2,4

You read it using csv.DictReader and then try to build a dictionary keyed by player name. An initial attempt might look like this, but it goes wrong:

import csv
# Intended to gather results
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    # Wrong: indexing row by integers with DictReader
    for rec in dict_reader:
        scorebook[rec[0]] = rec[1]  # KeyError: 0 / KeyError: 1

Or an attempt to unpack items directly from the reader:

import csv
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    for left, right in dict_reader:  # ValueError: too many values to unpack (expected 2)
        print(left, right)

What’s actually happening

csv.DictReader yields each row as a dict, not as a list. That’s the key. Row values are accessed by column names, not by numeric indexes. So rec['name'] is valid, while rec[0] is not, and throws KeyError: 0 because 0 is not a key in the row dict.

There’s also a structural issue with the desired result shape. The expression

'James': {'Game_1': '3'}, {'Game_2': '7'}, {'Game_3': '4'}

is not valid as a single value in a dict. In Python, you can store one of the following: a list of dicts, a tuple of dicts, or a single dict mapping game names to scores. So the valid shapes are:

'James': [ {'Game_1': '3'}, {'Game_2': '7'}, {'Game_3': '4'} ]
'James': ( {'Game_1': '3'}, {'Game_2': '7'}, {'Game_3': '4'} )
'James': {'Game_1': '3', 'Game_2': '7', 'Game_3': '4'}

The fix: let DictReader work for you

Because each row is already a dict, you should use the header name to pull out the player, and then either build a per-player dict of games, or a list/tuple of small dicts. Here’s the one-dict-per-player approach, which is usually the most practical:

import csv
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    game_fields = dict_reader.fieldnames[1:]  # skip the 'name' column
    for rec in dict_reader:
        person = rec['name']
        scorebook[person] = {g: rec[g] for g in game_fields}
# scorebook:
# {
#   'James':  {'Game_1': '3', 'Game_2': '7', 'Game_3': '4'},
#   'Charles':{'Game_1': '2', 'Game_2': '3', 'Game_3': '8'},
#   'Bob':    {'Game_1': '6', 'Game_2': '2', 'Game_3': '4'}
# }

If you prefer a list of per-game dicts instead, you can do this:

import csv
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    game_fields = dict_reader.fieldnames[1:]
    for rec in dict_reader:
        person = rec['name']
        scorebook[person] = [{g: rec[g]} for g in game_fields]
# scorebook:
# {
#   'James':  [ {'Game_1': '3'}, {'Game_2': '7'}, {'Game_3': '4'} ],
#   'Charles':[ {'Game_1': '2'}, {'Game_2': '3'}, {'Game_3': '8'} ],
#   'Bob':    [ {'Game_1': '6'}, {'Game_2': '2'}, {'Game_3': '4'} ]
# }

There’s also a compact variant that avoids enumerating the game columns entirely by removing the name key from each row and keeping what’s left. This naturally adapts to any number of game columns and their changing names:

import csv
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    for rec in dict_reader:
        person = rec.pop('name')  # extract and remove the key 'name'
        scorebook[person] = rec   # remaining keys are the games
# Same structure as the one-dict-per-player variant

Why those errors happened

KeyError: 0 and KeyError: 1 appear because DictReader returns dicts keyed by column names. Index-based access like rec[0] or rec[1] doesn’t exist in this mapping. To access a value, use its header name, for example rec['name'] or rec['Game_1'].

ValueError: too many values to unpack (expected 2) occurs when you try for left, right in dict_reader. The reader yields a dict per iteration, not a pair. Python then attempts to unpack a single dict into two variables, which fails. Iterate rows one at a time and access keys explicitly.

Why this matters

Game columns and their count can change. Building your transformation around header names rather than numeric positions makes the code resilient to these changes. It also makes the intent clear: you’re mapping a player’s name to their scores, not juggling indexes that can silently drift when the CSV schema evolves.

When debugging transformations like this, it helps to print intermediate values to see what you actually have at each step. Printing the row object and its type immediately reveals whether you’re dealing with a list or a dict and prevents chasing the wrong mental model.

Takeaways

Decide on a valid target structure before coding, then let csv.DictReader drive the mapping. Access row values by header names such as rec['name'], not by numeric indexes. If you want a single dict per player, use a dict comprehension with the game columns or remove the name key and store the rest. If you need a list or tuple of per-game dicts, build it directly from the field names. Keeping to these patterns removes KeyError surprises and makes the code resilient to changing CSV headers.

The article is based on a question from StackOverflow by user31290254 and an answer by furas.