Python с нуля · Глава 10 из 10

Глава 10. Практика: CLI-утилита «Менеджер задач»

Прогресс сохранится в этом браузере (войдите, чтобы синхронизировать).

Что мы строим

Финальный проект курса — небольшая 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__"

Без него код запустится и при импорте файла как модуля — это мешает тестированию. Всегда прячьте запуск в эту конструкцию.

Практика

  1. Соберите проект из четырёх файлов выше и убедитесь, что команды add/list/done/delete работают.
  2. Добавьте команду clear, которая удаляет все выполненные задачи.
  3. Добавьте в класс Task поле priority и сортируйте задачи по приоритету при выводе.
  4. Добавьте команду edit <N> <новый текст> для переименования задачи.
  5. Вынесите путь к файлу в переменную окружения TODO_FILE через os.environ.get.

Итог курса

За 10 глав мы прошли путь от установки Python до самостоятельного CLI-приложения. Вы изучили:

  • синтаксис, типы данных и операторы;
  • управляющие конструкции — условия и циклы;
  • коллекции — списки, кортежи, словари, множества;
  • функции с параметрами, *args/**kwargs и аннотациями типов;
  • работу с файлами и обработку исключений;
  • модули, стандартную библиотеку и основы ООП;
  • сборку полноценного приложения с сохранением состояния.

Это прочный фундамент, на котором строится любая специализация на Python. Удачи в дальнейших проектах — и помните, что лучший способ учиться дальше — решать настоящие задачи. До встречи в следующих курсах devdao!

Содержание серии (10)
{# Звук celebration (этап 4.3):