Что мы строим
Финальный проект курса — небольшая CLI-утилита для управления задачами (todo). Она умеет:
add "Купить хлеб"— добавить задачу.list— показать все задачи.done 2— отметить задачу №2 выполненной.delete 1— удалить задачу №1.
Состояние сохраняется между запусками в файле tasks.json. Так
выглядит работа с утилитой:
$ python todo.py add "Выучить Python"
Добавлено: Выучить Python
$ python todo.py add "Написать проект"
Добавлено: Написать проект
$ python todo.py list
1. [ ] Выучить Python
2. [ ] Написать проект
$ python todo.py done 1
Выполнено: Выучить Python
$ python todo.py list
1. [x] Выучить Python
2. [ ] Написать проект
Здесь сходится всё, что мы изучили: типы, структуры данных, функции, работа с файлами и исключениями, классы и разбор аргументов командной строки.
Структура проекта
todo-app/
├── todo.py # точка входа + CLI
├── task.py # класс Task
├── storage.py # сохранение и загрузка JSON
└── tasks.json # данные (создаётся автоматически)
Разнесение по модулям — это правильно: каждый модуль отвечает за свою область и легко тестируется отдельно.
Шаг 1. Класс Task
Файл task.py — модель одной задачи с сериализацией:
'''Модель задачи для CLI todo-утилиты.'''
class Task:
'''Одна задача: название и флаг выполнения.'''
def __init__(self, title: str, done: bool = False) -> None:
self.title = title
self.done = done
def mark_done(self) -> None:
'''Отметить задачу выполненной.'''
self.done = True
def __str__(self) -> str:
marker = "[x]" if self.done else "[ ]"
return f"{marker} {self.title}"
def to_dict(self) -> dict:
'''Сериализация в словарь для JSON.'''
return {"title": self.title, "done": self.done}
@classmethod
def from_dict(cls, data: dict) -> "Task":
'''Создать объект из словаря (обратная операция).'''
return cls(title=data["title"], done=data.get("done", False))
@classmethod — это метод, привязанный к классу, а не к
объекту. Первый параметр cls — сам класс. Удобен для альтернативных
конструкторов, как здесь: Task.from_dict({...}).
Шаг 2. Хранение в файле
Файл storage.py — сохранение и загрузка списка задач в JSON.
Здесь мы применяем обработку исключений из главы 8:
'''Загрузка и сохранение задач в JSON-файле.'''
import json
from pathlib import Path
from task import Task
TASKS_FILE = Path("tasks.json")
def load_tasks() -> list[Task]:
'''Загружает задачи из файла. Если файла нет — возвращает пустой список.'''
try:
with TASKS_FILE.open(encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
return []
except json.JSONDecodeError:
print("Файл задач повреждён, начнём с чистого листа.")
return []
return [Task.from_dict(item) for item in data]
def save_tasks(tasks: list[Task]) -> None:
'''Сохраняет список задач в файл.'''
with TASKS_FILE.open("w", encoding="utf-8") as f:
json.dump(
[t.to_dict() for t in tasks],
f,
ensure_ascii=False,
indent=2,
)
Шаг 3. Разбор аргументов через argparse
argparse — стандартный модуль для создания CLI. Он сам парсит
аргументы, генерирует помощь и сообщения об ошибках:
import argparse
parser = argparse.ArgumentParser(description="Менеджер задач")
subparsers = parser.add_subparsers(dest="command", required=True)
# Команда add
add_parser = subparsers.add_parser("add", help="Добавить задачу")
add_parser.add_argument("title", help="Название задачи")
# Команда list
subparsers.add_parser("list", help="Показать все задачи")
# Команда done
done_parser = subparsers.add_parser("done", help="Отметить выполненной")
done_parser.add_argument("number", type=int, help="Номер задачи")
# Команда delete
del_parser = subparsers.add_parser("delete", help="Удалить задачу")
del_parser.add_argument("number", type=int, help="Номер задачи")
args = parser.parse_args()
# args.command -> "add" | "list" | "done" | "delete"
# args.title, args.number — аргументы конкретной команды
argparse автоматически создаёт помощь -h/--help:
$ python todo.py -h
usage: todo.py [-h] {add,list,done,delete} ...
Менеджер задач
positional arguments:
{add,list,done,delete}
add Добавить задачу
list Показать все задачи
done Отметить выполненной
delete Удалить задачу
Шаг 4. Собираем всё вместе
Файл todo.py — главный, связывает модель, хранение и CLI:
'''CLI-утилита «Менеджер задач».'''
import argparse
import sys
from storage import load_tasks, save_tasks
from task import Task
def cmd_add(args) -> None:
'''Обработчик команды add.'''
tasks = load_tasks()
task = Task(args.title)
tasks.append(task)
save_tasks(tasks)
print(f"Добавлено: {task.title}")
def cmd_list(args) -> None:
'''Обработчик команды list.'''
tasks = load_tasks()
if not tasks:
print("Список задач пуст")
return
for i, task in enumerate(tasks, start=1):
print(f"{i}. {task}")
def _find_task(number: int) -> tuple[list[Task], int]:
'''Возвращает список задач и индекс (с проверкой корректности номера).'''
tasks = load_tasks()
if not 1 <= number <= len(tasks):
print(f"Нет задачи с номером {number}")
sys.exit(1)
return tasks, number - 1 # индекс на 1 меньше номера
def cmd_done(args) -> None:
'''Обработчик команды done.'''
tasks, index = _find_task(args.number)
tasks[index].mark_done()
save_tasks(tasks)
print(f"Выполнено: {tasks[index].title}")
def cmd_delete(args) -> None:
'''Обработчик команды delete.'''
tasks, index = _find_task(args.number)
removed = tasks.pop(index)
save_tasks(tasks)
print(f"Удалено: {removed.title}")
def build_parser() -> argparse.ArgumentParser:
'''Создаёт и настраивает парсер аргументов.'''
parser = argparse.ArgumentParser(description="Менеджер задач")
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", help="Добавить задачу")
p_add.add_argument("title", help="Название задачи")
p_add.set_defaults(func=cmd_add)
p_list = sub.add_parser("list", help="Показать все задачи")
p_list.set_defaults(func=cmd_list)
p_done = sub.add_parser("done", help="Отметить выполненной")
p_done.add_argument("number", type=int, help="Номер задачи")
p_done.set_defaults(func=cmd_done)
p_del = sub.add_parser("delete", help="Удалить задачу")
p_del.add_argument("number", type=int, help="Номер задачи")
p_del.set_defaults(func=cmd_delete)
return parser
def main() -> None:
'''Точка входа в приложение.'''
parser = build_parser()
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
Что здесь нового
set_defaults(func=cmd_add)— к каждой команде привязываем функцию-обработчик. Затем простоargs.func(args).if __name__ == "__main__":— Python-идиома: код внутри выполнится, только если файл запущен напрямую, а не импортирован.sys.exit(1)— завершить программу с кодом ошибки (0 — успех, не ноль — ошибка).
Запуск
python todo.py add "Выучить Python"
python todo.py add "Сделать домашку"
python todo.py list
python todo.py done 1
python todo.py delete 2
python todo.py list
Содержимое tasks.json после нескольких команд:
[
{
"title": "Выучить Python",
"done": true
}
]
Идеи для развития
Этот проект — база. Вот что можно добавить самостоятельно, опираясь на пройденный материал:
- Приоритеты — поле
priorityв классеTaskи сортировка при выводе. - Дедлайны — через
datetime, отметка просроченных. - Категории/теги — список тегов и фильтр
list --tag=work. - Команда
clear— удалить все выполненные задачи. - Цветной вывод — через ANSI-коды или библиотеку
rich(потребуетсяpip install).
Куда развиваться дальше
Поздравляем — вы освоили базу Python и написали полноценное приложение. Дальнейшие направления:
- Веб-разработка — фреймворки Django (полный, с ORM и админкой) или Flask/FastAPI (лёгкие). На devdao есть отдельный курс по Django.
- Data Science — библиотеки
pandas,NumPy,matplotlibдля анализа и визуализации данных. - Автоматизация —
requests(HTTP),BeautifulSoup(парсинг сайтов),selenium(управление браузером). - Тестирование — фреймворк
pytest, писать тесты на свой код. - Асинхронность —
asyncio,aiohttpдля высоконагруженных приложений. - ООП глубже — магические методы, миксины, датаклассы (
@dataclass).
Главный совет: пишите код регулярно. Лучше 30 минут в день, чем 5 часов раз в неделю. Придумывайте маленькие проекты (калькулятор, парсер погоды, бот для Telegram) — на реальных задачах знания закрепляются намного быстрее, чем на абстрактных упражнениях.
Типичные ошибки
1. Номер задачи vs индекс списка
Пользователь видит задачи с номера 1, а в списке индексация с 0. Не
забывайте преобразовывать: index = number - 1, и проверять
границы.
2. Не сохранять после изменения
tasks = load_tasks()
tasks[0].mark_done()
# забыли save_tasks(tasks) — изменение потеряно
3. Игнорировать if __name__ == "__main__"
Без него код запустится и при импорте файла как модуля — это мешает тестированию. Всегда прячьте запуск в эту конструкцию.
Практика
- Соберите проект из четырёх файлов выше и убедитесь, что команды
add/list/done/deleteработают. - Добавьте команду
clear, которая удаляет все выполненные задачи. - Добавьте в класс
Taskполеpriorityи сортируйте задачи по приоритету при выводе. - Добавьте команду
edit <N> <новый текст>для переименования задачи. - Вынесите путь к файлу в переменную окружения
TODO_FILEчерезos.environ.get.
Итог курса
За 10 глав мы прошли путь от установки Python до самостоятельного CLI-приложения. Вы изучили:
- синтаксис, типы данных и операторы;
- управляющие конструкции — условия и циклы;
- коллекции — списки, кортежи, словари, множества;
- функции с параметрами,
*args/**kwargsи аннотациями типов; - работу с файлами и обработку исключений;
- модули, стандартную библиотеку и основы ООП;
- сборку полноценного приложения с сохранением состояния.
Это прочный фундамент, на котором строится любая специализация на Python. Удачи в дальнейших проектах — и помните, что лучший способ учиться дальше — решать настоящие задачи. До встречи в следующих курсах devdao!