Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 15 из 20 Средний 35 мин 120 XP

Выпадающие списки в Excel через openpyxl: DataValidation

DataValidation добавляет в ячейки выпадающий список: бариста выбирают напиток из меню, а не придумывают свои варианты — разбор ошибок ввода и списков с другого листа.

Редакция Питоники

Журнал продаж кофейни «Дукат» заполняют четыре баристы, и каждый пишет по-своему: «капучино», «Капучино», «капучино с двумя о». В конце недели ты делаешь сводку — и вместо трёх напитков получаешь девять. Лечится это проверкой данных: ячейка становится выпадающим списком, и ввести в неё что-то постороннее Excel просто не даст. Сейчас добавим такое правило кодом.

Отдельно честно: openpyxl правило записывает, но не исполняет. Файл — это архив с XML, и внутри него появляется описание валидации. Следит за ним Excel в тот момент, когда человек вводит значение. Это не баг, а разделение труда: библиотека готовит файл, программа — guard-ит ввод. Ты уже видел такой же раздел в уроке про формулы: записываем строку, считает Excel.

Если ты когда-нибудь настраивал это руками, то знаешь путь: «Данные → Проверка данных», тип «Список», источник. Ровно то же самое мы сейчас сделаем кодом, только файл с правилом соберётся сам — и таких файлов можно собрать сколько угодно за один запуск. Ручной путь остаётся полезным как карта: каждое поле диалога Excel имеет зеркало в параметре DataValidation.

DataValidation: выпадающий список из строк

Класс DataValidation живёт в модуле openpyxl.worksheet.datavalidation. Три действия: создать правило, привязать его к листу и назначить диапазон ячеек. В formula1 для списка варианты перечисляются через запятую — и вся эта строка оборачивается в кавычки, которые Excel считает частью значения.

первый выпадающий список
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
ws.title = "Журнал"
ws["A1"] = "Бариста"
ws["B1"] = "Напиток"

dv = DataValidation(type="list", formula1='"Американо,Капучино,Латте"', allow_blank=True)
dv.errorTitle = "Нет такого напитка"
dv.error = "Выбери напиток из списка меню"
ws.add_data_validation(dv)   # правило прикреплено к листу
dv.add("B2:B20")             # диапазон, где работает правило
wb.save("dukat_dv.xlsx")

wb2 = load_workbook("dukat_dv.xlsx")
d = wb2["Журнал"].data_validations.dataValidation[0]
print("type:", d.type)
print("formula1:", d.formula1)
print("диапазон:", str(d.sqref))
print("allow_blank:", d.allow_blank)
print("errorTitle:", d.errorTitle)
wb2.close()
os.remove("dukat_dv.xlsx")
Вывод
type: list
formula1: "Американо,Капучино,Латте"
диапазон: B2:B20
allow_blank: True
errorTitle: Нет такого напитка

Разбираем поля. type="list" — это выпадающий список; позже возьмём type="whole" для целых чисел. formula1 — сам список вариантов. allow_blank=True разрешает оставить ячейку пустой — иначе незаполненный журнал превратится в сплошные жалобы на ошибку. sqref — диапазон, к которому привязано правило; правило можно добавить и нескольким диапазонам: dv.add("B2:B20"), затем dv.add("D2:D20"). Читается всё это обратно через ws.data_validations.dataValidation — список всех правил листа.

Единственный параметр, который мы сознательно не трогаем, — showDropDown. В спецификации xlsx его смысл вывернут наизнанку: True прячет выпадающий список из ячейки, оставляя только проверку ввода. Поэтому в девяноста девяти случаях из ста этот параметр просто не указывают — список выпадает как задумано.

Почему сообщение об ошибке не показывается?

Ты написал dv.error и dv.errorTitle — а пользователь вводит ерунду, и ничего не происходит. Причина в переключателе showErrorMessage: по умолчанию он выключен, и Excel хранит текст ошибки, но молчит. Это же касается подсказки при выборе ячейки — она живёт в prompt и promptTitle и включается флагом showInputMessage.

включаем сообщения
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
ws.title = "Журнал"
dv = DataValidation(
    type="list",
    formula1='"Американо,Капучино,Латте"',
    allow_blank=True,
    showErrorMessage=True,   # показать ошибку при плохом вводе
    showInputMessage=True,   # показать подсказку при выборе ячейки
)
dv.errorTitle = "Нет такого напитка"
dv.error = "Выбери напиток из списка меню"
dv.promptTitle = "Напиток"
dv.prompt = "Выбери из выпадающего списка"
ws.add_data_validation(dv)
dv.add("B2:B20")
wb.save("dukat_dv2.xlsx")

