From 564a3e1c959b623183e960c3b42ad3237e8fec34 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 29 Jul 2026 16:24:40 +0300 Subject: [PATCH] =?UTF-8?q?rework:=20lessons=201-12=20=E2=80=94=20one=20co?= =?UTF-8?q?ncept=20per=20lesson=20(style=20of=2013-25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New app/lessons_1_12.py: 12 reworked lessons, 1 concept each, 3 theory cards (concept / deepening / typical mistake), choice+term+code questions with reference solutions - content.py: import LESSONS_1_12, exams updated to new question ids - tests: 1-12 identity/style/reference/example-exec tests (mirroring 13-25), prereq test for lessons 4-6 updated to new ids - docs/CURRICULUM.md: rows 1 and 3 match reworked content pytest 42/42, ruff check+format OK, pyright 0 errors --- app/content.py | 722 ++---------------------------------- app/lessons_1_12.py | 782 +++++++++++++++++++++++++++++++++++++++ docs/CURRICULUM.md | 4 +- tests/test_api.py | 18 +- tests/test_curriculum.py | 87 ++++- 5 files changed, 901 insertions(+), 712 deletions(-) create mode 100644 app/lessons_1_12.py diff --git a/app/content.py b/app/content.py index e77787b..70eab4b 100644 --- a/app/content.py +++ b/app/content.py @@ -1,82 +1,16 @@ -"""Русский учебный план: 12 уроков, практика и контрольные точки.""" +"""Русский учебный план: 120 уроков, практика и контрольные точки.""" from __future__ import annotations from app.extended_curriculum import EXTRA_EXAMS, EXTRA_LESSONS, EXTRA_MODULES +from app.lessons_1_12 import LESSONS_1_12 from app.lessons_13_25 import LESSONS_13_25 - -def theory(title: str, text: str, example: str, tip: str = "") -> dict: - return {"title": title, "text": text, "example": example, "tip": tip} - - -def choice( - question_id: str, prompt: str, options: list[str], answer: str, explanation: str -) -> dict: - return { - "id": question_id, - "kind": "choice", - "prompt": prompt, - "options": options, - "answer": answer, - "explanation": explanation, - } - - -def text(question_id: str, prompt: str, answers: list[str], explanation: str) -> dict: - return { - "id": question_id, - "kind": "input", - "prompt": prompt, - "answers": answers, - "placeholder": "Введите ответ", - "explanation": explanation, - } - - -def code( - question_id: str, prompt: str, starter: str, tests: list[dict], explanation: str, hint: str -) -> dict: - return { - "id": question_id, - "kind": "code", - "prompt": prompt, - "starter": starter, - "tests": tests, - "explanation": explanation, - "hint": hint, - } - - -def lesson( - lesson_id: str, - module_id: str, - order: int, - title: str, - subtitle: str, - duration: int, - xp: int, - theory_cards: list[dict], - questions: list[dict], -) -> dict: - return { - "id": lesson_id, - "module_id": module_id, - "order": order, - "title": title, - "subtitle": subtitle, - "duration": duration, - "xp": xp, - "theory": theory_cards, - "questions": questions, - } - - MODULES = [ { "id": "start", "title": "Старт: говорим с Python", - "description": "Первая программа, переменные, типы и ввод данных.", + "description": "Первая программа, переменные и f-строки.", "color": "mint", "icon": "🌱", }, @@ -103,621 +37,7 @@ def lesson( }, ] - -LESSONS = [ - lesson( - "hello", - "start", - 1, - "Знакомство с Python", - "Запускаем первую программу", - 7, - 20, - [ - theory( - "Программа — точная инструкция", - "Python выполняет команды сверху вниз. Одна строка обычно описывает одно действие.", - "print('Привет, мир!')", - "После print обязательно ставь круглые скобки.", - ), - theory( - "Текст любит кавычки", - "Текст в одинарных или двойных кавычках называется строкой (str).", - 'print("Я учу Python")', - ), - theory( - "Ошибки — это подсказки", - "Если что-то не работает, прочитай последнюю строку сообщения: в ней указан тип ошибки и место.", - "print('Готово!')\nprint('Следующий шаг')", - ), - ], - [ - choice( - "hello-print", - "Какая команда выводит текст на экран?", - ["show('Привет')", "print('Привет')", "output('Привет')"], - "print('Привет')", - "В Python за вывод на экран отвечает функция print().", - ), - text( - "hello-string", - 'Как называется значение `"Привет"` в Python?', - ["строка", "string"], - "Текст в кавычках — это строка (string).", - ), - code( - "hello-code", - "Выведи на экран фразу: `Я начинаю путь в Python!`", - "# Напиши одну команду\n", - [{"kind": "stdout", "expected": "Я начинаю путь в Python!"}], - "Отлично! Твоя первая программа уже общается с пользователем.", - "Используй print(), а фразу заключи в кавычки.", - ), - ], - ), - lesson( - "variables", - "start", - 2, - "Переменные и типы", - "Дадим значениям имена", - 9, - 25, - [ - theory( - "Переменная — подпись на коробке", - "Оператор = сохраняет значение в переменной. Слева имя, справа — значение.", - "score = 10\nname = 'Маша'", - "Имя пишут без пробелов, часто в snake_case: total_score.", - ), - theory( - "У значений есть тип", - "Целые числа — int, дробные — float, текст — str, правда/ложь — bool.", - "age = 18\nprice = 19.9\nis_ready = True", - ), - theory( - "f-строки собирают текст", - "Поставь f перед строкой и помести переменную в фигурные скобки.", - "name = 'Лена'\nprint(f'Привет, {name}!')", - ), - ], - [ - choice( - "var-type", - "Какой тип у значения `False`?", - ["str", "bool", "int"], - "bool", - "False и True — логические значения типа bool.", - ), - text( - "var-value", - "После `coins = 4`, чему равно `coins + 3`?", - ["7"], - "Python подставляет значение переменной: 4 + 3 = 7.", - ), - code( - "var-code", - "Создай `city = 'Казань'` и выведи `Город: Казань` через f-строку.", - "city = ''\n# выведи город\n", - [{"kind": "stdout", "expected": "Город: Казань"}], - "Переменная помогла не дублировать значение и сделать код гибче.", - "Сначала присвой город, затем вызови print(f'Город: {city}').", - ), - ], - ), - lesson( - "strings-input", - "start", - 3, - "Строки и ввод", - "Получаем данные от человека", - 10, - 25, - [ - theory( - "input возвращает текст", - "Функция input ждёт ответ пользователя и всегда возвращает str — даже если введены цифры.", - "name = input('Как тебя зовут? ')\nprint(f'Рада знакомству, {name}!')", - ), - theory( - "Преобразуем типы осознанно", - "Чтобы получить число из ввода, оберни input в int() или float().", - "age = int(input('Твой возраст: '))\nprint(age + 1)", - "Преобразовывай в число, только когда ожидаешь цифры.", - ), - theory( - "Методы строк", - "lower() меняет регистр, strip() убирает пробелы по краям, len() считает символы.", - "word = ' Python '\nprint(word.strip().lower())", - ), - ], - [ - choice( - "input-type", - "Какой тип вернёт `input()` без преобразования?", - ["int", "str", "bool"], - "str", - "Даже введённое 42 сначала будет строкой '42'.", - ), - text("string-len", "Чему равно `len('кот')`?", ["3"], "В слове «кот» три символа."), - code( - "string-code", - "Создай `word = 'python'` и выведи его длину.", - "word = 'python'\n# выведи длину слова\n", - [{"kind": "stdout", "expected": "6"}], - "len() возвращает количество символов в строке.", - "Передай word в len(), а результат — в print().", - ), - ], - ), - lesson( - "conditions", - "logic", - 4, - "Условия: if и else", - "Учимся принимать решения", - 12, - 30, - [ - theory( - "if проверяет условие", - "Если условие истинно, Python выполняет вложенный блок. Отступ в четыре пробела — часть синтаксиса.", - "temperature = 22\nif temperature > 20:\n print('Можно без куртки')", - "После условия ставится двоеточие.", - ), - theory( - "else — запасной сценарий", - "Когда условие ложно, срабатывает else. Для нескольких вариантов есть elif.", - "if score >= 80:\n print('Отлично')\nelse:\n print('Продолжай')", - ), - theory( - "Сравниваем, а не присваиваем", - "== сравнивает значения, != означает «не равно», >= — «больше или равно».", - "if password == 'python':\n print('Вход выполнен')", - ), - ], - [ - choice( - "if-equality", - "Каким оператором сравнивают два значения?", - ["=", "==", ">="], - "==", - "Один знак = присваивает, два знака == сравнивают.", - ), - text( - "if-result", - "Что выведет этот код?\n\nif 3 > 5:\n print('да')\nelse:\n print('нет')", - ["нет"], - "3 не больше 5, поэтому выполняется ветка else.", - ), - code( - "if-code", - "Дано `temperature = 22`. Если температура >= 25, выведи `Надень шорты`, иначе — `Возьми куртку`.", - "temperature = 22\n# твой код\n", - [{"kind": "stdout", "expected": "Возьми куртку"}], - "Условие выбирает одежду по температуре.", - "Проверь temperature >= 25: в одной ветке выведи про шорты, в другой — про куртку.", - ), - ], - ), - lesson( - "for-loop", - "logic", - 5, - "Цикл for", - "Повторяем действия без копипаста", - 11, - 30, - [ - theory( - "for проходит по элементам", - "Цикл for по очереди берёт элементы из строки, списка или диапазона.", - "for letter in 'код':\n print(letter)", - ), - theory( - "range создаёт числа", - "range(5) выдаёт числа от 0 до 4: верхняя граница не включается.", - "for number in range(1, 4):\n print(number) # 1, 2, 3", - ), - theory( - "Накапливаем результат", - "Создай переменную-накопитель до цикла и меняй её на каждом шаге.", - "total = 0\nfor number in range(1, 4):\n total += number", - ), - ], - [ - choice( - "for-range", - "Сколько раз выполнится тело `for _ in range(4)`?", - ["3", "4", "5"], - "4", - "range(4) содержит 0, 1, 2 и 3 — четыре значения.", - ), - text("for-sum", "Чему равна сумма чисел в `range(1, 4)`?", ["6"], "Это 1 + 2 + 3 = 6."), - code( - "for-code", - "Выведи числа от 1 до 5, каждое с новой строки.", - "# Напиши цикл\n", - [{"kind": "stdout", "expected": "1\n2\n3\n4\n5"}], - "Цикл for и range позволяют вывести числа без повторения print().", - "Пройди по range(1, 6) и выведи каждое число через print().", - ), - ], - ), - lesson( - "while-loop", - "logic", - 6, - "Цикл while", - "Повторяем, пока выполняется условие", - 12, - 30, - [ - theory( - "while проверяет условие", - "Тело цикла выполняется, пока условие истинно. Важно менять переменную условия.", - "count = 3\nwhile count > 0:\n print(count)\n count -= 1", - "Если count не уменьшать, получится бесконечный цикл.", - ), - theory( - "break завершает цикл", - "break нужен, когда нужный ответ найден или работу следует прекратить раньше.", - "while True:\n command = 'стоп'\n if command == 'стоп':\n break", - ), - theory( - "Выбираем цикл", - "for удобен, когда известны элементы. while — когда важен момент, в который условие станет ложным.", - "attempts = 0\nwhile attempts < 3:\n attempts += 1", - ), - ], - [ - choice( - "while-infinite", - "Что чаще всего вызывает бесконечный while-цикл?", - ["Условие никогда не меняется", "В цикле есть print", "Есть переменная"], - "Условие никогда не меняется", - "Переменная условия должна приближать цикл к завершению.", - ), - text( - "while-count", - "Сколько чисел выведет цикл: count = 2, while count > 0, count -= 1?", - ["2"], - "Будут выведены 2 и 1, затем условие станет ложным.", - ), - code( - "while-code", - "Дано `start = 5`. Выведи обратный отсчёт до 1, каждое число с новой строки, затем `Пуск!`.", - "start = 5\n# твой код\n", - [{"kind": "stdout", "expected": "5\n4\n3\n2\n1\nПуск!"}], - "while хорош, когда счётчик меняется до достижения границы.", - "Пока start больше 0, выводи его и уменьшай на 1. После цикла выведи «Пуск!».", - ), - ], - ), - lesson( - "functions", - "structures", - 7, - "Функции", - "Убираем повторения из кода", - 13, - 35, - [ - theory( - "Функция упаковывает действие", - "def создаёт функцию, а параметры принимают данные снаружи.", - "def greet(name):\n print(f'Привет, {name}!')", - ), - theory( - "return отдаёт результат", - "print показывает значение человеку, return возвращает его в программу для дальнейшей работы.", - "def double(number):\n return number * 2", - ), - theory( - "Имя функции — обещание", - "Хорошее имя отвечает на вопрос: что делает функция? Используй глагол или понятное действие.", - "def calculate_discount(price):\n return price * 0.9", - ), - ], - [ - choice( - "func-return", - "Что позволяет использовать результат функции в выражении?", - ["print", "return", "def"], - "return", - "return передаёт значение туда, где функцию вызвали.", - ), - text( - "func-call", - "Чему равно `triple(4)`, если функция возвращает number * 3?", - ["12"], - "Функция умножает переданное 4 на 3.", - ), - code( - "func-code", - "Напиши `greeting(name)`, возвращающую `Привет, !`.", - "def greeting(name):\n # твой код\n pass\n", - [ - {"kind": "call", "call": "greeting('Аня')", "expected": "Привет, Аня!"}, - {"kind": "call", "call": "greeting('Илья')", "expected": "Привет, Илья!"}, - ], - "Параметр делает одну функцию полезной для разных имён.", - "Верни f-строку, не печатай её: здесь нужен return.", - ), - ], - ), - lesson( - "lists", - "structures", - 8, - "Списки", - "Храним несколько значений", - 13, - 35, - [ - theory( - "Список хранит порядок", - "Элементы списка записывают в квадратных скобках через запятую.", - "fruits = ['яблоко', 'груша', 'слива']", - ), - theory( - "Индексы начинаются с нуля", - "Первый элемент — [0], второй — [1]. Индекс -1 берёт элемент с конца.", - "colors = ['красный', 'синий']\nprint(colors[0])", - ), - theory( - "Методы меняют список", - "append добавляет элемент в конец, pop удаляет и возвращает, len считает размер.", - "tasks = ['код']\ntasks.append('отдых')", - ), - ], - [ - choice( - "list-index", - "Как получить первый элемент списка `items`?", - ["items[1]", "items[0]", "items.first"], - "items[0]", - "Индексация в Python начинается с нуля.", - ), - text("list-length", "Чему равно `len([10, 20, 30])`?", ["3"], "В списке три элемента."), - code( - "list-code", - "Напиши `last_item(items)`, которая возвращает последний элемент списка.", - "def last_item(items):\n # твой код\n pass\n", - [ - {"kind": "call", "call": "last_item(['a', 'b'])", "expected": "b"}, - {"kind": "call", "call": "last_item([3, 7, 9])", "expected": 9}, - ], - "Отрицательный индекс достаёт последний элемент независимо от длины списка.", - "Используй индекс -1.", - ), - ], - ), - lesson( - "dicts-sets", - "structures", - 9, - "Словари и множества", - "Быстро находим данные и убираем повторы", - 14, - 35, - [ - theory( - "Словарь хранит пары", - "По ключу можно быстро найти нужное значение. Ключи часто делают строками.", - "user = {'name': 'Саша', 'level': 3}\nprint(user['name'])", - ), - theory( - "get безопаснее", - "dict.get('ключ', запасное) не выдаст ошибку, если ключ отсутствует.", - "theme = settings.get('theme', 'light')", - ), - theory( - "Множество хранит уникальное", - "set автоматически удаляет дубликаты; порядок его элементов не гарантирован.", - "tags = {'python', 'api', 'python'}\nprint(len(tags)) # 2", - ), - ], - [ - choice( - "dict-key", - "Как получить имя из `user = {'name': 'Оля'}`?", - ["user.name", "user['name']", "user('name')"], - "user['name']", - "Словарь получает значение по ключу в квадратных скобках.", - ), - text( - "set-unique", - "Сколько элементов в `set([1, 1, 2, 3, 3])`?", - ["3"], - "Множество оставит уникальные 1, 2 и 3.", - ), - code( - "dict-code", - "Напиши `get_level(profile)`: верни level, а если ключа нет — 1.", - "def get_level(profile):\n # твой код\n pass\n", - [ - { - "kind": "call", - "call": "get_level({'name': 'Аня', 'level': 4})", - "expected": 4, - }, - {"kind": "call", "call": "get_level({'name': 'Аня'})", "expected": 1}, - ], - "get позволяет задать понятное значение по умолчанию.", - "Вспомни метод словаря .get('level', 1).", - ), - ], - ), - lesson( - "files", - "realworld", - 10, - "Файлы", - "Сохраняем данные между запусками", - 12, - 35, - [ - theory( - "Файл открывают через with", - "with сам корректно закроет файл, даже если внутри произойдёт ошибка.", - "with open('notes.txt', encoding='utf-8') as file:\n text = file.read()", - ), - theory( - "Режимы чтения и записи", - "'r' читает, 'w' перезаписывает, 'a' дописывает. Для русского текста указывай utf-8.", - "with open('notes.txt', 'w', encoding='utf-8') as file:\n file.write('Первая заметка')", - ), - theory( - "Работаем построчно", - "Файл можно перебирать в цикле — не нужно загружать всё содержимое сразу.", - "for line in file:\n print(line.strip())", - ), - ], - [ - choice( - "file-mode", - "Какой режим открывает файл для добавления в конец?", - ["'r'", "'w'", "'a'"], - "'a'", - "Режим append ('a') дописывает текст, не стирая старое содержимое.", - ), - text( - "file-with", - "Какое ключевое слово помогает автоматически закрыть файл?", - ["with"], - "with управляет ресурсом и закроет файл после блока.", - ), - code( - "file-code", - "Напиши `format_note(title)`, возвращающую `Заметка: `.", - "def format_note(title):\n # твой код\n pass\n", - [ - { - "kind": "call", - "call": "format_note('Список дел')", - "expected": "Заметка: Список дел", - } - ], - "Перед записью в файл полезно подготовить текст в отдельной функции.", - "Верни f-строку с префиксом «Заметка: ».", - ), - ], - ), - lesson( - "exceptions", - "realworld", - 11, - "Исключения", - "Делаем программу устойчивой", - 13, - 40, - [ - theory( - "Исключение — сигнал", - "Например, int('кот') вызывает ValueError: строку нельзя превратить в число.", - "number = int('42')", - ), - theory( - "try/except обрабатывает ожидаемое", - "В try помещают рискованный код, а в except — понятный запасной сценарий.", - "try:\n age = int('не число')\nexcept ValueError:\n print('Нужны цифры')", - "Лови конкретные ошибки, чтобы не скрыть настоящий баг.", - ), - theory( - "Проверка до ошибки", - "Граничные случаи часто понятнее проверить заранее, чем использовать исключение.", - "if text.strip():\n print('Есть текст')", - ), - ], - [ - choice( - "except-error", - "Какую ошибку вызовет `int('abc')`?", - ["TypeError", "ValueError", "KeyError"], - "ValueError", - "Тип данных подходит, но само значение невозможно преобразовать в число — ValueError.", - ), - text( - "try-keyword", - "Какое ключевое слово начинает блок, где может возникнуть ошибка?", - ["try"], - "Рискованная операция начинается внутри try.", - ), - code( - "exception-code", - "Напиши `safe_divide(left, right)`. При right = 0 верни `Нельзя делить на ноль`, иначе — частное.", - "def safe_divide(left, right):\n # твой код\n pass\n", - [ - {"kind": "call", "call": "safe_divide(8, 2)", "expected": 4.0}, - { - "kind": "call", - "call": "safe_divide(8, 0)", - "expected": "Нельзя делить на ноль", - }, - ], - "Проверка граничного случая до деления делает поведение функции понятным.", - "Сначала проверь right == 0, иначе выполни left / right.", - ), - ], - ), - lesson( - "classes", - "realworld", - 12, - "Классы и объекты", - "Описываем собственные сущности", - 15, - 45, - [ - theory( - "Класс — чертёж", - "Класс описывает общие данные и действия. Объект — конкретный экземпляр этого чертежа.", - "class Player:\n pass\n\nhero = Player()", - ), - theory( - "__init__ задаёт состояние", - "Метод __init__ запускается при создании объекта. self — это сам объект.", - "class Player:\n def __init__(self, name):\n self.name = name", - ), - theory( - "Методы описывают поведение", - "Метод получает self первым параметром и может обращаться к данным объекта.", - "class Player:\n def level_up(self):\n self.level += 1", - ), - ], - [ - choice( - "class-blueprint", - "Что точнее всего описывает класс?", - ["Конкретный объект", "Чертёж для объектов", "Цикл"], - "Чертёж для объектов", - "По одному классу можно создать много объектов с разными данными.", - ), - text( - "class-init", - "Как называется метод, который запускается при создании объекта?", - ["__init__", "init"], - "__init__ задаёт начальное состояние объекта.", - ), - code( - "class-code", - "Создай класс `Badge` с методом `label(self, name)`, возвращающим `Награда: <name>`.", - "class Badge:\n def label(self, name):\n # твой код\n pass\n", - [ - { - "kind": "call", - "call": "Badge().label('Первые шаги')", - "expected": "Награда: Первые шаги", - } - ], - "Поздравляем! Теперь ты умеешь описывать собственные объекты.", - "Метод должен вернуть f-строку. self уже передан первым параметром.", - ), - ], - ), -] - +LESSONS = list(LESSONS_1_12) MODULES.extend(EXTRA_MODULES) LESSONS.extend(LESSONS_13_25) @@ -728,27 +48,45 @@ def lesson( "start": { "title": "Мини-экзамен: основы", "description": "Проверь, уверенно ли ты пишешь первые программы.", - "question_ids": ["hello-print", "var-type", "input-type", "var-code"], + "question_ids": [ + "hello-choice", + "variables-term", + "strings-input-choice", + "hello-code", + "variables-code", + ], }, "logic": { "title": "Мини-экзамен: логика", "description": "Условия и циклы в одном коротком забеге.", - "question_ids": ["if-equality", "for-range", "while-infinite", "for-code"], + "question_ids": [ + "conditions-choice", + "for-loop-choice", + "while-loop-term", + "for-loop-code", + "while-loop-code", + ], }, "structures": { "title": "Мини-экзамен: структуры", "description": "Функции, списки и словари — твой новый инструментарий.", - "question_ids": ["func-return", "list-index", "dict-key", "dict-code"], + "question_ids": [ + "functions-choice", + "lists-term", + "dicts-sets-choice", + "functions-code", + "dicts-sets-code", + ], }, "realworld": { "title": "Финальный экзамен", "description": "Закрепи навыки, которые используют в реальных программах.", "question_ids": [ - "file-mode", - "except-error", - "class-blueprint", - "exception-code", - "class-code", + "files-choice", + "exceptions-choice", + "classes-term", + "exceptions-code", + "classes-code", ], }, **EXTRA_EXAMS, diff --git a/app/lessons_1_12.py b/app/lessons_1_12.py new file mode 100644 index 0000000..6de2ff3 --- /dev/null +++ b/app/lessons_1_12.py @@ -0,0 +1,782 @@ +"""Переписанные уроки 1–12: одна концепция на урок, стиль уроков 13–25.""" + +from __future__ import annotations + +from typing import Any + + +def _card(title: str, text: str, example: str, tip: str = "") -> dict[str, str]: + return {"title": title, "text": text, "example": example, "tip": tip} + + +def _choice( + lesson_id: str, + prompt: str, + options: list[str], + answer: str, + explanation: str, +) -> dict[str, Any]: + return { + "id": f"{lesson_id}-choice", + "kind": "choice", + "prompt": prompt, + "options": options, + "answer": answer, + "explanation": explanation, + } + + +def _term( + lesson_id: str, + prompt: str, + answers: list[str], + explanation: str, +) -> dict[str, Any]: + return { + "id": f"{lesson_id}-term", + "kind": "input", + "prompt": prompt, + "answers": answers, + "placeholder": "Введите ответ", + "explanation": explanation, + } + + +def _code( + lesson_id: str, + prompt: str, + starter: str, + tests: list[dict[str, Any]], + reference: str, + explanation: str, + hint: str, +) -> dict[str, Any]: + return { + "id": f"{lesson_id}-code", + "kind": "code", + "prompt": prompt, + "starter": starter, + "tests": tests, + "reference": reference, + "explanation": explanation, + "hint": hint, + } + + +def _lesson( + lesson_id: str, + module_id: str, + order: int, + title: str, + subtitle: str, + duration: int, + theory: list[dict[str, str]], + questions: list[dict[str, Any]], +) -> dict[str, Any]: + return { + "id": lesson_id, + "module_id": module_id, + "order": order, + "title": title, + "subtitle": subtitle, + "duration": duration, + "xp": 35, + "theory": theory, + "questions": questions, + } + + +LESSONS_1_12 = [ + _lesson( + "hello", + "start", + 1, + "Знакомство с Python", + "Запускаем первую программу", + 8, + [ + _card( + "print() показывает текст", + "Команда `print()` выводит текст на экран. Текст нужно взять в кавычки: одинарные `'...'` или двойные `\"...\"`. Такой текст называется строкой. Python не меняет строку, а показывает её именно так, как она записана между кавычками. Внутри `print()` строка становится сообщением для пользователя.", + "print('Привет, Python!')\n# Вывод:\n# Привет, Python!", + ), + _card( + "Команды идут сверху вниз", + "Программа выполняет команды по порядку: сначала первую строку, потом вторую. Каждая команда `print()` выводит отдельную строку на экран. Поэтому команды удобно писать с новой строки. Так можно составить короткое приветствие или показать несколько сообщений одно за другим.", + 'print("Добро пожаловать!")\nprint("Начинаем учиться.")\n# Вывод:\n# Добро пожаловать!\n# Начинаем учиться.', + ), + _card( + "Типичная ошибка: пропущены кавычки", + "Если написать текст без кавычек, Python не поймёт, что это сообщение. Если забыть скобку, команда тоже не будет закончена. Например, в `print(Привет)` нет кавычек, а в `print('Привет'` не хватает закрывающей скобки. Проверь: у текста есть две кавычки, а у `print` — открывающая и закрывающая скобки.", + "print('Всё написано правильно')\n# Вывод:\n# Всё написано правильно", + "Сначала напиши `print()`, затем помести текст в кавычки.", + ), + ], + [ + _choice( + "hello", + "Какая команда выводит текст на экран?", + [ + "print('Привет')", + "text('Привет')", + "show('Привет')", + ], + "print('Привет')", + "`print()` показывает то, что написано внутри скобок.", + ), + _term( + "hello", + "Как называется текст в кавычках?", + ["строка", "string"], + "Текст в одинарных или двойных кавычках называется строкой.", + ), + _code( + "hello", + "Выведи фразу «Я начинаю путь в Python!».", + "# Напиши одну команду\n", + [ + {"kind": "stdout", "expected": "Я начинаю путь в Python!"}, + ], + "print('Я начинаю путь в Python!')\n", + "Верно: `print()` вывел строку на экран.", + "Возьми фразу в кавычки и передай её в `print()`.", + ), + ], + ), + _lesson( + "variables", + "start", + 2, + "Переменные", + "Даём значениям имена", + 9, + [ + _card( + "Имя хранит значение", + "Переменная помогает сохранить значение под понятным именем. В записи `score = 10` слева находится имя, а справа — значение. Знак `=` сохраняет это значение в переменной. В имени нельзя ставить пробелы; для нескольких слов используют нижнее подчёркивание: `game_score`. Переменная может хранить число или текст в кавычках.", + "player = 'Маша'\nprint(player)\n# Вывод:\n# Маша", + ), + _card( + "Переменную можно использовать", + "После присваивания можно написать имя переменной в выражении или передать его в `print()`. Python возьмёт сохранённое значение. Переменную можно и переприсвоить: новое значение заменит прежнее. Это удобно, когда счёт меняется во время игры. В выражении `score + 3` к сохранённому числу прибавляется 3.", + "score = 5\nprint(score + 3)\n# Вывод:\n# 8", + ), + _card( + "Типичная ошибка: имя ещё пустое", + "Нельзя использовать переменную раньше, чем ей присвоили значение: Python ещё не знает, что означает её имя. Также значение всегда пишут справа от `=`, а имя — слева. Запись `10 = score` неверна. Сначала создай переменную и только потом выводи её или используй в выражении.", + "score = 10\nscore = 12\nprint(score)\n# Вывод:\n# 12", + "Слева от `=` — имя, справа — значение.", + ), + ], + [ + _choice( + "variables", + "Что делает оператор `=` в записи `coins = 4`?", + [ + "Сравнивает coins и 4", + "Сохраняет 4 в переменной coins", + "Выводит 4 на экран", + ], + "Сохраняет 4 в переменной coins", + "Знак `=` присваивает переменной значение справа от него.", + ), + _term( + "variables", + "После `coins = 4`, чему равно `coins + 3`?", + ["7"], + "В переменной `coins` хранится 4, а 4 + 3 равно 7.", + ), + _code( + "variables", + "Создай `coins = 4`, затем выведи результат `coins + 3`.", + "# создай переменную coins\n# выведи результат\n", + [ + {"kind": "stdout", "expected": "7"}, + ], + "coins = 4\nprint(coins + 3)\n", + "Верно: переменная хранит 4, а `print()` показывает результат выражения.", + "Сначала присвой `coins` число 4.", + ), + ], + ), + _lesson( + "strings-input", + "start", + 3, + "f-строки", + "Вставляем значения в текст", + 9, + [ + _card( + "f-строка подставляет значение", + "f-строка — это строка с буквой `f` перед кавычкой. Внутри неё имя переменной пишут в фигурных скобках: `f'Привет, {name}!'`. Python заменит `{name}` значением переменной и соберёт готовый текст. Затем этот текст можно вывести через `print()`. Кавычки у f-строки могут быть одинарными или двойными.", + "name = 'Аня'\nprint(f'Привет, {name}!')\n# Вывод:\n# Привет, Аня!", + ), + _card( + "Вставок может быть несколько", + "В одну f-строку можно вставить несколько переменных, а ещё — простое выражение, например `{score + 1}`. Python вычислит выражение и подставит его результат в текст. Без буквы `f` фигурные скобки не работают: они останутся обычными символами строки. f-строка помогает соединить понятный текст и сохранённые значения.", + "name = 'Илья'\nscore = 4\nprint(f'{name}, у тебя {score + 1} очков')\n# Вывод:\n# Илья, у тебя 5 очков", + ), + _card( + "Типичная ошибка: забыта буква f", + "Если перед кавычками нет буквы `f`, Python не подставит значение переменной. Вместо имени на экране появятся сами фигурные скобки: `{name}`. Проверь начало строки перед открывающей кавычкой. Буква `f` нужна именно там, а переменная внутри текста должна быть записана в фигурных скобках.", + "name = 'Лена'\nprint(f'Привет, {name}!')\n# Вывод:\n# Привет, Лена!", + "Поставь `f` прямо перед первой кавычкой.", + ), + ], + [ + _choice( + "strings-input", + "Что выведет `print(f'Привет, {name}!')`, если `name = 'Аня'`?", + [ + "Привет, {name}!", + "name", + "Привет, Аня!", + ], + "Привет, Аня!", + "f-строка подставляет значение переменной вместо `{name}`.", + ), + _term( + "strings-input", + "Какой символ ставят перед строкой, чтобы подставить переменную?", + ["f"], + "Буква `f` перед кавычками делает строку f-строкой.", + ), + _code( + "strings-input", + "Создай `city = 'Казань'` и выведи через f-строку «Город: Казань».", + "city = 'Казань'\n# выведи город через f-строку\n", + [ + {"kind": "stdout", "expected": "Город: Казань"}, + ], + "city = 'Казань'\nprint(f'Город: {city}')\n", + "Верно: f-строка подставила значение переменной `city` в текст.", + "Напиши `f'Город: {city}'` внутри `print()`.", + ), + ], + ), + _lesson( + "conditions", + "logic", + 4, + "Условия: if и else", + "Учимся принимать решения", + 11, + [ + _card( + "if выбирает действие", + "Условие позволяет программе выбрать действие. После `if` пишут проверку и двоеточие. Если проверка истинна, Python выполняет блок ниже. Строки блока начинаются с отступа в 4 пробела — он показывает, какие команды относятся к `if`. Без двоеточия или отступа Python не сможет понять структуру условия.", + "score = 8\nif score > 5:\n print('Уровень пройден')\n# Вывод:\n# Уровень пройден", + ), + _card( + "else — другой сценарий", + "Блок `else` выполняется, когда условие в `if` ложно. Сравнение `==` проверяет, равны ли два значения, и даёт `True` или `False`. Также можно сравнивать `!=`, `>`, `<`, `>=` и `<=`. У `else` тоже есть двоеточие, а его команда пишется с отступом. Так программа выбирает один из двух вариантов.", + "name = 'Аня'\nif name == 'Маша':\n print('Привет, Маша')\nelse:\n print('Привет, Аня')\n# Вывод:\n# Привет, Аня", + ), + _card( + "Типичная ошибка: `=` вместо `==`", + "В условии `=` не сравнивает значения: этот знак используют для присваивания переменной. Для проверки равенства нужен `==`. Ещё две частые ошибки — пропущенное двоеточие после `if` или `else` и команда без отступа. Внимательно проверь знаки и четыре пробела перед командой внутри каждого блока.", + "temperature = 20\nif temperature >= 25:\n print('Тепло')\nelse:\n print('Прохладно')\n# Вывод:\n# Прохладно", + "Для проверки равенства всегда пиши два знака: `==`.", + ), + ], + [ + _choice( + "conditions", + "Каким оператором сравнивают два значения?", + [ + "==", + "=", + "=>", + ], + "==", + "`==` проверяет равенство двух значений.", + ), + _term( + "conditions", + "Что выведет этот код?\n\nif 3 > 5:\n print('да')\nelse:\n print('нет')", + ["нет"], + "Условие `3 > 5` ложно, поэтому выполняется блок `else`.", + ), + _code( + "conditions", + "Дано `temperature = 22`; если температура >= 25 выведи «Надень шорты», иначе «Возьми куртку».", + "temperature = 22\n# твой код\n", + [ + {"kind": "stdout", "expected": "Возьми куртку"}, + ], + "temperature = 22\nif temperature >= 25:\n print('Надень шорты')\nelse:\n print('Возьми куртку')\n", + "Верно: 22 меньше 25, поэтому выполнен блок `else`.", + "Проверь `temperature >= 25`, затем добавь `else`.", + ), + ], + ), + _lesson( + "for-loop", + "logic", + 5, + "Цикл for", + "Повторяем действия без копипаста", + 11, + [ + _card( + "for повторяет блок", + "Цикл `for` берёт по очереди каждое число из `range()` и для каждого выполняет вложенный блок команд. После строки `for` обязательно ставят двоеточие, а команды внутри пишут с отступом в 4 пробела — так же, как в `if`. Это позволяет повторять действие без копирования одинаковых строк кода.", + "for number in range(1, 4):\n print(number)\n# Вывод:\n# 1\n# 2\n# 3", + ), + _card( + "range задаёт числа", + "`range(5)` даёт числа от 0 до 4, а `range(1, 6)` — от 1 до 5. Переменная цикла хранит текущее число на каждом шаге. Её можно вывести через `print()` или подставить в f-строку. После очередного шага Python берёт следующее число из `range()`, пока числа не закончатся.", + "for number in range(3):\n print(f'Шаг {number}')\n# Вывод:\n# Шаг 0\n# Шаг 1\n# Шаг 2", + ), + _card( + "Типичная ошибка: последняя граница", + "Верхняя граница `range()` не входит в цикл. Поэтому `range(1, 5)` даст числа 1, 2, 3 и 4, но не 5. Чтобы пройти числа от 1 до 5, нужно написать `range(1, 6)`. Полезно помнить правило: первое число входит, а последнее показывает, где цикл должен остановиться.", + "for number in range(1, 6):\n print(number)\n# Вывод:\n# 1\n# 2\n# 3\n# 4\n# 5", + "Для чисел до 5 включительно укажи 6 как верхнюю границу.", + ), + ], + [ + _choice( + "for-loop", + "Сколько раз выполнится тело цикла `for number in range(4)`?", + [ + "3", + "4", + "5", + ], + "4", + "`range(4)` содержит четыре числа: 0, 1, 2 и 3.", + ), + _term( + "for-loop", + "Чему равна сумма чисел `range(1, 4)`?", + ["6"], + "`range(1, 4)` даёт 1, 2 и 3: 1 + 2 + 3 = 6.", + ), + _code( + "for-loop", + "Выведи числа от 1 до 5, каждое с новой строки.", + "# Напиши цикл\n", + [ + {"kind": "stdout", "expected": "1\n2\n3\n4\n5"}, + ], + "for number in range(1, 6):\n print(number)\n", + "Верно: цикл прошёл числа от 1 до 5.", + "Используй `range(1, 6)` и `print(number)` внутри цикла.", + ), + ], + ), + _lesson( + "while-loop", + "logic", + 6, + "Цикл while", + "Повторяем, пока выполняется условие", + 11, + [ + _card( + "while проверяет условие", + "Цикл `while` проверяет условие перед каждым шагом. Пока условие истинно, выполняется вложенный блок с отступом в 4 пробела. Например, счётчик можно начать с 3: пока `count > 0`, вывести его и уменьшить на 1. Когда условие станет ложным, Python перейдёт к следующей команде после цикла.", + "count = 3\nwhile count > 0:\n print(count)\n count = count - 1\n# Вывод:\n# 3\n# 2\n# 1", + ), + _card( + "Счётчик должен меняться", + "Переменная в условии должна изменяться внутри цикла, иначе условие останется таким же. В обратном отсчёте после каждого вывода уменьшают счётчик: `count = count - 1`. Можно выводить текущее значение в f-строке. Когда счётчик дойдёт до 0, проверка `count > 0` даст ложный результат и цикл остановится.", + "count = 2\nwhile count > 0:\n print(f'Осталось: {count}')\n count = count - 1\n# Вывод:\n# Осталось: 2\n# Осталось: 1", + ), + _card( + "Типичная ошибка: бесконечный цикл", + "Бесконечный цикл появляется, если забыть изменить счётчик. Например, при `count = 3` условие `count > 0` всегда будет истинным, пока значение не уменьшается. Программа будет повторять один и тот же блок. После `print()` проверь, что счётчик получает новое значение и когда-нибудь сделает условие ложным.", + "count = 1\nwhile count > 0:\n print('Последний шаг')\n count = count - 1\n# Вывод:\n# Последний шаг", + "В цикле со счётчиком обычно нужна строка `count = count - 1`.", + ), + ], + [ + _choice( + "while-loop", + "Что чаще всего вызывает бесконечный цикл `while`?", + [ + "Условие никогда не меняется", + "Внутри есть `print()`", + "У цикла есть отступ", + ], + "Условие никогда не меняется", + "Если условие остаётся истинным, цикл не получает причины остановиться.", + ), + _term( + "while-loop", + "Сколько чисел выведет цикл: `count = 2`, `while count > 0`, вывод, `count = count - 1`?", + ["2"], + "Будут выведены 2 и 1, затем `count` станет равен 0.", + ), + _code( + "while-loop", + "Дано `start = 5`; выведи обратный отсчёт до 1, каждое число с новой строки, затем «Пуск!».", + "start = 5\n# твой код\n", + [ + {"kind": "stdout", "expected": "5\n4\n3\n2\n1\nПуск!"}, + ], + "start = 5\nwhile start > 0:\n print(start)\n start = start - 1\nprint('Пуск!')\n", + "Верно: после обратного отсчёта условие стало ложным, затем вывелось сообщение.", + "Уменьшай `start` на 1 внутри цикла, а «Пуск!» напиши после него.", + ), + ], + ), + _lesson( + "functions", + "structures", + 7, + "Функции", + "Вход и результат", + 12, + [ + _card( + "Функция получает вход", + "Функция — это именованный блок кода. Строка `def` создаёт функцию, её имя помогает понять задачу, а параметр в скобках принимает входное значение. После заголовка ставят двоеточие, а команды функции пишут с отступом. При вызове `greet('Аня')` значение `'Аня'` попадает в параметр функции.", + "def greet(name):\n print(f'Привет, {name}!')\n\ngreet('Аня')\n# Вывод:\n# Привет, Аня!", + ), + _card( + "return отдаёт результат", + "`return` возвращает результат туда, где вызвали функцию. Возвращённое значение можно сохранить в переменную, вывести или использовать дальше в выражении. Функция с параметром может давать разный результат для разных входных значений. f-строка внутри функции помогает собрать текст из значения параметра.", + "def greeting(name):\n return f'Привет, {name}!'\n\nmessage = greeting('Илья')\nprint(message)\n# Вывод:\n# Привет, Илья!", + ), + _card( + "Типичная ошибка: print вместо return", + "`print()` показывает текст на экране, но не возвращает его из функции. Если нужно сохранить результат в переменную или использовать его дальше, нужен `return`. Функция без `return` отдаёт пустоту (`None`). Вывод внутри функции полезен для сообщения, а `return` — для результата работы функции.", + "def triple(number):\n return number * 3\n\nprint(triple(4))\n# Вывод:\n# 12", + "Если результат нужен после вызова, начни строку с `return`.", + ), + ], + [ + _choice( + "functions", + "Что позволяет использовать результат функции в выражении?", + [ + "print", + "return", + "def", + ], + "return", + "`return` передаёт результат туда, где функцию вызвали.", + ), + _term( + "functions", + "Если функция `triple` возвращает `number * 3`, чему равно `triple(4)`?", + ["12"], + "При вызове параметр `number` равен 4, а 4 * 3 равно 12.", + ), + _code( + "functions", + "Напиши `greeting(name)`, возвращающую `f'Привет, {name}!'`.", + "def greeting(name):\n # твой код\n pass\n", + [ + {"kind": "call", "call": "greeting('Аня')", "expected": "Привет, Аня!"}, + {"kind": "call", "call": "greeting('Илья')", "expected": "Привет, Илья!"}, + ], + "def greeting(name):\n return f'Привет, {name}!'\n", + "Верно: функция возвращает f-строку с переданным именем.", + "Перед f-строкой напиши `return`.", + ), + ], + ), + _lesson( + "lists", + "structures", + 8, + "Списки", + "Несколько значений под одним именем", + 12, + [ + _card( + "Список хранит несколько значений", + "Список хранит несколько значений под одним именем и сохраняет их порядок. Элементы списка записывают в квадратных скобках и разделяют запятыми. Например, `fruits = ['яблоко', 'груша', 'слива']`. Список можно передать в `print()`, чтобы увидеть все его элементы. Один список может хранить строки или числа.", + "fruits = ['яблоко', 'груша', 'слива']\nprint(fruits)\n# Вывод:\n# ['яблоко', 'груша', 'слива']", + ), + _card( + "Индекс показывает элемент", + "Чтобы получить элемент списка, после имени пишут его индекс в квадратных скобках. Индексы начинаются с нуля: `fruits[0]` — первый элемент, `fruits[1]` — второй. Индекс `-1` всегда означает последний элемент списка. Полученный элемент можно вывести через `print()` или вставить в f-строку.", + "fruits = ['яблоко', 'груша', 'слива']\nprint(f'Последний фрукт: {fruits[-1]}')\n# Вывод:\n# Последний фрукт: слива", + ), + _card( + "Типичная ошибка: первый индекс — ноль", + "Новичок часто пишет `items[1]`, когда хочет первый элемент. Но у списка нумерация начинается с нуля, поэтому первый элемент — `items[0]`. Индекс 1 указывает уже на второй элемент. Перед выбором элемента мысленно посчитай: ноль — первый, один — второй, а `-1` — последний.", + "items = ['ручка', 'тетрадь']\nprint(items[0])\n# Вывод:\n# ручка", + "Первый элемент списка всегда находится по индексу `0`.", + ), + ], + [ + _choice( + "lists", + "Как получить первый элемент списка `items`?", + [ + "items[1]", + "items[0]", + "items.first", + ], + "items[0]", + "Индексы списка начинаются с нуля.", + ), + _term( + "lists", + "Какой индекс у последнего элемента любого списка?", + ["-1"], + "Индекс `-1` обращается к последнему элементу списка.", + ), + _code( + "lists", + "Напиши `last_item(items)`, возвращающую последний элемент списка.", + "def last_item(items):\n # твой код\n pass\n", + [ + {"kind": "call", "call": "last_item(['a', 'b'])", "expected": "b"}, + {"kind": "call", "call": "last_item([3, 7, 9])", "expected": 9}, + ], + "def last_item(items):\n return items[-1]\n", + "Верно: `items[-1]` возвращает последний элемент списка.", + "Используй индекс `-1` после имени параметра `items`.", + ), + ], + ), + _lesson( + "dicts-sets", + "structures", + 9, + "Словари", + "Находим значение по ключу", + 12, + [ + _card( + "Словарь хранит пары", + "Словарь хранит данные парами: ключ и значение. Ключ — это понятная подпись, а значение — содержимое по этой подписи. Пары записывают в фигурных скобках через двоеточие. Например, в `user = {'name': 'Саша', 'level': 3}` ключ `'name'` связан с именем. Значение получают по ключу в квадратных скобках.", + "user = {'name': 'Саша', 'level': 3}\nprint(user['name'])\n# Вывод:\n# Саша", + ), + _card( + "get даёт запасное значение", + "Метод `.get()` получает значение по ключу безопасно. Если ключа нет, он вернёт запасное значение из второго аргумента: `settings.get('theme', 'light')`. Обращение `user['theme']` к отсутствующему ключу вызовет ошибку и остановит программу. Поэтому `.get()` удобен, когда ключ может отсутствовать.", + "settings = {'language': 'ru'}\nprint(settings.get('theme', 'light'))\n# Вывод:\n# light", + ), + _card( + "Типичная ошибка: точка вместо ключа", + "Словарь не отдаёт значение через точку: запись `user.name` неверна. Нужно указать ключ в квадратных скобках — `user['name']`, или использовать `.get('name')`. Скобки показывают Python, какую подпись искать в словаре. Проверь также кавычки вокруг текстового ключа: `'name'` — это строка.", + "user = {'name': 'Оля'}\nprint(user['name'])\n# Вывод:\n# Оля", + "Для доступа к значению укажи ключ в квадратных скобках.", + ), + ], + [ + _choice( + "dicts-sets", + "Как получить имя из `user = {'name': 'Оля'}`?", + [ + "user.name", + "user['name']", + "user('name')", + ], + "user['name']", + "Значение словаря получают по ключу в квадратных скобках.", + ), + _term( + "dicts-sets", + "Что вернёт `user.get('level', 1)`, если ключа `'level'` в `user` нет?", + ["1"], + "Если ключ не найден, `.get()` возвращает запасное значение.", + ), + _code( + "dicts-sets", + "Напиши `get_level(profile)`: верни `level`, а если ключа нет — 1.", + "def get_level(profile):\n # твой код\n pass\n", + [ + { + "kind": "call", + "call": "get_level({'name': 'Аня', 'level': 4})", + "expected": 4, + }, + {"kind": "call", "call": "get_level({'name': 'Аня'})", "expected": 1}, + ], + "def get_level(profile):\n return profile.get('level', 1)\n", + "Верно: `.get()` возвращает уровень или запасное значение 1.", + "Передай в `.get()` ключ `'level'` и запасное число 1.", + ), + ], + ), + _lesson( + "files", + "realworld", + 10, + "Файлы", + "Сохраняем данные между запусками", + 12, + [ + _card( + "with открывает и закрывает файл", + "Текстовый файл открывают через `with open(...) as file:`. Блок `with` сам закрывает файл после работы, поэтому не нужно делать это отдельной командой. Метод `file.read()` возвращает весь текст из файла. Для русского текста указывают `encoding='utf-8'`, чтобы Python правильно прочитал буквы.", + "with open('notes.txt', 'w', encoding='utf-8') as file:\n file.write('Купить хлеб')\nwith open('notes.txt', encoding='utf-8') as file:\n print(file.read())\n# Вывод:\n# Купить хлеб", + ), + _card( + "Режим задаёт действие", + "Без указанного режима файл открывается для чтения, то есть в режиме `'r'`. Режим `'w'` перезаписывает файл, а `file.write('текст')` сохраняет в него текст. Для дописывания в конец существует режим `'a'`. Перед записью подумай, нужно ли заменить старое содержимое: `'w'` очищает файл перед сохранением.", + "with open('message.txt', 'w', encoding='utf-8') as file:\n file.write('Заметка готова')\nwith open('message.txt', encoding='utf-8') as file:\n print(file.read())\n# Вывод:\n# Заметка готова", + ), + _card( + "Типичная ошибка: забыта кодировка", + "Без `encoding='utf-8'` русский текст на некоторых компьютерах может превратиться в нечитаемые символы. Всегда указывай эту кодировку и при записи, и при чтении файла. Так Python одинаково сохранит буквы и затем правильно их восстановит. Кодировку пишут внутри `open()` рядом с именем файла и режимом.", + "with open('russian.txt', 'w', encoding='utf-8') as file:\n file.write('Привет, мир!')\nwith open('russian.txt', encoding='utf-8') as file:\n print(file.read())\n# Вывод:\n# Привет, мир!", + "Для русского текста добавь `encoding='utf-8'` в каждый вызов `open()`.", + ), + ], + [ + _choice( + "files", + "Какое ключевое слово открывает файл и автоматически закрывает его?", + [ + "with", + "def", + "for", + ], + "with", + "Блок `with` закрывает файл после завершения вложенного блока.", + ), + _term( + "files", + "В какой кодировке стоит открывать файл с русским текстом?", + ["utf-8", "ютф-8"], + "`utf-8` правильно сохраняет и читает русский текст.", + ), + _code( + "files", + "Напиши `format_note(title)`, возвращающую `f'Заметка: {title}'`.", + "def format_note(title):\n # твой код\n pass\n", + [ + { + "kind": "call", + "call": "format_note('Список дел')", + "expected": "Заметка: Список дел", + }, + ], + "def format_note(title):\n return f'Заметка: {title}'\n", + "Верно: функция подготовила текст заметки перед записью в файл.", + "Верни f-строку с `title` в фигурных скобках.", + ), + ], + ), + _lesson( + "exceptions", + "realworld", + 11, + "Исключения", + "Делаем программу устойчивой", + 12, + [ + _card( + "Ошибка имеет тип", + "Ошибка во время работы программы называется исключением. Она может остановить программу, если её не обработать. У исключения есть тип: например, `int('кот')` вызывает `ValueError`, потому что текст нельзя превратить в число. Зная тип ошибки, можно подготовить понятный запасной сценарий вместо неожиданной остановки.", + "try:\n number = int('кот')\n print(number)\nexcept ValueError:\n print('Нужно ввести число')\n# Вывод:\n# Нужно ввести число", + ), + _card( + "try и except продолжают работу", + "В блок `try` помещают строку, где может возникнуть ожидаемая ошибка. После `except` указывают её тип и пишут запасной сценарий с отступом. Если ошибка случится, Python выполнит `except` и продолжит программу после него. Если ошибки нет, выполняется код из `try`, а блок `except` пропускается.", + "try:\n age = int('семь')\n print(age)\nexcept ValueError:\n print('Возраст нужно записать числом')\nprint('Программа работает дальше')\n# Вывод:\n# Возраст нужно записать числом\n# Программа работает дальше", + ), + _card( + "Типичная ошибка: слишком широкий except", + "Запись `except` без типа может спрятать настоящую ошибку в программе. Лучше ловить конкретный ожидаемый тип, например `ValueError`. А простую проверку иногда понятнее сделать заранее через `if`: перед делением проверить, что делитель не равен нулю. Так программа избегает ошибки заранее.", + "number = 0\nif number == 0:\n print('На ноль делить нельзя')\nelse:\n print(10 / number)\n# Вывод:\n# На ноль делить нельзя", + "Лови конкретную ошибку, а простые условия проверяй через `if`.", + ), + ], + [ + _choice( + "exceptions", + "Какую ошибку вызовет `int('abc')`?", + [ + "TypeError", + "ValueError", + "KeyError", + ], + "ValueError", + "Текст `'abc'` нельзя превратить в число, поэтому возникает `ValueError`.", + ), + _term( + "exceptions", + "Какое ключевое слово начинает блок, где может возникнуть ошибка?", + ["try"], + "Рискованный код помещают в блок `try`.", + ), + _code( + "exceptions", + "Напиши `safe_divide(left, right)`: если `right` равен 0 — верни 'Нельзя делить на ноль', иначе результат деления.", + "def safe_divide(left, right):\n # твой код\n pass\n", + [ + {"kind": "call", "call": "safe_divide(8, 2)", "expected": 4.0}, + { + "kind": "call", + "call": "safe_divide(8, 0)", + "expected": "Нельзя делить на ноль", + }, + ], + "def safe_divide(left, right):\n if right == 0:\n return 'Нельзя делить на ноль'\n return left / right\n", + "Верно: функция проверяет делитель до выполнения деления.", + "Сначала сравни `right` с 0 через `if`.", + ), + ], + ), + _lesson( + "classes", + "realworld", + 12, + "Классы и объекты", + "Описываем собственные сущности", + 13, + [ + _card( + "Класс — чертёж объекта", + "Класс — это чертёж, по которому создают объекты. Объект — отдельный экземпляр этого чертежа с собственными данными. Например, класс `Player` описывает игрока, а `hero = Player('Аня')` создаёт одного конкретного игрока. По одному классу можно создать много объектов с разными значениями.", + "class Player:\n def __init__(self, name):\n self.name = name\n\nhero = Player('Аня')\nprint(hero.name)\n# Вывод:\n# Аня", + ), + _card( + "__init__ сохраняет данные", + "`__init__` запускается при создании объекта. `self` означает сам создаваемый объект, а строка `self.name = name` сохраняет переданное имя в его данных. Затем `hero.name` возвращает это значение. У двух объектов свои данные: изменение или создание одного не меняет данные другого. Их удобно выводить через f-строку.", + "class Player:\n def __init__(self, name):\n self.name = name\n\nfirst = Player('Аня')\nsecond = Player('Илья')\nprint(f'{first.name} и {second.name}')\n# Вывод:\n# Аня и Илья", + ), + _card( + "Типичная ошибка: пропущен self", + "В параметрах `__init__` первым всегда пишут `self`. Без него Python не поймёт, к какому объекту сохранить данные. После `self` идут обычные параметры, например `name`. Внутри класса значение сохраняют как `self.name = name`: слева данные объекта, справа значение, переданное при создании объекта.", + "class Badge:\n def __init__(self, name):\n self.name = name\n\nbadge = Badge('Первые шаги')\nprint(badge.name)\n# Вывод:\n# Первые шаги", + "Начинай параметры `__init__` с `self`.", + ), + ], + [ + _choice( + "classes", + "Что точнее всего описывает класс?", + [ + "Конкретный объект", + "Чертёж для объектов", + "Цикл", + ], + "Чертёж для объектов", + "Класс описывает, по какому шаблону создавать объекты.", + ), + _term( + "classes", + "Как называется метод, который запускается при создании объекта?", + ["__init__", "init"], + "`__init__` получает начальные данные нового объекта.", + ), + _code( + "classes", + "Создай класс `Badge` с `__init__(self, name)`, сохраняющим `name` в `self.name`.", + "class Badge:\n def __init__(self, name):\n # твой код\n pass\n", + [ + { + "kind": "call", + "call": "Badge('Первые шаги').name", + "expected": "Первые шаги", + }, + ], + "class Badge:\n def __init__(self, name):\n self.name = name\n", + "Верно: объект `Badge` сохраняет переданное имя в `self.name`.", + "Слева напиши `self.name`, а справа — параметр `name`.", + ), + ], + ), +] diff --git a/docs/CURRICULUM.md b/docs/CURRICULUM.md index b4fadf3..c4713d0 100644 --- a/docs/CURRICULUM.md +++ b/docs/CURRICULUM.md @@ -4,9 +4,9 @@ | № | Раздел | Что освоит ученик | | ---: | --- | --- | -| 1 | Старт: говорим с Python | Первый вывод, переменные, типы, ввод | +| 1 | Старт: говорим с Python | Первый вывод, переменные, f-строки | | 2 | Логика и повторения | `if`, `for`, `while` | -| 3 | Функции и данные | Функции, списки, словари, множества | +| 3 | Функции и данные | Функции, списки, словари | | 4 | Python в деле | Файлы, ошибки, первые классы | | 5 | Операторы и выражения | Арифметика, boolean-логика, точность чисел | | 6 | Строки глубже | Срезы, поиск, `split`, `join`, f-строки | diff --git a/tests/test_api.py b/tests/test_api.py index 4bad006..3660d9f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -44,8 +44,8 @@ def test_lesson_unlocking_and_progression() -> None: "/api/lessons/hello/submit", json={ "answers": [ - {"question_id": "hello-print", "answer": "print('Привет')"}, - {"question_id": "hello-string", "answer": "строка"}, + {"question_id": "hello-choice", "answer": "print('Привет')"}, + {"question_id": "hello-term", "answer": "строка"}, { "question_id": "hello-code", "answer": "print('Я начинаю путь в Python!')", @@ -56,7 +56,7 @@ def test_lesson_unlocking_and_progression() -> None: result = response.json() assert response.status_code == 200 assert result["passed"] is True - assert result["xp_gained"] == 20 + assert result["xp_gained"] == 35 assert client.get("/api/lessons/variables").status_code == 200 client.post("/api/reset") @@ -90,8 +90,8 @@ def test_lesson_requires_correct_code_for_completion() -> None: with TestClient(app) as client: client.post("/api/reset") two_without_code = [ - {"question_id": "hello-print", "answer": "print('Привет')"}, - {"question_id": "hello-string", "answer": "строка"}, + {"question_id": "hello-choice", "answer": "print('Привет')"}, + {"question_id": "hello-term", "answer": "строка"}, {"question_id": "hello-code", "answer": ""}, ] response = client.post("/api/lessons/hello/submit", json={"answers": two_without_code}) @@ -100,16 +100,16 @@ def test_lesson_requires_correct_code_for_completion() -> None: assert client.get("/api/lessons/variables").status_code == 403 code_and_one_other = [ - {"question_id": "hello-print", "answer": "print('неверно')"}, - {"question_id": "hello-string", "answer": "строка"}, + {"question_id": "hello-choice", "answer": "print('неверно')"}, + {"question_id": "hello-term", "answer": "строка"}, {"question_id": "hello-code", "answer": "print('Я начинаю путь в Python!')"}, ] response = client.post("/api/lessons/hello/submit", json={"answers": code_and_one_other}) assert response.json()["passed"] is True code_only = [ - {"question_id": "hello-print", "answer": "неверно"}, - {"question_id": "hello-string", "answer": "неверно"}, + {"question_id": "hello-choice", "answer": "неверно"}, + {"question_id": "hello-term", "answer": "неверно"}, {"question_id": "hello-code", "answer": "print('Я начинаю путь в Python!')"}, ] client.post("/api/reset") diff --git a/tests/test_curriculum.py b/tests/test_curriculum.py index 75df7bf..01026d6 100644 --- a/tests/test_curriculum.py +++ b/tests/test_curriculum.py @@ -1,3 +1,5 @@ +import contextlib +import io import re from pathlib import Path @@ -5,8 +7,24 @@ from app.content import EXAMS, LESSONS, MODULES, QUESTION_BY_ID from app.evaluator import evaluate, normalize from app.extended_curriculum import EXTRA_LESSONS, build_extended_course +from app.lessons_1_12 import LESSONS_1_12 from app.lessons_13_25 import LESSONS_13_25 +LESSONS_1_12_IDENTITY = [ + ("hello", "start", 1), + ("variables", "start", 2), + ("strings-input", "start", 3), + ("conditions", "logic", 4), + ("for-loop", "logic", 5), + ("while-loop", "logic", 6), + ("functions", "structures", 7), + ("lists", "structures", 8), + ("dicts-sets", "structures", 9), + ("files", "realworld", 10), + ("exceptions", "realworld", 11), + ("classes", "realworld", 12), +] + LESSONS_13_25_IDENTITY = [ ("operators-arithmetic", "operators", 13), ("operators-integer-division", "operators", 14), @@ -50,6 +68,53 @@ def test_every_module_has_a_valid_exam() -> None: assert all(question_id in QUESTION_BY_ID for question_id in exam["question_ids"]) +def test_lessons_1_12_are_reworked_and_keep_their_identity() -> None: + assert [ + (lesson["id"], lesson["module_id"], lesson["order"]) for lesson in LESSONS_1_12 + ] == LESSONS_1_12_IDENTITY + assert LESSONS[:12] == LESSONS_1_12 + + +def test_lessons_1_12_have_beginner_theory_and_three_questions() -> None: + for lesson in LESSONS_1_12: + cards = lesson["theory"] + assert len(cards) == 3 + assert len({card["title"] for card in cards}) == 3 + assert len({card["example"] for card in cards}) == 3 + assert 600 <= sum(len(card["text"]) for card in cards) <= 900 + assert all("# Вывод:" in card["example"] for card in cards) + assert any( + "ошиб" in (card["title"] + card["text"] + card["tip"]).casefold() for card in cards + ) + + questions = lesson["questions"] + assert [question["kind"] for question in questions] == ["choice", "input", "code"] + assert isinstance(questions[1]["answers"], list) + assert questions[1]["answers"] + assert all(isinstance(answer, str) and answer for answer in questions[1]["answers"]) + + +def test_reference_solutions_for_lessons_1_12_pass_the_runner() -> None: + for lesson in LESSONS_1_12: + code_question = lesson["questions"][2] + result = evaluate(code_question, code_question["reference"]) + assert result["correct"] is True, (lesson["order"], result) + + +def test_theory_examples_for_lessons_1_12_run_cleanly(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + for lesson in LESSONS_1_12: + for card in lesson["theory"]: + example = card["example"].split("# Вывод:")[0].strip() + expected = "\n".join( + line[2:] for line in card["example"].split("# Вывод:")[1].strip().splitlines() + ) + namespace: dict = {} + with contextlib.redirect_stdout(output := io.StringIO()): + exec(example, namespace) + assert output.getvalue().strip() == expected.strip(), (lesson["id"], card["title"]) + + def test_lessons_13_25_are_handwritten_and_keep_their_identity() -> None: assert [ (lesson["id"], lesson["module_id"], lesson["order"]) for lesson in LESSONS_13_25 @@ -85,17 +150,17 @@ def test_reference_solutions_for_lessons_13_25_pass_the_runner() -> None: def test_lessons_four_to_six_do_not_require_future_topics() -> None: - if_result = QUESTION_BY_ID["if-result"] - assert "\n" in if_result["prompt"] + conditions_term = QUESTION_BY_ID["conditions-term"] + assert "\n" in conditions_term["prompt"] assert ( - if_result["prompt"] + conditions_term["prompt"] == "Что выведет этот код?\n\nif 3 > 5:\n print('да')\nelse:\n print('нет')" ) starters = { - "if-code": "temperature = 22\n# твой код\n", - "for-code": "# Напиши цикл\n", - "while-code": "start = 5\n# твой код\n", + "conditions-code": "temperature = 22\n# твой код\n", + "for-loop-code": "# Напиши цикл\n", + "while-loop-code": "start = 5\n# твой код\n", } for question_id, starter in starters.items(): question = QUESTION_BY_ID[question_id] @@ -106,9 +171,13 @@ def test_lessons_four_to_six_do_not_require_future_topics() -> None: styles = Path("app/static/styles.css").read_text(encoding="utf-8") assert re.search(r"\.question-prompt\s*\{[^}]*white-space:\s*pre-wrap", styles) - assert QUESTION_BY_ID["if-code"]["tests"] == [{"kind": "stdout", "expected": "Возьми куртку"}] - assert QUESTION_BY_ID["for-code"]["tests"] == [{"kind": "stdout", "expected": "1\n2\n3\n4\n5"}] - assert QUESTION_BY_ID["while-code"]["tests"] == [ + assert QUESTION_BY_ID["conditions-code"]["tests"] == [ + {"kind": "stdout", "expected": "Возьми куртку"} + ] + assert QUESTION_BY_ID["for-loop-code"]["tests"] == [ + {"kind": "stdout", "expected": "1\n2\n3\n4\n5"} + ] + assert QUESTION_BY_ID["while-loop-code"]["tests"] == [ {"kind": "stdout", "expected": "5\n4\n3\n2\n1\nПуск!"} ]