Первый проект: от идеи до работающей программы
До этого момента вы решали чужие задачи. Условие было готовым, правильный ответ существовал, и если что-то не получалось — вы точно знали, что искать нужно в текущей теме: раз статья про циклы, значит нужен цикл.
Проект устроен иначе. Никто не говорит, что делать. Никто не подсказывает, какая конструкция здесь уместна. И — самое непривычное — нет «правильного ответа»: есть только «работает» и «не работает».
Именно на этом переходе застревает огромное количество людей. Они прошли двадцать уроков, решили сотню упражнений, а сесть и написать свою программу не могут — экран пустой, и непонятно, с какой строчки начинать.
Хорошая новость: это не про знания. Вам уже хватает всего, что вы прошли. Не хватает процесса — привычной последовательности действий, которая ведёт от размытой идеи в голове до файла, который запускается. Этот процесс мы сегодня разберём и тут же пройдём целиком на настоящем проекте.
Проект — это не «упражнение побольше»
Разница не в объёме, а в четырёх вещах.
У упражнения одна задача, у проекта — много связанных. «Посчитать сумму списка» — это одна мысль. «Менеджер задач» — это десяток мыслей, которые ещё и мешают друг другу: как хранить, как показывать, что делать при кривом вводе.
Упражнение вы держите в голове целиком, проект — нет. Как только программа переваливает за полсотни строк, вы физически перестаёте помнить её целиком. Отсюда все приёмы вроде разбиения на функции — это не «хороший стиль» ради стиля, это способ не сойти с ума.
У упражнения есть конец, у проекта — нет. Проект всегда можно улучшать дальше. Поэтому решать, когда остановиться, придётся вам, и решать заранее.
Упражнение никто не запускает дважды. А проектом вы будете пользоваться сами, и он должен помнить данные, не падать от опечатки и не бесить вас через неделю.
Как выбрать первую идею
Три критерия. Все три обязательны — если хоть один не выполняется, проект, скорее всего, заглохнет.
1. Вы сами будете этим пользоваться. Не «полезно для портфолио», а «мне это правда нужно». Когда программа решает вашу настоящую мелкую боль, у вас есть бесконечный источник мотивации: вы её запускаете и видите, чего не хватает.
2. Идея помещается в одно предложение. «Программа, которая хранит мой список дел в терминале» — годится. «Соцсеть для любителей растений с чатом и картой» — нет: это не идея, это список из тридцати идей.
3. Результат видно глазами. Что-то печатается на экран, где-то появляется файл. Программа, у которой нет наблюдаемого результата, отнимает главное топливо новичка — ощущение, что оно живое.
Чего не стоит брать первым проектом, при всём уважении к амбициям: игру с графикой (там своя большая тема — библиотеки и игровой цикл), сайт (нужен ещё целый слой знаний про веб — см. Как работают интернет и сайты), нейросеть (нужна математика и данные) и «клон популярного приложения» (там годы работы команды).
Хороший первый проект — консольный: только текст в терминале, данные в файле. Скучно выглядит, зато вы дойдёте до конца.
Мы сделаем менеджер задач: список дел, который живёт в терминале и помнит записи между запусками. Да, это классика и таких программ миллион. Это как раз плюс: если застрянете, в интернете найдётся много объяснений, а если не застрянете — легко придумать своё продолжение.
Формулируем задачу: одно предложение и список функций
Прежде чем писать код, напишите обычными словами, что программа делает. Буквально в текстовом файле или на бумаге.
Список дел. Программа для терминала: хранит мои задачи, позволяет добавлять новые и отмечать выполненные. Задачи не пропадают после закрытия.
Теперь выпишите всё, что хотелось бы, — не сдерживаясь. А потом безжалостно разделите на «нужно, чтобы программа вообще имела смысл» и «потом».
Левая ветка — это MVP (minimum viable product, «минимально работающий продукт»): самая маленькая версия, которой уже можно пользоваться. Всё остальное отложено — не выброшено, а именно отложено, список сохраняется.
Причина такого деления не в скромности. Если вы попытаетесь сделать сразу всё, вы никогда не увидите работающую программу — а именно она даёт понять, что делать дальше. У Мартина Фаулера этот принцип называется YAGNI — You Aren’t Gonna Need It, «не понадобится тебе это». Большая часть заранее придуманных функций после первого запуска оказывается ненужной или устроенной совсем иначе.
Аналогия. MVP — это как переехать в квартиру, где есть кровать, чайник и лампочка. Жить можно уже сегодня, а шкаф и полки докупаются по мере понимания, что и где стоять должно.
Где аналогия ломается. В квартире с одной кроватью вам просто неуютно. Программа без ключевой функции может быть буквально бессмысленной: список дел, который не сохраняется, никому не нужен. Поэтому в MVP попадает не «самое дешёвое», а «то, без чего идея не работает».
Главная ошибка: строить слоями
Вот момент, где новички чаще всего теряются. Спросите себя: с чего начать код?
Инстинкт подсказывает — «сначала сделаю всё хранение данных, потом всю логику, потом красивое меню». Логично звучит. И почти гарантированно приводит к тому, что проект умирает.
Когда вы строите слоями, программа не запускается до самого конца. Вы пишете вслепую: код есть, а работает он или нет — неизвестно. Ошибка, допущенная в первый день, вылезет на третий, и к тому времени вокруг неё уже накручено двести строк.
Тонкий вертикальный срез — это когда каждый шаг проходит через все слои сразу, но делает лишь чуть-чуть. Программа после каждого шага запускается и что-то умеет. Ошибка обнаруживается там же, где сделана, потому что вокруг неё — десять новых строк, а не двести.
У этого приёма есть имя — walking skeleton, «ходячий скелет» Алистера Коберна: сначала собирается тончайшая версия, которая проходит весь путь насквозь, а потом наращивается мясо.
И связанное наблюдение, известное как закон Джона Голла: работающая сложная система всегда происходит от работавшей простой системы. Сложную систему, спроектированную сразу целиком, обычно не удаётся заставить работать вообще.
Практическое правило, которое стоит повесить над столом:
В конце каждого рабочего сеанса программа должна запускаться и что-то делать.
Проектируем данные раньше кода
Один шаг всё-таки делается до написания функций: решить, в каком виде хранятся данные. Это скелет, на который потом навешивается всё остальное, и переделывать его больно.
Что такое одна задача? У неё есть текст и признак «сделана или нет». Значит, это словарь (см. Списки, словари и множества):
{"название": "купить хлеб", "сделана": False}
А все задачи — список таких словарей:
[
{"название": "купить хлеб", "сделана": False},
{"название": "позвонить маме", "сделана": True},
]
Почему список словарей, а не, например, два параллельных списка (отдельно названия, отдельно галочки)? Потому что связанные данные должны лежать вместе. С двумя списками вы обязаны следить, чтобы они не разъехались по длине, — и однажды не уследите.
Такая структура прямо ложится в JSON из статьи Файлы и форматы данных. Это не совпадение: JSON и придуман для списков и словарей.
Жизненный цикл одной задачи получается совсем простой — и это тоже полезно нарисовать, чтобы увидеть, какие действия вообще нужны:
Шаг 1. Скелет, который уже работает
Начинаем с самого тонкого среза: программа показывает меню, принимает команду и не делает ничего осмысленного. Зато запускается.
Создайте файл tasks.py:
# tasks.py — шаг 1: пока только меню
while True: # бесконечный цикл: крутимся, пока не выйдут
print() # пустая строка — просто чтобы не слипалось
print("1 — показать задачи")
print("2 — добавить задачу")
print("3 — отметить выполненной")
print("0 — выход")
choice = input("Ваш выбор: ") # ждём ввод; программа стоит, пока не нажмут Enter
if choice == "1":
print("(здесь будет список)")
elif choice == "2":
print("(здесь будет добавление)")
elif choice == "3":
print("(здесь будет отметка)")
elif choice == "0":
print("Пока!")
break # break прерывает while — программа завершается
else:
print("Не понимаю такую команду.")
Важная деталь: input() всегда возвращает текст, поэтому сравниваем с "1" в кавычках, а не с 1. Сравнение choice == 1 тихо даст False при любом вводе, и вы будете долго удивляться, почему меню не реагирует.
Запустите (python tasks.py), понажимайте цифры, выйдите через 0. Работает? Первый срез готов.
Вот тот же цикл в виде схемы — так виднее, что программа делает по кругу:
и добавить"] C -- "3" --> F["Спросить номер
и отметить"] C -- "0" --> G["Выход"] C -- "другое" --> H["Сказать, что не понял"] D --> A E --> A F --> A H --> A
Шаг 2. Задачи в памяти
Теперь пусть программа действительно хранит задачи — пока только на время запуска. Добавляем список и две функции.
tasks = [] # список задач; пока пустой
def add_task(tasks, title):
"""Добавляет новую невыполненную задачу в конец списка."""
tasks.append({"название": title, "сделана": False})
def format_tasks(tasks):
"""Превращает список задач в текст для показа на экране."""
if not tasks: # пустой список считается ложью
return "Список пуст. Самое время что-нибудь добавить."
lines = []
for number, task in enumerate(tasks, start=1):
mark = "x" if task["сделана"] else " " # галочка или пробел
lines.append(f"{number}. [{mark}] {task['название']}")
return "\n".join(lines)
Разберём построчно то, что могло быть непривычным.
enumerate(tasks, start=1) на каждом витке цикла отдаёт пару: порядковый номер и сам элемент. Без него пришлось бы заводить отдельную переменную-счётчик и не забывать её увеличивать. start=1 нужен, потому что людям привычна нумерация с единицы, а Python внутри считает с нуля — это расхождение мы ещё вспомним.
mark = "x" if task["сделана"] else " " — короткая запись условия: «возьми "x", если задача сделана, иначе пробел». То же самое можно написать обычным if/else на четыре строки.
"\n".join(lines) склеивает список строк в одну, вставляя между ними перевод строки. Печатать текст одним print в конце удобнее, чем печатать по кусочкам: функция возвращает текст, а кто и куда его выведет — уже не её забота.
Заметьте: format_tasks ничего не печатает. Функция, которая считает и функция, которая общается с человеком, — разные функции. Это то самое разделение слоёв с картинки, только в миниатюре, и именно оно позже позволит спокойно менять интерфейс, не трогая логику.
Подключаем их к меню:
if choice == "1":
print(format_tasks(tasks))
elif choice == "2":
title = input("Что нужно сделать? ").strip()
if title: # пустая строка — тоже ложь
add_task(tasks, title)
print("Добавлено.")
else:
print("Пустую задачу добавлять не буду.")
.strip() убирает пробелы по краям — без него задача " " из одних пробелов пройдёт проверку и попадёт в список.
Запустите, добавьте пару задач, посмотрите список. Работает — второй срез готов.
Шаг 3. Отмечаем выполненное
def complete_task(tasks, number):
"""Отмечает задачу под номером number выполненной.
Возвращает True, если номер существует, и False, если нет."""
if 1 <= number <= len(tasks): # номер в допустимых пределах?
tasks[number - 1]["сделана"] = True
return True
return False
Здесь спрятана ошибка, на которой спотыкаются вообще все: number - 1. Пользователь видит задачу под номером 1, а в списке она лежит по индексу 0. Забыть про минус единицу — значит отмечать не ту задачу; выйти за границы — получить IndexError.
Функция возвращает True/False вместо того, чтобы самой напечатать «нет такой задачи». Это опять разделение слоёв: логика сообщает результат, а интерфейс решает, какими словами об этом сказать.
Шаг 4. Защищаемся от кривого ввода
Пользователь введёт «два» вместо «2». Или «первая». Или просто нажмёт Enter. Программа не должна от этого падать.
Если написать просто number = int(input("Номер: ")), то на вводе «два» вы получите:
Traceback (most recent call last):
File "tasks.py", line 42, in <module>
number = int(input("Номер: "))
ValueError: invalid literal for int() with base 10: 'два'
Дословно это значит: «неподходящая запись для int() с основанием 10: 'два'» — то есть функция int не смогла увидеть в тексте два десятичное число. Разбирать такие сообщения мы учились в статье Ошибки и отладка; здесь важно другое — падать программе нельзя, ошибку надо перехватить.
def ask_number(question):
"""Спрашивает число. Если человек ввёл не число — возвращает None."""
answer = input(question)
try:
return int(answer)
except ValueError:
print(f"«{answer}» — это не номер. Нужно целое число.")
return None
None — специальное значение «ничего». Оно как раз для случая «ответа нет». Проверять его нужно через is None, а не == None.
Отдельно отметим: перехватывается конкретный ValueError, а не любые ошибки подряд. Голый except: проглотит и опечатку в имени переменной, и вашу собственную ошибку в коде — и вы будете часами искать, почему программа «просто ничего не делает».
Шаг 5. Сохранение между запусками
Последний срез MVP — и самый ценный. Без него программой пользоваться нельзя.
import json
FILE_NAME = "tasks.json"
def load_tasks():
"""Читает задачи из файла. Если файла нет — возвращает пустой список."""
try:
with open(FILE_NAME, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError: # первый запуск: файла ещё нет — это нормально
return []
def save_tasks(tasks):
"""Записывает список задач в файл, перезаписывая старое содержимое."""
with open(FILE_NAME, "w", encoding="utf-8") as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
FILE_NAME записано заглавными буквами — так в Python помечают константы, значения, которые не меняются по ходу программы. Технически Python не запрещает их менять, это договорённость между людьми. Смысл в том, чтобы имя файла лежало в одном месте, а не было размазано по коду в пяти строчках.
ensure_ascii=False нужен, чтобы русские буквы в файле остались буквами, а не превратились в купить. indent=2 делает файл читаемым для человека — откройте tasks.json редактором и убедитесь сами.
Когда сохранять? Простейший честный ответ — после каждого изменения. Задач у вас десятки, файл крошечный, а вот привычка сохранять «при выходе» однажды закончится тем, что программа закроется аварийно и день работы пропадёт.
Собираем всё вместе
Вот итоговая программа целиком. Обратите внимание на комментарии-разделители: функции сгруппированы по слоям — данные, логика, интерфейс. Файл в семьдесят строк ещё читается глазами, а в двести — уже нет, и такая разметка спасает.
"""Список дел — маленький менеджер задач для терминала."""
import json
FILE_NAME = "tasks.json"
# --- слой данных: файл ---
def load_tasks():
"""Читает задачи из файла. Если файла нет — возвращает пустой список."""
try:
with open(FILE_NAME, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
def save_tasks(tasks):
"""Записывает список задач в файл, перезаписывая старое содержимое."""
with open(FILE_NAME, "w", encoding="utf-8") as f:
json.dump(tasks, f, ensure_ascii=False, indent=2)
# --- слой логики: что можно делать с задачами ---
def add_task(tasks, title):
"""Добавляет новую невыполненную задачу в конец списка."""
tasks.append({"название": title, "сделана": False})
def complete_task(tasks, number):
"""Отмечает задачу под номером number выполненной.
Возвращает True, если номер существует, и False, если нет."""
if 1 <= number <= len(tasks):
tasks[number - 1]["сделана"] = True
return True
return False
def format_tasks(tasks):
"""Превращает список задач в текст для показа на экране."""
if not tasks:
return "Список пуст. Самое время что-нибудь добавить."
lines = []
for number, task in enumerate(tasks, start=1):
mark = "x" if task["сделана"] else " "
lines.append(f"{number}. [{mark}] {task['название']}")
return "\n".join(lines)
# --- слой интерфейса: разговор с человеком ---
def ask_number(question):
"""Спрашивает число. Если человек ввёл не число — возвращает None."""
answer = input(question)
try:
return int(answer)
except ValueError:
print(f"«{answer}» — это не номер. Нужно целое число.")
return None
def main():
tasks = load_tasks()
print("Список дел. Введите 0, чтобы выйти.")
while True:
print()
print("1 — показать задачи")
print("2 — добавить задачу")
print("3 — отметить выполненной")
print("0 — выход")
choice = input("Ваш выбор: ").strip()
if choice == "1":
print(format_tasks(tasks))
elif choice == "2":
title = input("Что нужно сделать? ").strip()
if title:
add_task(tasks, title)
save_tasks(tasks)
print("Добавлено.")
else:
print("Пустую задачу добавлять не буду.")
elif choice == "3":
print(format_tasks(tasks))
number = ask_number("Номер задачи: ")
if number is not None:
if complete_task(tasks, number):
save_tasks(tasks)
print("Отлично, минус одно дело.")
else:
print(f"Задачи с номером {number} нет.")
elif choice == "0":
print("Пока!")
break
else:
print("Не понимаю такую команду. Введите 0, 1, 2 или 3.")
main()
Ожидаемый вывод при первом запуске:
Список дел. Введите 0, чтобы выйти.
1 — показать задачи
2 — добавить задачу
3 — отметить выполненной
0 — выход
Ваш выбор: 2
Что нужно сделать? купить хлеб
Добавлено.
...
Ваш выбор: 1
1. [ ] купить хлеб
2. [x] позвонить маме
Функция main — это просто договорённость: точка входа, откуда начинается программа. В чужом коде вы часто увидите рядом строчку if __name__ == "__main__": main() — она означает «запускай main только если файл запустили напрямую, а не подключили как библиотеку». Пока достаточно знать, что это такое; писать обычный вызов main() тоже совершенно нормально.
Закройте программу и запустите заново — задачи на месте. Вот теперь это работающий проект, а не упражнение.
Как это выглядело в истории коммитов
Если вы вели проект под Git из статьи Командная строка и Git, история получится ровно по срезам — по одному коммиту на шаг:
Такая история — не бюрократия. Это страховка: если после очередной правки всё сломалось и вы не понимаете почему, всегда можно вернуться к последнему состоянию, где работало. Коммит стоит делать в тот момент, когда программа запускается и делает то, что должна, — то есть в конце каждого среза.
README: две минуты, которые всё меняют
Рядом с tasks.py положите файл README.md — короткое описание проекта. Это первое, что открывает любой человек (включая вас через полгода).
# Список дел
Маленький менеджер задач для терминала. Хранит задачи в `tasks.json`
рядом со скриптом, поэтому они не пропадают между запусками.
## Как запустить
python tasks.py
Нужен Python 3.10 или новее, сторонних библиотек нет.
## Что умеет
- показать список задач;
- добавить задачу;
- отметить задачу выполненной.
## Чего пока не умеет
- удалять задачи;
- сроки и приоритеты.
Три вопроса, на которые README обязан отвечать: что это, как запустить, что умеет. Всё остальное — по желанию. Развёрнутые рекомендации есть на makeareadme.com.
Типичные ошибки первых проектов
«Сначала придумаю архитектуру». Пока проект меньше пары сотен строк, архитектура — это «разложить функции по смыслу». Продуманная заранее структура для программы, которой ещё нет, почти всегда оказывается неподходящей.
Слишком большой первый шаг. Если между двумя запусками программы вы написали сто строк — шаг был слишком велик. Режьте мельче: неприятно чувствовать себя медленным, зато вы не будете час искать ошибку в куче нового кода.
Красота вместо работы. Рамочки, цвета, ASCII-арт в меню — заманчиво и абсолютно бесполезно, пока сохранение не работает. Оформление всегда последним.
Копирование кода, который вы не понимаете. Найти в интернете готовый кусок — нормально. Вставить его, не понимая ни строчки, — гарантированный тупик: сломается он обязательно, а чинить будет нечего. Правило простое: не можете объяснить строку вслух — не вставляйте её.
Ожидание, что «правильно» бывает одно. Ваш код можно написать десятком способов, и почти все они приемлемы. Работающая программа, написанная неидеально, бесконечно лучше идеальной, которой нет.
Бесконечное расширение. Проект без границы не заканчивается никогда. Отметьте себе: MVP готов — проект завершён. Дальше начинается «версия 2», и это уже другое решение, принятое осознанно.
Страх показать. Ваш первый проект будет несовершенным — это норма, а не приговор. Покажите его тому, кто программирует: один взгляд со стороны заменяет неделю самостоятельных догадок.
Практика
Задания идут от простого к сложному. Первые четыре — доработка нашего менеджера задач, последнее — ваш собственный проект.
1. Счётчик в меню. Сделайте так, чтобы перед меню выводилась строка вида Задач: 5, выполнено: 2.
Подсказка: всего задач — это len(tasks). Выполненные считайте циклом с переменной-счётчиком, либо через sum(1 for t in tasks if t["сделана"]), если хочется короче.
2. Удаление задачи. Добавьте пункт меню 4 — удалить задачу. По номеру задача исчезает из списка, файл сразу перезаписывается.
Подсказка: напишите функцию delete_task(tasks, number) по образцу complete_task — с той же проверкой 1 <= number <= len(tasks) и тем же number - 1. Удаляет элемент tasks.pop(number - 1).
3. Спрятать выполненное. Сделайте, чтобы выполненные задачи по умолчанию не показывались, а пункт 5 — показать все выводил список целиком.
Подсказка: добавьте в format_tasks второй параметр со значением по умолчанию: def format_tasks(tasks, show_done=False). Внутри пропускайте задачу через continue, если она сделана и показывать её не просили. Только осторожно: если фильтровать список до enumerate, номера съедут и пользователь отметит не ту задачу — подумайте, как этого избежать.
4. Дата создания (со звёздочкой). Пусть каждая задача запоминает, когда её создали, и список показывает дату.
Подсказка: нужен модуль из стандартной библиотеки: from datetime import date, затем date.today().isoformat() даёт строку вида "2026-07-16". Строка кладётся в словарь третьим ключом. Внимание: старые задачи в tasks.json этого ключа не имеют, и обращение task["дата"] уронит программу с KeyError. Безопасный способ достать значение с запасным вариантом — task.get("дата", "—").
5. Свой проект. Возьмите свою идею и пройдите весь путь: одно предложение → список функций → деление на MVP и «потом» → структура данных → срезы по одному. Идеи, которые точно укладываются в изученное: учёт расходов, дневник тренировок, тренажёр иностранных слов, генератор паролей с сохранением сайтов, счётчик привычек по дням. Подсказка: если не получается придумать первый срез — берите тот, который просто выводит на экран заранее вписанные в код данные. Это почти всегда правильное начало.
Мини-итог
Проект отличается от упражнения не размером, а тем, что задачу вы ставите себе сами и сами решаете, когда остановиться. Поэтому всё начинается не с кода, а с одного предложения о том, что программа делает, и с деления функций на MVP и «потом».
Дальше — главное правило: никаких слоёв, только тонкие вертикальные срезы. Каждый шаг проходит насквозь от данных до экрана, и после каждого шага программа запускается. Это даёт и раннее обнаружение ошибок, и то самое ощущение движения, без которого проекты умирают.
Порядок внутри кода наводится теми же средствами, что вы уже знаете: функции по смыслу, разделение «считает» и «показывает», константы вместо разбросанных значений, перехват конкретных ошибок вместо голого except. А снаружи — коммит на каждый срез и README из трёх абзацев.
Если хочется копать глубже: учебник Python на официальном сайте (главы про модули и стандартную библиотеку — логичный следующий шаг), разбор YAGNI у Мартина Фаулера и «ходячий скелет» Алистера Коберна.
Что дальше
У вас есть работающая программа, которую вы написали сами, — этого достаточно, чтобы посмотреть на профессию не снаружи, а изнутри. И тут возникает следующий вопрос: а как всё это выглядит, когда за код платят деньги?
Кто такие тимлид, продакт и тестировщик, зачем нужны код-ревью и планёрки, почему разработчик пишет код далеко не весь рабочий день — разбираемся дальше.
Как устроена IT-индустрия: роли, команды, как выглядит рабочий день