wb2 = load_workbook("dukat_dv2.xlsx")
d = wb2["Журнал"].data_validations.dataValidation[0]
print("showErrorMessage:", d.showErrorMessage)
print("showInputMessage:", d.showInputMessage)
print("prompt:", d.prompt)
wb2.close()
os.remove("dukat_dv2.xlsx")
Вывод
showErrorMessage: True
showInputMessage: True
prompt: Выбери из выпадающего списка

Список из диапазона другого листа

Перечислять напитки прямо в формуле удобно, пока меню не поменяется: сменил цену и состав — и ползуй по всем правилам файла. Правильнее держать меню на отдельном листе и указывать правило ссылку на диапазон: =Меню!$A$1:$A$5. Поменял меню — список обновился сам, без единой строчки кода.

список с листа «Меню»
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
menu = wb.active
menu.title = "Меню"
for i, drink in enumerate(["Американо", "Капучино", "Латте", "Раф", "Какао"], start=1):
    menu.cell(row=i, column=1, value=drink)

ws = wb.create_sheet("Журнал")
dv = DataValidation(type="list", formula1="=Меню!$A$1:$A$5", allow_blank=True)
ws.add_data_validation(dv)
dv.add("B2:B20")
wb.save("dukat_dv3.xlsx")

wb2 = load_workbook("dukat_dv3.xlsx")
d = wb2["Журнал"].data_validations.dataValidation[0]
print("formula1:", d.formula1)
print("диапазон:", str(d.sqref))
wb2.close()
os.remove("dukat_dv3.xlsx")
Вывод
formula1: =Меню!$A$1:$A$5
диапазон: B2:B20

Два штриха к ссылке. Знак равенства в начале обязателен — так правило отличает ссылку от перечня строк. Доллары в $A$1:$A$5 фиксируют адрес: правило не «съедет», когда на листе что-то добавят или отсортируют. У строкового списка есть ещё жёсткий лимит — вся строка formula1 не длиннее 255 символов; для длинных справочников (список клиентов, SKU) диапазон — единственный выход.

openpyxl проверяет введённые значения?

Проверим границу ответственности напрямую: запишем в проверяемую ячейку напиток, которого нет в списке, и посмотрим, что будет.

мусор в проверяемую ячейку
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
dv = DataValidation(type="list", formula1='"Американо,Капучино,Латте"', allow_blank=True)
ws.add_data_validation(dv)
dv.add("B2:B20")

ws["B2"] = "Латте макиято"   # в списке такого нет
wb.save("dukat_check.xlsx")

wb2 = load_workbook("dukat_check.xlsx")
print("B2:", wb2.active["B2"].value)
wb2.close()
os.remove("dukat_check.xlsx")
Вывод
B2: Латте макиято

Ни ошибки, ни предупреждения — значение лежит в файле как ни в чём не бывало. Проверка данных — это не про запись, а про ввод: openpyxl кладёт правило в файл, а следит за ним Excel в тот момент, когда человек печатает. Это же значит, что «испорченные» старые файлы правило само не вычистит: Excel помечает невалидные значения только по команде «Обвести неверные данные» на ленте.

С уже испорченными журналами поступают так: читают колонку через iter_rows, приводят варианты к канону словарём нормализации («капучино» → «Капучино», «каппучино» → «Капучино») и перезаписывают файл — а уже затем закрывают колонку списком. Сначала чистка, потом замок: правило, поставленное поверх мусора, останется декорацией.

ПараметрЧто делаетЗначения
typeтип проверкиlist, whole, decimal, date, time, textLength, custom
formula1 / formula2варианты списка или границы'"Латте,Раф"', "=Меню!$A$1:$A$5", "0", "500"
operatorправило сравнения для чисел и датbetween, greaterThan, lessThan, equal
allow_blankразрешить пустую ячейкуTrue / False
showErrorMessageпоказывать диалог об ошибкеTrue / False, по умолчанию False
errorStyleстрогость диалогаstop, warning, information

Как ограничить не список, а число?

Выпадающим списком дело не ограничивается: в журнал «Дуката» бариста вписывают количество проданных стаканов, и туда умудрялись записать «-5» и «два». Правило type="whole" с оператором between разрешает только целые числа в границах.

целые от 0 до 500
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
ws.title = "Журнал"
dv = DataValidation(
    type="whole",
    operator="between",
    formula1="0",
    formula2="500",
    allow_blank=True,
    showErrorMessage=True,
    errorTitle="Проверь количество",
    error="Количество — целое число от 0 до 500",
)
ws.add_data_validation(dv)
dv.add("C2:C50")
wb.save("dukat_whole.xlsx")

