2025, Oct 07 03:20

Как безопасно собрать словарь из CSV с csv.DictReader в Python без KeyError

Разбираем типичные ошибки с csv.DictReader в Python: KeyError и распаковка. Покажем, как собрать устойчивый словарь из CSV при любом числе столбцов данных.

Преобразовать CSV с очками игроков в аккуратный словарь Python кажется задачей попроще — пока не столкнёшься с неожиданными KeyError и проблемами распаковки. Главная путаница обычно возникает из‑за смешения представлений о том, как работает csv.DictReader, и о том, что в Python считается корректной вложенной структурой. Разберём типичные ловушки и придём к устойчивым приёмам, которые справляются с любым количеством столбцов игр и меняющимися названиями.

Исходные данные и подводные камни

Предположим, у вас есть CSV со строкой заголовков и переменным набором столбцов с играми:

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

Вы читаете его с помощью csv.DictReader и пытаетесь собрать словарь с ключом по имени игрока. Первая попытка может выглядеть так, но она приводит к ошибкам:

import csv
# Предполагается собрать результаты
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    # Неверно: попытка индексировать строку целыми числами при работе с DictReader
    for rec in dict_reader:
        scorebook[rec[0]] = rec[1]  # Приводит к KeyError: 0 / KeyError: 1

Или попытка сразу распаковать элементы из итератора:

import csv
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    for left, right in dict_reader:  # ValueError: слишком много значений для распаковки (ожидалось 2)
        print(left, right)

Что на самом деле происходит

csv.DictReader возвращает каждую строку как словарь, а не как список. Это принципиально. Значения в строке обращаются по именам столбцов, а не по числовым индексам. Поэтому rec['name'] — корректно, а rec[0] — нет и приводит к KeyError: 0, потому что 0 не является ключом в словаре строки.

Есть и структурная проблема с желаемой формой результата. Выражение

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

не является допустимым единым значением словаря. В Python вместо этого можно хранить одно из следующего: список словарей, кортеж словарей или один словарь, сопоставляющий названия игр и очки. Корректные формы выглядят так:

'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'}

Решение: позвольте DictReader сделать работу за вас

Поскольку каждая строка уже словарь, берите имя игрока по заголовку и дальше либо собирайте по игроку один словарь с играми, либо формируйте список/кортеж небольших словарей. Вот вариант «по одному словарю на игрока» — обычно он самый удобный:

import csv
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    game_fields = dict_reader.fieldnames[1:]  # пропускаем столбец 'name'
    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'}
# }

Если же вам нужен список словарей по каждой игре, можно сделать так:

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'} ]
# }

Есть и компактный вариант: вовсе не перечислять столбцы с играми, а убрать из строки ключ name и сохранить остальное. Такой подход естественно адаптируется к любому числу столбцов и их меняющимся именам:

import csv
scorebook = {}
with open("scores.txt") as fh:
    dict_reader = csv.DictReader(fh)
    for rec in dict_reader:
        person = rec.pop('name')  # извлекаем и удаляем ключ 'name'
        scorebook[person] = rec   # оставшиеся ключи — это игры
# Та же структура, что и в варианте с одним словарём на игрока

Почему возникли эти ошибки

KeyError: 0 и KeyError: 1 появляются потому, что DictReader возвращает словари с ключами — именами столбцов. Индексный доступ вроде rec[0] или rec[1] в таком отображении не существует. Чтобы получить значение, используйте заголовок столбца, например rec['name'] или rec['Game_1'].

ValueError: too many values to unpack (expected 2) возникает, когда вы пишете for left, right in dict_reader. Итератор возвращает словарь на каждой итерации, а не пару. Python пытается распаковать один словарь в две переменные и падает. Итерируйтесь по строкам по одной и обращайтесь к ключам явно.

Почему это важно

Столбцы с играми и их количество могут меняться. Если опираться на имена заголовков, а не на числовые позиции, код становится устойчивым к таким изменениям. К тому же намерение становится очевидным: вы сопоставляете имя игрока с его очками, а не жонглируете индексами, которые незаметно «уплывают», когда схема CSV эволюционирует.

При отладке подобных преобразований полезно выводить промежуточные значения, чтобы понимать, что у вас на каждом шаге. Печать самой строки и её типа сразу покажет, работаете ли вы со списком или со словарём, и убережёт от неверной ментальной модели.

Итоги

Сначала определитесь с корректной целевой структурой, а затем позвольте csv.DictReader направлять отображение. Обращайтесь к значениям по именам столбцов, например rec['name'], а не по числовым индексам. Если нужен один словарь на игрока, используйте словарное включение по игровым столбцам или удалите ключ name и сохраните остальное. Если требуется список или кортеж словарей по играм, собирайте его прямо из имён полей. Следование этим шаблонам избавляет от неожиданных KeyError и делает код устойчивым к изменениям заголовков CSV.

Статья основана на вопросе на StackOverflow от user31290254 и ответе furas.