wb2 = load_workbook("dukat_whole.xlsx")
d = wb2["Журнал"].data_validations.dataValidation[0]
print("type:", d.type, "| operator:", d.operator)
print("границы:", d.formula1, "...", d.formula2)
print("диапазон:", str(d.sqref))
wb2.close()
os.remove("dukat_whole.xlsx")
Вывод
type: whole | operator: between
границы: 0 ... 500
диапазон: C2:C50

Тот же каркас работает для дат (type="date" — не дай вписать смену 32-м числом) и длины текста (type="textLength" — артикул ровно из восьми символов). Меняются только type, operator и границы — механика привязки к диапазону одна и та же.

Какие колонки вообще стоит закрывать списком?

Проверка данных — не украшение, а способ зафиксировать договор о том, что именно заходит в таблицу. Практика показывает: чем раньше колонка закрыта, тем меньше боли в сводках. Типовые кандидаты в любом бизнесе:

  • статусы: «принят», «в работе», «готов», «отменён» — без списка рано или поздно появится «готово (не отдано)»;
  • справочники: товары, точки, исполнители — держи их на отдельном листе и ссылайся диапазоном;
  • колонки для чужих людей: журнал баристы, опрос клиентов, заявки от коллег — люди пишут как удобнее им;
  • числа с границами: количество, оценка, процент — whole и decimal с operator вместо списка;
  • даты: type="date" отсечёт «31.02» ещё на вводе.

Собираем журнал «Дуката» целиком

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

журнал с проверкой по меню
import os
from datetime import datetime
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
menu = wb.active
menu.title = "Меню"
for i, drink in enumerate(["Американо", "Капучино", "Латте", "Раф", "Какао"], start=1):
    menu.cell(row=i, column=1, value=drink)

ws = wb.create_sheet("Журнал")
for col, title in enumerate(["Дата", "Бариста", "Напиток"], start=1):
    c = ws.cell(row=1, column=col, value=title)
    c.font = Font(bold=True, color="FFFFFF")
    c.fill = PatternFill("solid", fgColor="6F4E37")

ws["A2"] = datetime(2026, 10, 1)
dv = DataValidation(
    type="list",
    formula1="=Меню!$A$1:$A$5",
    allow_blank=True,
    showErrorMessage=True,
    errorTitle="Нет в меню",
    error="Выбери напиток из листа Меню",
)
ws.add_data_validation(dv)
dv.add("C2:C30")
wb.save("dukat_journal.xlsx")

wb2 = load_workbook("dukat_journal.xlsx")
ws2 = wb2["Журнал"]
d = ws2.data_validations.dataValidation[0]
print("листы:", wb2.sheetnames)
print("список из:", d.formula1)
print("проверяем:", str(d.sqref))
wb2.close()
os.remove("dukat_journal.xlsx")
Вывод
листы: ['Меню', 'Журнал']
список из: =Меню!$A$1:$A$5
проверяем: C2:C30

Файл готов к выдаче: дата в A2 — настоящий datetime из урока про даты, шапка окрашена, напитки выбираются из меню. Когда заведение добавит «Матча», ты допишешь одну строку на лист «Меню» — правило подхватит новый вариант без единой правки кода.

Ошибку можно смягчить: stop, warning, information

Строгий stop — не единственный вариант: errorStyle="warning" разрешит ввести постороннее значение после предупреждения, а "information" — мягко напомнит и примет всё, что ввели. Для журнала, где бариста иногда продаёт лимонад вне меню, warning честнее stop: правило помогает, а не воюет.

нестрогая ошибка: warning
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
ws.title = "Журнал"
dv = DataValidation(
    type="list",
    formula1='"Американо,Капучино,Латте"',
    allow_blank=True,
    showErrorMessage=True,
    errorStyle="warning",
    errorTitle="Точно верно?",
    error="Такого напитка нет в меню",
)
ws.add_data_validation(dv)
dv.add("B2:B20")
wb.save("dukat_warn.xlsx")

d = load_workbook("dukat_warn.xlsx")["Журнал"].data_validations.dataValidation[0]
print("errorStyle:", d.errorStyle)
print("error:", d.error)
os.remove("dukat_warn.xlsx")
Вывод
errorStyle: warning
error: Такого напитка нет в меню

Выбор строгости — это вопрос доверия к тому, кто заполняет файл. Для колонок, где ошибка ломает расчёты (цены, артикулы), оставляй stop: молчаливое «почти правильно» хуже честного отказа. Для журналов и форм, где возможны исключения вне меню, warning спасает и от анархии, и от тупика — человек подтвердит «Лимонад» осознанно, и он останется в файле.

Как правило выглядит внутри файла?

Ты уже видел в уроке про картинки, что xlsx — zip-архив. Правило валидации живёт там же: XML-элемент dataValidation внутри разметки листа. Заглянем глазами zipfile — и заодно увидим те самые флаги, о которых говорили выше.

правило внутри xlsx
import os
import re
import zipfile
from openpyxl import Workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
dv = DataValidation(type="list", formula1='"Американо,Капучино,Латте"', allow_blank=True)
ws.add_data_validation(dv)
dv.add("B2:B20")
wb.save("dukat_xml.xlsx")

z = zipfile.ZipFile("dukat_xml.xlsx")
xml = z.read("xl/worksheets/sheet1.xml").decode("utf-8")
m = re.search(r"<dataValidation .*?</dataValidation>", xml)
print(m.group(0))
z.close()
os.remove("dukat_xml.xlsx")
Вывод
<dataValidation sqref="B2:B20" showDropDown="0" showInputMessage="0" showErrorMessage="0" allowBlank="1" type="list"><formula1>"Американо,Капучино,Латте"</formula1></dataValidation>

Вот они, наши знания в первозданном виде: sqref с диапазоном, allowBlank="1", формула со списком во внутренних кавычках — и showErrorMessage="0", потому что флаг мы не включали. Excel читает ровно эту разметку и решает, как охранять ячейки.

Правило валидации — это договор, записанный в файл: библиотека его составляет, Excel — исполняет.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
dv = DataValidation(type="list", formula1='"Раф,Какао"', allow_blank=True)
ws.add_data_validation(dv)
dv.add("C2:C9")
dv.add("E2:E9")
wb.save("oq15.xlsx")

wb2 = load_workbook("oq15.xlsx")
d = wb2.active.data_validations.dataValidation[0]
print(len(wb2.active.data_validations.dataValidation))
print(str(d.sqref))
wb2.close()
os.remove("oq15.xlsx")
import os
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation

wb = Workbook()
ws = wb.active
dv = DataValidation(type="list", formula1='"Латте,Раф"', allow_blank=True)
dv.error = "Нет в меню"
ws.add_data_validation(dv)
dv.add("B2:B9")
wb.save("oq15b.xlsx")

wb2 = load_workbook("oq15b.xlsx")
d = wb2.active.data_validations.dataValidation[0]
print(d.formula1)
print(d.showErrorMessage)
print(d.error)
wb2.close()
os.remove("oq15b.xlsx")
Проверь себя
0 / 5

1. Как правильно передать варианты в строковый список DataValidation?

2. Ты задал dv.error, но диалог об ошибке не появляется. Что забыли?

3. Что означает запись formula1="=Меню!$A$1:$A$5"?

4. openpyxl записал в проверяемую ячейку «Латте макиято», которого нет в списке. Что произойдёт?

5. Какое правило разрешит в ячейке только целые числа от 0 до 100?

Карточки терминов
Запомнено: 0 / 6
Практика

Собери мини-журнал кофейни: на листе «Журнал» добавь выпадающий список с вариантами «Раф, Какао, Матча» (одной строкой, без листа «Меню») для диапазона B2:B15, включи показ ошибки с текстом «Нет в меню». Сохрани файл, перечитай его и выведи три строки: formula1, диапазон и флаг showErrorMessage.

practice.py
Вопросы и ответы по уроку

Как сделать выпадающий список в Excel через Python?

Через openpyxl: создайте DataValidation с type="list" и formula1 со списком вариантов во внутренних кавычках, привяжите правило к листу методом ws.add_data_validation и назначьте диапазон через dv.add("B2:B20"). После сохранения в ячейках диапазона появляется выпадающий список.

Почему не работает ошибка ввода в DataValidation?

Скорее всего не установлен флаг showErrorMessage: по умолчанию он выключен, и Excel не показывает ни dv.error, ни dv.errorTitle. Передайте showErrorMessage=True в конструкторе или присвойте его отдельно — диалог появится при вводе значения вне списка.

Как сделать список из значений другого листа?

В formula1 передайте ссылку вида "=Меню!$A$1:$A$5", где «Меню» — лист с вариантами, а доллары закрепляют диапазон. Такой список обновляется, когда меняется лист-источник, и снимает лимит в 255 символов, который есть у строкового перечня.

openpyxl не даёт записать значение вне списка — это норм?

Наоборот: openpyxl вообще не проверяет значения и запишет что угодно. Правило срабатывает только при вводе в Excel, когда человек выбирает из выпадающего списка или печатает вручную. Проверка данных — про ввод, а не про запись.

Понравился урок? Сошлитесь на него

«Проверка данных — это не про запись, а про ввод: openpyxl кладёт правило в файл, а следит за ним Excel в тот момент, когда человек печатает.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.