Python с нуля до Junior-разработчика

Курс проведёт от базовых конструкций Python к написанию небольших прикладных проектов и пониманию типичных задач джуна. Вы освоите основы языка, работу с файлами и API, ООП, тестирование, Git и базовые принципы разработки. Итогом станет портфолио из нескольких проектов и уверенность для прохождения собеседований на Junior-позиции.

Установка, среда разработки и первые программы

Установка, среда разработки и первые программы

На этом уроке вы установите Python, настроите рабочее окружение, научитесь запускать программы из терминала и из редактора кода, а также напишете первые мини-программы. Это база, без которой дальше (переменные, условия, циклы, функции, модули) будет неудобно.

Что такое Python и как он запускает ваш код

Python — это язык программирования и одновременно набор инструментов (интерпретатор и стандартная библиотека), которые выполняют ваш код.

Обычно вы пишете программу в файле с расширением *.py, а затем запускаете её командой python имя_файла.py.

Установка Python

Наша цель — поставить актуальный Python 3 и убедиться, что команда python (или python3) работает в терминале.

Windows

  1. Скачайте установщик с сайта Python Downloads.
  2. Запустите установщик.
  3. Перед установкой поставьте галочку Add python.exe to PATH (важно, чтобы python работал из терминала).
  4. Завершите установку.
  5. Откройте PowerShell и проверьте:
python --version

Если команда не найдена, обычно проблема в PATH. В таком случае проще всего переустановить Python, не забыв включить добавление в PATH.

macOS

  1. Скачайте установщик с сайта Python Downloads.
  2. Установите Python (обычно это пакет *.pkg).
  3. Откройте Terminal и проверьте:
python3 --version

На macOS часто используется команда python3, потому что python может указывать на системный Python (или отсутствовать).

Linux

На большинстве Linux Python уже установлен, но версия может быть не самой новой.

  1. Откройте терминал и проверьте:
python3 --version
  1. Если Python 3 не установлен — установите через пакетный менеджер вашей системы (в рамках курса не фиксируем одну команду, потому что дистрибутивы отличаются).

Проверка установки: интерпретатор, pip и путь к Python

Проверка интерпретатора

Проверьте версию:

python --version

или:

python3 --version

Проверка pip

pip — это менеджер пакетов Python: с его помощью вы устанавливаете сторонние библиотеки.

Проверьте:

python -m pip --version

Если у вас используется python3, то:

python3 -m pip --version

Почему команда именно такая: запуск через python -m pip гарантирует, что pip относится именно к этому Python, а не к другому, установленному в системе.

Узнать, какой Python запускается

Иногда в системе несколько Python. Полезно проверить путь:

python -c "import sys; print(sys.executable)"

Выбор среды разработки

Вам нужен редактор кода и терминал. Ниже — два популярных варианта.

Среда Когда подходит Плюсы Минусы
Visual Studio Code Почти всем, особенно на старте Быстрый, много расширений, удобно работать с Git и терминалом Нужно поставить расширение Python и немного настроить
PyCharm Community Edition Тем, кто хочет максимально «всё из коробки» Сильная поддержка Python-проектов, удобная навигация Требовательнее к ресурсам, иногда «тяжелее» для простых задач

Для курса можно выбрать любой вариант. Важно: вы должны уметь запускать код и видеть ошибки.

Проект и папки: как организовать первые файлы

Создайте папку, например python-course, а внутри — папку для урока, например lesson-01.

Рекомендуемая структура:

python-course/
  lesson-01/
    main.py

Файл main.py — это ваша первая программа.

Виртуальное окружение

Виртуальное окружение — это отдельная «песочница» для библиотек конкретного проекта. Оно помогает избежать конфликтов версий и путаницы между проектами.

Создание виртуального окружения (внутри папки проекта, например lesson-01):

python -m venv .venv

Активация:

  1. Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
  1. macOS/Linux:
source .venv/bin/activate

После активации в терминале обычно появляется пометка окружения (например, .venv).

Деактивация:

deactivate

Официальная справка: venv — Creation of virtual environments

Первая программа: Hello, world

Откройте main.py и напишите:

print("Hello, world!")

Запуск из терминала (в папке lesson-01):

python main.py

Если у вас используется python3:

python3 main.py

Что происходит:

  • print(...) — выводит текст в терминал.
  • Строка в кавычках — это текст, который вы выводите.

Программа с вводом данных

Сделаем программу, которая спрашивает имя и здоровается:

name = input("Как тебя зовут? ")
print("Привет,", name)

Важно понимать:

  • input(...) всегда возвращает строку.
  • Запятая в print("Привет,", name) печатает слова через пробел.

Калькулятор на минималках (и типичная ошибка новичка)

Попробуем сложить два числа:

a = input("Введите a: ")
b = input("Введите b: ")
print(a + b)

Это почти наверняка даст не сумму, а склеивание строк. Например, 2 и 3 превратятся в 23.

Правильный вариант:

a = int(input("Введите a: "))
b = int(input("Введите b: "))
print(a + b)

Здесь:

  • int(...) преобразует строку в целое число.
  • Если пользователь введёт не число, Python покажет ошибку. Позже вы научитесь обрабатывать такие ситуации.

Как читать ошибки (traceback)

Ошибки — нормальная часть разработки. Python обычно показывает:

  • тип ошибки (например, SyntaxError, NameError, ValueError)
  • файл и строку, где ошибка произошла
  • фрагмент проблемного кода

Пример частой ошибки:

print(hello)

Если hello не определено, будет NameError. Исправление — использовать строку:

print("hello")

Режим REPL: быстрые проверки

REPL — это интерактивный режим Python, где можно быстро проверить выражения.

Запуск:

python

Выход:

  • Windows: Ctrl+Z, затем Enter
  • macOS/Linux: Ctrl+D

REPL полезен, чтобы быстро проверить идею, но основной код курса вы будете писать в файлах.

Полезные правила на старте

  • Всегда создавайте отдельную папку под проект и храните код в *.py файлах.
  • Используйте виртуальное окружение .venv для каждого проекта.
  • Запускайте pip через python -m pip, чтобы не перепутать версии.
  • Если что-то не работает, сначала проверьте python --version, затем путь к интерпретатору.

Что дальше

На следующем уроке вы начнёте писать программы увереннее: разберём базовые типы данных (числа, строки, булевы значения), переменные и простые операции — это фундамент для любых условий, циклов и функций.

Базовый синтаксис: типы данных, условия, циклы

Базовый синтаксис: типы данных, условия, циклы

На прошлом уроке вы установили Python, научились запускать *.py файлы и уже использовали print() и input(). Теперь разберём основу, без которой невозможно писать реальные программы: типы данных, условия и циклы.

Как Python читает вашу программу

Python выполняет код сверху вниз, строка за строкой. Управлять этим порядком позволяют:

  • условия if (выполнить код только если истинно)
  • циклы while и for (выполнять код много раз)

Ключевая особенность Python — блоки кода задаются отступами. Обычно используется 4 пробела.

Переменные и присваивание

Переменная — это имя, которое ссылается на значение.

age = 18
name = "Аня"

Правила, которые важно запомнить:

  • имя переменной может содержать буквы, цифры и _, но не может начинаться с цифры
  • регистр важен: name и Name — разные переменные
  • в Python тип хранится у значения, а не “приклеен” к имени переменной

Быстрый взгляд на тип значения

x = 10
print(type(x))

Документация: Встроенные функции Python (type)

Базовые типы данных

Числа: int и float

  • int — целые числа: -2, 0, 15
  • float — числа с точкой: 3.14, -0.5
a = 7
b = 2
print(a + b)   # 9
print(a / b)   # 3.5 (деление всегда даёт float)
print(a // b)  # 3 (целочисленное деление)
print(a % b)   # 1 (остаток)

Полезный случай a % b: проверка чётности.

n = 10
print(n % 2 == 0)  # True

Строки: str

Строка — это текст в кавычках.

first = "Py"
second = "thon"
print(first + second)  # Python

Частые операции:

  • конкатенация: "a" + "b"
  • повторение: "ha" * 3
  • длина: len("привет")
text = "ha"
print(text * 3)       # hahaha
print(len("привет")) # 6

Документация: Встроенные типы (str)

Логический тип: bool

bool имеет два значения: True и False. Он получается из сравнений и логических операций.

print(5 > 3)          # True
print(10 == 10)       # True
print(10 != 10)       # False
print(True and False) # False
print(True or False)  # True
print(not True)       # False

Специальное значение: None

None означает “нет значения”. Это полезно, когда значение пока неизвестно или отсутствует.

result = None
print(result)

Ввод данных и преобразование типов

Вы уже видели, что input() возвращает строку.

age_text = input("Сколько вам лет? ")
print(type(age_text))  # <class 'str'>

Чтобы получить число:

age = int(input("Сколько вам лет? "))
print(age + 1)

Важно:

  • int("12") работает
  • int("12.5") вызовет ошибку ValueError
  • float("12.5") работает

Условия: if, elif, else

Условие выполняет блок кода, только если выражение истинно.

age = int(input("Возраст: "))

if age >= 18:
    print("Доступ разрешён")
else:
    print("Доступ запрещён")

Если веток больше двух, используйте elif.

score = int(input("Баллы: "))

if score >= 90:
    print("5")
elif score >= 70:
    print("4")
elif score >= 50:
    print("3")
else:
    print("2")

Что считается истинным и ложным

В Python в условии можно писать не только == True. Многие значения сами по себе трактуются как “истина/ложь”.

Ложными считаются:

  • 0, 0.0
  • пустая строка ""
  • пустые коллекции (например, [], {})
  • None

Пример:

text = input("Введите что-нибудь: ")

if text:
    print("Вы ввели:", text)
else:
    print("Пустая строка")

Цикл while

while повторяет блок, пока условие истинно.

count = 1
while count <= 5:
    print("Шаг", count)
    count = count + 1

Типичная ошибка новичка — забыть изменить переменную, из-за чего цикл станет бесконечным.

break и continue

  • break — выйти из цикла
  • continue — перейти к следующей итерации
while True:
    text = input("Введите команду (exit для выхода): ")
    if text == "exit":
        break
    if text == "":
        continue
    print("Команда:", text)

Цикл for и range()

for в Python обычно используют для перебора последовательностей.

Повторить N раз

for i in range(5):
    print(i)

range(5) создаёт последовательность чисел 0, 1, 2, 3, 4.

Частые формы:

  • range(stop)
  • range(start, stop)
  • range(start, stop, step)
for i in range(2, 10, 2):
    print(i)  # 2 4 6 8

Документация: Встроенные функции Python (range)

Перебор строки

Строка — это последовательность символов.

word = "python"
for ch in word:
    print(ch)

Практический мини-пример: сумма чисел

Задача: посчитать сумму чисел от 1 до n.

Вариант с while:

n = int(input("n: "))

total = 0
current = 1
while current <= n:
    total += current
    current += 1

print("Сумма:", total)

Вариант с for:

n = int(input("n: "))

total = 0
for current in range(1, n + 1):
    total += current

print("Сумма:", total)

Частые ошибки и как их чинить

  • SyntaxError из-за двоеточия: после if, elif, else, for, while обязательно ставится :
  • ошибки из-за отступов: в одном блоке используйте одинаковые отступы (обычно 4 пробела)
  • сравнение вместо присваивания: = — присваивание, == — сравнение
  • “склейка строк” вместо сложения чисел: помните, что input() даёт str, и используйте int() или float()

Что дальше

Теперь у вас есть базовый инструментарий управления логикой программы: типы данных, условия и циклы. На следующем шаге вы начнёте писать более структурный код: функции, модули и работа с коллекциями (списки, словари), чтобы решать задачи не “в лоб”, а по-взрослому — переиспользуемо и читаемо.

Функции, модули и работа с исключениями

Функции, модули и работа с исключениями

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

  • выносить повторяющуюся логику в функции
  • организовывать код по файлам и переиспользовать его через модули
  • делать программы устойчивыми к ошибкам с помощью исключений

Эти три темы постоянно используются в реальной разработке.

Функции

Функция — это именованный блок кода, который можно вызывать многократно. Функции помогают:

  • не повторять одинаковый код
  • делать программу понятнее
  • легче тестировать и исправлять ошибки

Документация: Функции в Python (def)

Объявление и вызов функции

Простейшая функция:

def say_hello():
    print("Hello!")

say_hello()
say_hello()

Что важно:

  • def объявляет функцию
  • после имени функции идут скобки ()
  • тело функции — это блок с отступом
  • чтобы выполнить функцию, её нужно вызвать по имени

Параметры: как передавать данные в функцию

Функция может принимать параметры (входные данные):

def greet(name):
    print("Привет,", name)

greet("Аня")
greet("Игорь")

Если параметров несколько:

def add(a, b):
    print(a + b)

add(2, 3)

return: как вернуть результат

Если функция должна вернуть значение, используйте return:

def add(a, b):
    return a + b

result = add(10, 5)
print(result)

Если return не указан, функция возвращает None.

def do_something():
    x = 10  # что-то посчитали

value = do_something()
print(value)  # None

Идея простая:

  • print() показывает текст пользователю
  • return отдаёт значение обратно в код, чтобы его можно было использовать дальше

Локальные переменные и область видимости

Переменные, созданные внутри функции, обычно не видны снаружи:

def f():
    x = 123
    print("внутри:", x)

f()
# print(x)  # будет ошибка NameError

Это полезно: функции не должны случайно менять данные в других частях программы.

Значения по умолчанию

Можно задавать значения по умолчанию для параметров:

def greet(name, city="Москва"):
    print("Привет,", name, "из города", city)

greet("Аня")
greet("Аня", "Казань")

Докстроки: как документировать функцию

Докстрока — это строка в начале функции, которая описывает, что функция делает:

def area_of_rectangle(width, height):
    """Возвращает площадь прямоугольника по ширине и высоте."""
    return width * height

print(area_of_rectangle.__doc__)

Документация: Докстроки (PEP 257)

Практический пример: вынесем повторяющуюся логику

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

def read_int(prompt):
    text = input(prompt)
    return int(text)

a = read_int("Введите a: ")
b = read_int("Введите b: ")
print("Сумма:", a + b)

В следующем разделе вы научитесь делать этот код устойчивым к неправильному вводу.

Модули

Модуль — это обычный файл *.py, который можно подключать в другие файлы с помощью import.

Документация: Модули

Зачем нужны модули

Модули нужны, чтобы:

  • хранить функции в отдельном файле и переиспользовать
  • разделять программу на части, а не держать всё в одном main.py
  • подключать стандартную библиотеку и сторонние пакеты

Импорт стандартной библиотеки

Python поставляется со стандартной библиотекой. Например, модуль math:

import math

print(math.sqrt(16))

Документация: Модуль math

Импорт своих модулей

Представим структуру:

lesson-03/
  main.py
  utils.py

utils.py:

def is_even(n):
    return n % 2 == 0

main.py:

import utils

number = int(input("Введите число: "))
print(utils.is_even(number))

Важно:

  • импортируется имя файла без .py
  • обращаться к функциям нужно через utils.is_even(...)

from ... import ... и псевдонимы

Можно импортировать конкретные имена:

from math import sqrt

print(sqrt(25))

Можно задавать псевдоним через as:

import math as m

print(m.pi)

Почему нужен if __name__ == "__main__"

Если файл является точкой входа (вы запускаете его напрямую), часто используют такую конструкцию:

def main():
    print("Запуск программы")

if __name__ == "__main__":
    main()

Смысл:

  • при запуске python main.py условие истинно и main() вызывается
  • если этот файл импортируют как модуль, main() автоматически не запускается

Документация: Специальные переменные модуля (name)

Сторонние пакеты и pip

Ранее вы познакомились с pip и виртуальным окружением. Напоминание:

  • ставьте пакеты в активированное виртуальное окружение
  • используйте python -m pip ..., чтобы не перепутать версии Python

Документация: pip — установщик пакетов Python

Исключения

Исключение — это механизм Python для сообщения об ошибке во время выполнения программы. Когда возникает ошибка (например, неправильный ввод), Python:

  • создаёт объект исключения
  • прерывает обычный ход выполнения
  • показывает traceback, если исключение не обработано

Документация: Ошибки и исключения

Пример: где возникает ValueError

Если пользователь введёт не число:

age = int(input("Возраст: "))
print(age)

то int(...) может вызвать ValueError.

try и except: перехват ошибки

Чтобы программа не падала, используйте try/except:

try:
    age = int(input("Возраст: "))
    print("Ваш возраст:", age)
except ValueError:
    print("Ошибка: нужно ввести целое число")

Как это читать:

  • в блоке try мы пишем код, где может случиться ошибка
  • в except ValueError мы описываем, что делать, если случилась именно эта ошибка

Несколько except

Можно обрабатывать разные ошибки по-разному:

try:
    a = int(input("a: "))
    b = int(input("b: "))
    print(a / b)
except ValueError:
    print("Введите целые числа")
except ZeroDivisionError:
    print("На ноль делить нельзя")

Документация: Встроенные исключения

else и finally

  • else выполняется, если в try не было ошибки
  • finally выполняется всегда (обычно для освобождения ресурсов)
try:
    x = int(input("x: "))
except ValueError:
    print("Это не число")
else:
    print("Ок, вы ввели:", x)
finally:
    print("Конец попытки ввода")

raise: как намеренно создать ошибку

Иногда вы хотите сами остановить выполнение, если данные неправильные:

def set_age(age):
    if age < 0:
        raise ValueError("Возраст не может быть отрицательным")
    return age

try:
    print(set_age(-5))
except ValueError as e:
    print("Ошибка:", e)

Обратите внимание на as e: так вы получаете текст ошибки.

Собираем всё вместе: функция безопасного ввода числа

Вернёмся к идее вынести ввод в функцию, но теперь сделаем её устойчивой к ошибкам:

def read_int(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("Введите целое число. Попробуйте ещё раз.")

a = read_int("Введите a: ")
b = read_int("Введите b: ")
print("Сумма:", a + b)

Что здесь происходит:

  • цикл while True повторяет попытки
  • return завершает функцию сразу, как только ввод корректный
  • except не даёт программе упасть и объясняет пользователю, что делать

Типичные ошибки при работе с исключениями

  • ловить слишком общее исключение без необходимости
# плохо: можно скрыть реальные баги
try:
    ...
except Exception:
    print("что-то пошло не так")
  • перехватывать исключение и молча его игнорировать
# плохо: вы теряете информацию об ошибке
try:
    ...
except ValueError:
    pass

Практическое правило: ловите конкретные ошибки и сообщайте понятное сообщение или исправляйте ситуацию.

Что дальше

Теперь вы умеете:

  • превращать повторяющиеся фрагменты в функции
  • выносить код в отдельные модули и подключать их через импорт
  • обрабатывать ошибки ввода и другие проблемы через исключения

Дальше этот фундамент пригодится для более сложных тем: коллекций (списки, словари), работы с файлами, тестирования и небольших проектов, где структура кода и обработка ошибок становятся критически важными.

Коллекции и алгоритмическое мышление на практике

Коллекции и алгоритмическое мышление на практике

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

Коллекции позволяют хранить сразу много значений и удобно их обрабатывать. А ещё на них очень хорошо тренируется алгоритмическое мышление: как превратить задачу в понятные шаги, подобрать структуру данных и написать решение без лишней сложности.

Что такое коллекции и зачем они нужны

Коллекция — это объект, который хранит несколько значений.

Самые популярные встроенные коллекции в Python:

  • list (список): упорядоченная изменяемая последовательность.
  • tuple (кортеж): упорядоченная неизменяемая последовательность.
  • dict (словарь): отображение ключ → значение.
  • set (множество): набор уникальных элементов без порядка.

Официальная документация по типам:

Список (list)

Список подходит, когда важен порядок и элементы могут повторяться.

Создание и доступ по индексу

numbers = [10, 20, 30]
print(numbers[0])  # 10
print(numbers[-1]) # 30

Важно:

  • индексация начинается с 0
  • отрицательный индекс идёт с конца (-1 — последний элемент)

Добавление, удаление, длина

items = ["a", "b"]
items.append("c")
print(items)  # ['a', 'b', 'c']

items.remove("b")
print(items)  # ['a', 'c']

print(len(items))  # 2

Полезные методы (и что они делают):

  • append(x) добавляет элемент в конец
  • pop() удаляет и возвращает последний элемент (или по индексу pop(i))
  • remove(x) удаляет первое вхождение значения
  • sort() сортирует список на месте

Документация: Тип list

Перебор списка

nums = [2, 5, 8]
for n in nums:
    print(n)

Если нужен индекс:

nums = [2, 5, 8]
for i, n in enumerate(nums):
    print(i, n)

Документация: Встроенная функция enumerate

Кортеж (tuple)

Кортеж похож на список, но неизменяемый. Это полезно, когда вы хотите зафиксировать набор данных.

point = (10, 20)
print(point[0])

Частый кейс: вернуть из функции сразу несколько значений.

def min_max(values):
    return min(values), max(values)

mn, mx = min_max([3, 1, 9])
print(mn, mx)

Документация: Тип tuple

Словарь (dict)

Словарь — это структура ключ → значение. Он незаменим, когда нужно быстро находить данные по ключу.

Создание и чтение

user = {
    "name": "Аня",
    "age": 18
}

print(user["name"])  # Аня

Правила, которые важно запомнить:

  • ключи в словаре уникальны
  • если обратиться к отсутствующему ключу через user["city"], будет KeyError

Документация: Тип dict

Безопасное чтение: get

Если ключ может отсутствовать, удобнее использовать get:

user = {"name": "Аня"}
city = user.get("city", "(не указан)")
print(city)

Добавление и обновление

user = {"name": "Аня"}
user["age"] = 18
user["name"] = "Анна"  # обновление

Перебор словаря

prices = {"apple": 120, "banana": 90}

for key in prices:
    print(key, prices[key])

for key, value in prices.items():
    print(key, value)

Множество (set)

Множество хранит уникальные элементы. Обычно используют, чтобы:

  • убрать дубликаты
  • быстро проверять принадлежность элемента
nums = [1, 2, 2, 3]
unique = set(nums)
print(unique)  # {1, 2, 3}

print(2 in unique)  # True

Важно: порядок элементов в set не фиксирован.

Документация: Тип set

Как выбрать коллекцию под задачу

Задача Подходящая коллекция Почему
Хранить элементы по порядку, дополнять, удалять list порядок + изменяемость
Хранить фиксированный набор значений tuple неизменяемость, удобно передавать/возвращать
Быстро получать значение по ключу dict доступ по ключу, удобно хранить пары
Убрать дубликаты, проверять принадлежность set уникальность и быстрый in

Алгоритмическое мышление: как решать задачи на коллекциях

Алгоритмическое мышление — это умение превращать задачу в последовательность простых шагов и выбирать подходящие структуры данных.

Полезный шаблон:

  1. Чётко сформулировать входные данные и ожидаемый результат.
  2. Выбрать коллекцию(и), которая делает решение простым.
  3. Разбить на шаги: чтение → обработка → вывод.
  4. Написать решение в виде функций.
  5. Добавить обработку ошибок (если есть ввод пользователя).

Ниже — несколько типовых задач, которые встречаются постоянно.

Практика: частотный словарь (подсчёт повторений)

Задача: есть список слов. Нужно посчитать, сколько раз встречается каждое.

Идея:

  1. Завести пустой словарь counts.
  2. Для каждого слова увеличивать счётчик.
words = ["python", "java", "python", "go", "python", "go"]
counts = {}

for w in words:
    if w in counts:
        counts[w] += 1
    else:
        counts[w] = 1

print(counts)  # {'python': 3, 'java': 1, 'go': 2}

Почему это хороший алгоритм:

  • один проход по данным
  • структура dict идеально подходит для хранения ключ → счётчик

Практика: уникальные значения и фильтрация

Задача: из списка email оставить только уникальные.

emails = ["a@mail.com", "b@mail.com", "a@mail.com"]
unique_emails = list(set(emails))
print(unique_emails)

Если важен исходный порядок, делаем аккуратнее:

emails = ["a@mail.com", "b@mail.com", "a@mail.com"]
seen = set()
result = []

for e in emails:
    if e not in seen:
        seen.add(e)
        result.append(e)

print(result)  # ['a@mail.com', 'b@mail.com']

Практика: поиск минимума/максимума и «лучшего кандидата»

Задача: найти самое длинное слово.

words = ["hi", "python", "code"]
longest = words[0]

for w in words:
    if len(w) > len(longest):
        longest = w

print(longest)  # python

Здесь тренируется важная идея: держать текущий лучший результат и обновлять его по условию.

Сортировка и параметр key

Очень частая реальная операция — сортировка по «чему-то».

Пример: отсортировать список слов по длине.

words = ["hi", "python", "code"]
words.sort(key=len)
print(words)  # ['hi', 'code', 'python']

Или получить новый отсортированный список, не меняя исходный:

words = ["hi", "python", "code"]
sorted_words = sorted(words, key=len, reverse=True)
print(sorted_words)  # ['python', 'code', 'hi']

Документация:

Ошибки и исключения при работе с коллекциями

Коллекции часто приводят к типовым ошибкам. Хорошая новость: большинство из них предсказуемы.

KeyError в словаре

Плохо (может упасть):

prices = {"apple": 120}
print(prices["banana"])  # KeyError

Хорошо (безопасно):

prices = {"apple": 120}
print(prices.get("banana", 0))

Если по логике программы отсутствие ключа — это ошибка, можно обработать через try/except:

prices = {"apple": 120}
try:
    print(prices["banana"])
except KeyError:
    print("Такого товара нет")

IndexError в списке

Ошибка возникает, если индекс выходит за границы.

items = ["a"]
# print(items[1])  # IndexError

Обычно это чинится проверкой len(items) или правильной логикой цикла.

Простая «оценка скорости» без сложной математики

На уровне начинающего достаточно понимать 3 идеи:

  • один цикл по списку — обычно нормально даже для больших данных
  • вложенные циклы по большим спискам быстро становятся медленными
  • проверка x in list обычно требует перебора (медленнее), а x in set — обычно быстрее

Практический вывод:

  • для поиска по ключу выбирайте dict
  • для проверки принадлежности выбирайте set
  • для упорядоченных данных выбирайте list

Как писать «по-взрослому»: функции + коллекции + модули

Коллекции почти всегда живут вместе с функциями:

  • функция получает коллекцию
  • возвращает результат (число, строку, новую коллекцию)
  • внутри использует цикл и условия

Пример: вынесем подсчёт частот в функцию.

def count_words(words):
    counts = {}
    for w in words:
        counts[w] = counts.get(w, 0) + 1
    return counts

result = count_words(["python", "go", "python"])
print(result)

Дальше это можно вынести в отдельный модуль text_utils.py и импортировать в main.py — ровно как вы делали в уроке про модули.

Что дальше

Теперь вы умеете выбирать и применять коллекции под разные задачи, а главное — решать типовые задачи через понятный алгоритм: пройтись по данным, накопить результат (в списке, словаре или множестве) и корректно обработать ошибки.

Следующий шаг к уровню Junior — закреплять это на мини-проектах и подключать новые темы (работа с файлами, тестирование, основы ООП), где коллекции используются постоянно.

ООП в Python: классы, наследование, композиция

ООП в Python: классы, наследование, композиция

На прошлых уроках вы научились писать программы с условиями и циклами, оформлять повторяющуюся логику в функции, разносить код по модулям, использовать исключения и хранить данные в коллекциях. Следующий шаг к уровню Junior — освоить объектно-ориентированное программирование (ООП).

ООП помогает собирать программу из понятных «кирпичиков»: объектов, которые хранят состояние (данные) и умеют что-то делать (методы). Это особенно полезно, когда проект растёт: появляется больше сущностей, правил и связей.

Документация: Учебник Python: классы

Зачем нужно ООП

ООП обычно используют, когда:

  • в задаче есть сущности с данными и поведением (пользователь, заказ, товар)
  • нужно ограничить доступ к внутренним деталям и защититься от неправильных состояний
  • хочется переиспользовать и расширять код без бесконечных if/elif
  • важна тестируемость и возможность «подменять» части системы

Важно: ООП — это инструмент, а не обязательное правило. Многие задачи отлично решаются функциями и словарями. ООП полезно там, где оно упрощает модель предметной области.

Основные понятия

  • Класс — «чертёж» (шаблон), по которому создаются объекты.
  • Объект (экземпляр) — конкретная созданная сущность.
  • Атрибут — переменная, принадлежащая объекту или классу.
  • Метод — функция, принадлежащая классу (и вызываемая у объекта).

Классы и объекты

Минимальный пример класса

class User:
    def greet(self):
        print("Привет!")

u = User()
u.greet()

Что здесь важно:

  • class User: объявляет класс
  • User() создаёт объект (экземпляр)
  • u.greet() вызывает метод у объекта

Конструктор __init__ и атрибуты

Чаще всего объекту нужно начальное состояние. Для этого используют __init__.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print("Привет, меня зовут", self.name)

u = User("Аня", 18)
u.greet()
print(u.age)

Ключевые идеи:

  • __init__ вызывается при создании объекта
  • self — ссылка на текущий объект
  • self.name и self.age — атрибуты объекта

self: почему он нужен

Метод — это функция, которая работает с конкретным объектом. Чтобы метод мог читать и менять данные объекта, Python передаёт этот объект первым аргументом. По соглашению его называют self.

Атрибуты объекта и атрибуты класса

Иногда данные общие для всех объектов. Это атрибуты класса.

class User:
    role = "user"  # атрибут класса

    def __init__(self, name):
        self.name = name  # атрибут объекта

u1 = User("Аня")
u2 = User("Игорь")

print(u1.role, u2.role)
User.role = "member"
print(u1.role, u2.role)

Практическое правило:

  • если значение относится к каждому конкретному объекту — делайте атрибут объекта
  • если значение общее для всех — делайте атрибут класса

Методы и полезные магические методы

Обычные методы

Обычный метод получает self и работает с состоянием объекта.

class Counter:
    def __init__(self):
        self.value = 0

    def inc(self):
        self.value += 1

c = Counter()
c.inc()
print(c.value)

__str__ и __repr__

Эти методы помогают красиво выводить объект.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"User: {self.name} ({self.age})"

    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age!r})"

u = User("Аня", 18)
print(u)        # использует __str__
print([u])      # список использует __repr__ элементов

Документация: Модель данных Python (специальные методы)

Инкапсуляция и валидация состояния

В Python нет строгих модификаторов доступа как в некоторых языках, но есть соглашения.

  • атрибуты, начинающиеся с _, считаются внутренними и не предназначены для прямого использования
  • для контроля чтения и записи часто используют property

Пример: защита возраста

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age  # пойдёт через property

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Возраст не может быть отрицательным")
        self._age = value

u = User("Аня", 18)
# u.age = -1  # ValueError

Что здесь происходит:

  • снаружи вы работаете как обычно: u.age
  • внутри можно проверять и защищать состояние

Документация: property

Наследование

Наследование — это механизм, позволяющий создать новый класс на основе существующего. Новый класс получает методы и атрибуты базового класса и может добавлять свои или переопределять старые.

Пример: базовый класс и расширение

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def final_price(self):
        return self.price

class DigitalProduct(Product):
    def __init__(self, name, price, file_size_mb):
        super().__init__(name, price)
        self.file_size_mb = file_size_mb

class PhysicalProduct(Product):
    def __init__(self, name, price, weight_kg):
        super().__init__(name, price)
        self.weight_kg = weight_kg

    def final_price(self):
        return self.price + 300  # допустим, фиксированная доставка

p1 = DigitalProduct("Курс", 1500, 800)
p2 = PhysicalProduct("Книга", 1200, 0.6)

print(p1.final_price())
print(p2.final_price())

Ключевые идеи:

  • class Child(Parent): создаёт наследника
  • super().__init__(...) вызывает конструктор родителя
  • переопределение метода (как final_price) — основа полиморфизма: одинаковый вызов даёт разное поведение в зависимости от типа объекта

Когда наследование уместно

Наследование хорошо работает, когда:

  • связь действительно «является»: PhysicalProduct является Product
  • базовый класс задаёт общий контракт поведения (например, final_price)

Осторожно с наследованием:

  • слишком глубокие иерархии становятся трудными для понимания
  • часто лучше использовать композицию

Композиция

Композиция — это подход, когда объект содержит другие объекты и делегирует им часть работы. Это связь «имеет»: Order имеет список товаров, Order имеет политику скидок.

Пример: заказ, товары и политика скидки

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

class NoDiscount:
    def apply(self, total):
        return total

class PercentDiscount:
    def __init__(self, percent):
        self.percent = percent

    def apply(self, total):
        return total * (1 - self.percent / 100)

class Order:
    def __init__(self, discount_policy):
        self.items = []
        self.discount_policy = discount_policy

    def add(self, product):
        self.items.append(product)

    def total(self):
        raw_total = sum(p.price for p in self.items)
        return self.discount_policy.apply(raw_total)

order = Order(PercentDiscount(10))
order.add(Product("Книга", 1200))
order.add(Product("Курс", 1500))
print(order.total())

Почему это сильный пример:

  • Order не знает, как именно считать скидку, он делегирует это объекту политики
  • скидку можно легко заменить: Order(NoDiscount())
  • код расширяется добавлением новых политик без переписывания Order

Композиция тесно связана с тем, что вы уже знаете:

  • Order.items — это список (коллекции)
  • sum(...) и генераторы — это удобный способ обработать коллекцию
  • объекты можно хранить в списках и словарях как обычные значения

Наследование или композиция: как выбрать

Ситуация Чаще подходит Почему
«X является Y» и нужен общий контракт поведения наследование общий базовый API, переопределение методов
«X имеет Y» и нужно легко подменять часть логики композиция меньше связности, проще расширять
Хочется избежать глубоких иерархий композиция упрощает поддержку

Практическое правило: если сомневаетесь, начните с композиции. Наследование добавляйте тогда, когда связь «является» очевидна и даёт реальную пользу.

Как это связывается с функциями, модулями и исключениями

  • методы класса — это те же функции, просто сгруппированные вокруг данных
  • классы удобно раскладывать по модулям: models.py, services.py, utils.py
  • исключения по-прежнему основной способ сигнализировать об ошибках: например, ValueError в сеттере age

Структура маленького проекта может выглядеть так:

lesson-oop/
  main.py
  models.py
  discounts.py

main.py импортирует классы и собирает сценарий программы, как вы делали раньше с модулями.

Что дальше

Теперь у вас есть основа ООП:

  • вы умеете создавать классы и объекты, определять атрибуты и методы
  • понимаете, как работает наследование, super() и переопределение
  • знаете композицию и почему она часто делает код гибче

Следующий практический шаг — закреплять ООП на мини-проектах (консольный магазин, трекер задач, простой менеджер заметок) и добавлять темы, которые часто идут рядом с ООП: работа с файлами, тестирование, основы проектирования.

Файлы, JSON, HTTP-запросы и работа с API

Файлы, JSON, HTTP-запросы и работа с API

На предыдущих уроках вы научились писать функции, разносить код по модулям, работать с исключениями, коллекциями и освоили базовые принципы ООП. Теперь соберём это в практический набор навыков, который постоянно нужен Junior-разработчику:

  • читать и записывать файлы (тексты, настройки, данные)
  • работать с форматом JSON (самый популярный формат обмена данными)
  • делать HTTP-запросы и получать ответы от API
  • аккуратно обрабатывать ошибки: неправильные данные, отсутствие файла, сетевые проблемы

Работа с файлами

Файл на диске можно представить как последовательность байт. В Python чаще всего работают в двух режимах:

  • текстовый режим (строки str, нужна кодировка)
  • бинарный режим (байты bytes, используют для изображений, архивов)

В этом уроке сфокусируемся на текстовых файлах.

Открытие файла и контекстный менеджер with

Базовая функция для работы с файлами — open(). Почти всегда открывайте файл через with: так файл гарантированно закроется, даже если произошла ошибка.

Документация: open

Чтение всего файла целиком:

with open("notes.txt", "r", encoding="utf-8") as f:
    text = f.read()

print(text)

Что означают аргументы:

  • "notes.txt" — путь к файлу
  • "r" — режим чтения (read)
  • encoding="utf-8" — кодировка текста (в большинстве проектов используйте UTF-8)

Режимы открытия файла

Чаще всего используются:

  • "r" — чтение (файл должен существовать)
  • "w" — запись (создаст файл заново или перезапишет существующий)
  • "a" — дозапись в конец файла
  • "r+" — чтение и запись

Чтение построчно

Если файл большой, удобнее читать по строкам.

with open("notes.txt", "r", encoding="utf-8") as f:
    for line in f:
        line = line.rstrip("\n")
        print("Строка:", line)

Почему используется rstrip("\n"):

  • при чтении строка обычно заканчивается переводом строки
  • иногда удобнее убрать его для обработки

Запись в файл

Запись с перезаписью:

data = ["apple", "banana", "orange"]

with open("fruits.txt", "w", encoding="utf-8") as f:
    for item in data:
        f.write(item + "\n")

Дозапись:

with open("fruits.txt", "a", encoding="utf-8") as f:
    f.write("mango\n")

Типичные ошибки при работе с файлами

  • FileNotFoundError — файл не найден
  • PermissionError — нет прав на чтение/запись
  • ошибки кодировки, если открыть файл не в той кодировке

Пример безопасного чтения:

def read_text_file(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        print("Файл не найден:", path)
        return None

content = read_text_file("missing.txt")
print(content)

Здесь вы применяете то, что изучали раньше:

  • функция возвращает результат через return
  • ошибка не «роняет» программу, потому что мы обработали исключение

Пути к файлам и pathlib

Чтобы писать переносимый код, удобно работать с путями через pathlib.

Документация: pathlib

from pathlib import Path

base_dir = Path(".")
data_dir = base_dir / "data"
file_path = data_dir / "input.txt"

print(file_path)

Полезный момент:

  • оператор / у Path не делит, а «склеивает» части пути корректно для вашей ОС

JSON: чтение и запись структурированных данных

JSON (JavaScript Object Notation) — формат данных, который выглядит похоже на словари и списки Python. В API чаще всего возвращают именно JSON.

В JSON есть основные типы:

  • объект (object) — похоже на dict
  • массив (array) — похоже на list
  • строка, число, булево (true/false), null

В Python JSON обрабатывается модулем json из стандартной библиотеки.

Документация: json

Преобразование JSON-строки в объекты Python

json.loads() берёт строку и превращает её в dict/list.

import json

raw = '{"name": "Аня", "age": 18, "skills": ["python", "git"]}'
obj = json.loads(raw)

print(obj["name"])
print(obj["skills"][0])

Преобразование объектов Python в JSON-строку

json.dumps() превращает dict/list в строку JSON.

import json

data = {
    "task": "learn json",
    "done": False,
    "tags": ["python", "backend"]
}

text = json.dumps(data, ensure_ascii=False, indent=2)
print(text)

Что дают параметры:

  • ensure_ascii=False — чтобы кириллица не превращалась в \u041f...
  • indent=2 — красивое форматирование

Чтение и запись JSON-файлов

json.load() читает JSON из файла, а json.dump() записывает JSON в файл.

import json

payload = {
    "city": "Казань",
    "temp": 5
}

with open("weather.json", "w", encoding="utf-8") as f:
    json.dump(payload, f, ensure_ascii=False, indent=2)

with open("weather.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded["city"], loaded["temp"])

Частая проблема: не все объекты сериализуются

JSON не умеет напрямую хранить, например, set или Path. Поэтому перед записью их обычно преобразуют:

  • setlist
  • Pathstr
import json
from pathlib import Path

data = {
    "tags": list({"python", "api"}),
    "file": str(Path("data") / "input.txt")
}

print(json.dumps(data, ensure_ascii=False))

HTTP и API: как это работает

HTTP — протокол, по которому клиент (ваша программа) общается с сервером.

API — интерфейс, который сервер предоставляет для получения/отправки данных. Обычно это URL-адреса (эндпоинты), на которые можно делать HTTP-запросы.

Частые HTTP-методы

  • GET — получить данные
  • POST — создать данные
  • PUT/PATCH — обновить данные
  • DELETE — удалить данные

Статус-коды ответа

  • 200 — успех
  • 201 — создано (часто после POST)
  • 400 — ошибка клиента (неправильные параметры)
  • 401/403 — нет доступа
  • 404 — не найдено
  • 500 — ошибка сервера

HTTP-запросы в Python: requests

В стандартной библиотеке есть urllib.request, но в реальных проектах очень часто используют библиотеку requests, потому что она проще для новичка.

Документация: Requests

Установка (в активированном виртуальном окружении):

python -m pip install requests

Простой GET и разбор JSON

Возьмём учебный публичный API JSONPlaceholder.

import requests

url = "https://jsonplaceholder.typicode.com/todos/1"
resp = requests.get(url, timeout=10)

print(resp.status_code)
print(resp.headers.get("Content-Type"))

data = resp.json()  # превращает JSON в dict/list
print(data)
print("title:", data["title"])

Что здесь важно:

  • timeout=10 защищает от бесконечного ожидания
  • resp.json() удобнее, чем вручную делать json.loads(resp.text)

Параметры запроса (params)

Query-параметры удобно передавать словарём.

import requests

url = "https://jsonplaceholder.typicode.com/todos"
params = {"userId": 1}

resp = requests.get(url, params=params, timeout=10)
items = resp.json()

print("Количество задач:", len(items))
print("Первая задача:", items[0]["title"])

POST с JSON-телом (json=)

import requests

url = "https://jsonplaceholder.typicode.com/posts"
new_post = {
    "title": "Hello API",
    "body": "Post from python",
    "userId": 1
}

resp = requests.post(url, json=new_post, timeout=10)
print(resp.status_code)
print(resp.json())

Параметр json= делает две вещи:

  • сериализует словарь в JSON
  • ставит корректный заголовок Content-Type: application/json

Надёжность: ошибки сети и проверка ответа

Сетевой код почти всегда должен учитывать ошибки:

  • нет интернета
  • сервер не отвечает
  • сервер вернул неуспешный статус
  • пришёл не JSON

Обработка сетевых исключений

У requests есть общий тип исключений RequestException.

import requests

try:
    resp = requests.get("https://jsonplaceholder.typicode.com/todos/1", timeout=5)
    resp.raise_for_status()  # выбросит исключение для 4xx/5xx
    data = resp.json()
except requests.exceptions.Timeout:
    print("Сервер не ответил вовремя")
except requests.exceptions.HTTPError as e:
    print("HTTP ошибка:", e)
except requests.exceptions.RequestException as e:
    print("Ошибка сети:", e)
else:
    print("Успех:", data["title"])

Почему полезен raise_for_status():

  • вам не нужно вручную проверять status_code каждый раз
  • логика ошибки становится явной и попадает в except

Практический шаблон: маленький клиент для API

Ниже пример, который связывает темы курса:

  • модульность: вынесем API-код в отдельный файл
  • функции: сделаем понятные функции
  • исключения: аккуратно обработаем проблемы
  • коллекции: используем list/dict из JSON

Структура:

lesson-api/
  main.py
  api_client.py
  storage.py

api_client.py:

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

def fetch_todos(user_id, timeout=10):
    url = f"{BASE_URL}/todos"
    resp = requests.get(url, params={"userId": user_id}, timeout=timeout)
    resp.raise_for_status()
    return resp.json()

storage.py:

import json

def save_json(path, data):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

main.py:

import requests

from api_client import fetch_todos
from storage import save_json

def main():
    user_id = 1

    try:
        todos = fetch_todos(user_id)
    except requests.exceptions.RequestException as e:
        print("Не удалось загрузить данные:", e)
        return

    done = [t for t in todos if t.get("completed") is True]
    report = {
        "userId": user_id,
        "total": len(todos),
        "done": len(done),
        "titles_done": [t.get("title") for t in done],
    }

    save_json("report.json", report)
    print("Отчёт сохранён в report.json")

if __name__ == "__main__":
    main()

Обратите внимание на важные «джуновские» привычки:

  • функции маленькие и делают одно понятное действие
  • модули разделяют ответственность: сеть отдельно, хранение отдельно
  • сетевые ошибки не валят программу
  • данные из JSON обрабатываются как обычные коллекции

Если хочется сделать это через ООП

Когда логики и настроек становится много, удобно обернуть работу с API в класс.

import requests

class JsonPlaceholderClient:
    def __init__(self, base_url="https://jsonplaceholder.typicode.com", timeout=10):
        self.base_url = base_url
        self.timeout = timeout

    def fetch_todos(self, user_id):
        url = f"{self.base_url}/todos"
        resp = requests.get(url, params={"userId": user_id}, timeout=self.timeout)
        resp.raise_for_status()
        return resp.json()

Это продолжение темы ООП:

  • состояние клиента (base_url, timeout) хранится в объекте
  • методы используют это состояние

Что дальше

Теперь вы умеете:

  • читать и записывать файлы через with open(...)
  • сохранять и загружать структурированные данные в JSON
  • делать HTTP-запросы, передавать параметры и JSON-тело
  • обрабатывать типичные ошибки сети и HTTP-ответов

Следующий логичный шаг для уровня Junior — закреплять эти навыки на мини-проектах: консольный клиент к API, утилита, которая скачивает данные и строит отчёт, простое приложение с хранением данных в JSON и синхронизацией с сервером.

Инструменты джуна: Git, тестирование, стиль кода и проекты

Инструменты джуна: Git, тестирование, стиль кода и проекты

До этого вы учились писать рабочий код: функции, модули, исключения, коллекции, ООП, файлы, JSON и HTTP-запросы. На уровне Junior от вас ждут не только чтобы код работал, но и чтобы вы умели:

  • вести историю изменений в Git
  • доказывать корректность кода через тесты
  • поддерживать читаемость через стиль кода и автоинструменты
  • оформлять решение как проект, который можно запустить и поддерживать

Эти навыки делают ваш код командным, проверяемым и пригодным для реальной разработки.

Git: контроль версий как стандарт индустрии

Git — система контроля версий. Она хранит историю изменений, позволяет откатываться, вести разработку параллельно (ветки) и безопасно объединять изменения.

Официальные ресурсы:

Базовая модель Git

  • working directory — ваши файлы в папке проекта
  • staging area — “корзина” изменений, которые пойдут в коммит
  • repository — история коммитов

Инициализация репозитория и первый коммит

  1. Создайте репозиторий:
git init
  1. Проверьте статус:
git status
  1. Добавьте файлы в staging:
git add .
  1. Создайте коммит:
git commit -m "Initial commit"

Коммиты: как писать полезную историю

Коммит — это “атомарная” порция изменений.

Практические правила:

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

Пример понятных сообщений:

  • Add safe integer input helper
  • Fix JSON serialization for Path
  • Add tests for discount policy

.gitignore: что не должно попадать в репозиторий

В Python-проектах обычно игнорируют:

  • виртуальное окружение .venv/
  • кеш Python __pycache__/
  • файлы окружения .env

Пример .gitignore:

.venv/
__pycache__/
*.pyc
.env
.DS_Store

Ветки: параллельная разработка

Ветка — отдельная линия истории.

Базовый сценарий:

  • main — стабильная ветка
  • feature-ветки — для задач (например, feature/add-api-client)

Команды:

git branch

git checkout -b feature/add-tests

git checkout main

Чтобы объединить feature-ветку в main:

git merge feature/add-tests

Если возник конфликт, Git попросит вручную выбрать правильный вариант. Конфликты — нормальная часть командной работы.

Удалённый репозиторий: GitHub и повседневный цикл

Обычно удалённый репозиторий называется origin.

Связать локальный репозиторий с удалённым:

git remote add origin <URL>

Отправить изменения:

git push -u origin main

Забрать изменения:

git pull

В командной разработке часто используется модель:

  • создаёте feature-ветку
  • пушите её в origin
  • открываете Pull Request
  • проходите ревью и тесты
  • мёржите в main

Справка по Pull Request на GitHub:

Тестирование: как быстро проверять корректность кода

Тесты помогают:

  • ловить баги до пользователей
  • уверенно менять код (рефакторинг)
  • фиксировать ожидаемое поведение

Для Python-де-факто стандарт — pytest.

Что такое unit-тест

Unit-тест проверяет маленькую часть логики: функцию или метод.

Если вы писали функции в прошлых уроках (например, count_words, read_int, final_price), то большинство из них можно покрывать unit-тестами.

Установка pytest

В активированном виртуальном окружении:

python -m pip install pytest

Запуск:

pytest

Минимальная структура с тестами

Обычно тесты кладут в папку tests/.

Пример:

project/
  src/
    utils.py
  tests/
    test_utils.py

Файл src/utils.py:

def is_even(n: int) -> bool:
    return n % 2 == 0

Файл tests/test_utils.py:

from src.utils import is_even

def test_is_even_true():
    assert is_even(10) is True

def test_is_even_false():
    assert is_even(7) is False

Ключевая идея: тест — это обычная функция, которая делает assert.

Параметризация: меньше копипаста

Если нужно проверить много входов, используйте параметризацию.

import pytest
from src.utils import is_even

@pytest.mark.parametrize(
    "n, expected",
    [
        (0, True),
        (1, False),
        (2, True),
        (-3, False),
    ],
)
def test_is_even_parametrized(n, expected):
    assert is_even(n) is expected

Проверка исключений

В прошлых уроках вы поднимали ошибки через raise ValueError(...). Это удобно тестировать.

import pytest

def set_age(age: int) -> int:
    if age < 0:
        raise ValueError("Возраст не может быть отрицательным")
    return age

def test_set_age_raises():
    with pytest.raises(ValueError):
        set_age(-1)

Что тестировать в учебных проектах

  1. Чистые функции, которые:
  • принимают данные
  • возвращают результат
  • не ходят в сеть
  • не читают файлы напрямую
  1. Код с файлами и HTTP лучше проектировать так, чтобы его можно было тестировать отдельно:
  • функции, которые парсят JSON, тестируются без сети
  • функции, которые считают отчёт, тестируются на подготовленных list/dict

Стиль кода: читаемость, PEP 8 и автоинструменты

В реальной разработке ваш код читают чаще, чем пишут. Поэтому стиль — это не “красота”, а снижение стоимости поддержки.

PEP 8: базовые правила

PEP 8 — основной гайд по стилю Python.

Короткий минимум, который надо соблюдать:

  • отступы: 4 пробела
  • имена функций и переменных: snake_case
  • имена классов: PascalCase
  • длина строки: часто ориентируются на 88–120 символов (зависит от команды)
  • понятные имена важнее коротких

Type hints: подсказки типов для читаемости

Аннотации типов помогают:

  • быстрее понимать код
  • ловить часть ошибок статически
  • улучшать автодополнение в IDE

Документация:

Пример:

from typing import Iterable

def count_words(words: Iterable[str]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for w in words:
        counts[w] = counts.get(w, 0) + 1
    return counts

Если хочется проверять типы автоматически, используйте mypy:

Автоформатирование и линтинг

Чтобы не спорить о стиле, в командах обычно используют автоинструменты.

  1. Black — автоформаттер (делает код единообразным)

Установка и запуск:

python -m pip install black
black .
  1. Ruff — быстрый линтер (находит ошибки и плохие практики)

Установка и запуск:

python -m pip install ruff
ruff check .

Практический эффект для Junior:

  • меньше замечаний на ревью
  • быстрее учитесь хорошим практикам
  • проще поддерживать качество по мере роста проекта

Проектное оформление: как сделать работу “как на настоящей разработке”

Умение упаковать код в понятный проект — важный критерий уровня Junior.

Минимальная структура Python-проекта

Один из удобных вариантов для учебных проектов:

my_project/
  src/
    main.py
    api_client.py
    storage.py
    models.py
  tests/
    test_storage.py
    test_models.py
  README.md
  .gitignore
  requirements.txt

Почему так удобно:

  • src/ отделяет код от остального
  • tests/ рядом и понятно, где проверки
  • README.md объясняет, как запустить
  • requirements.txt фиксирует зависимости

README: что обязательно написать

README.md должен позволять другому человеку запустить проект.

Минимальный набор:

  • что делает проект (1–3 предложения)
  • требования: версия Python
  • как установить зависимости
  • как запустить
  • как прогнать тесты

Пример содержания (без привязки к конкретной теме):

# Название проекта

Короткое описание.

## Установка
python -m venv .venv
source .venv/bin/activate  # или Activate.ps1 на Windows
python -m pip install -r requirements.txt

## Запуск
python -m src.main

## Тесты
pytest

requirements.txt: зависимости

Если вы использовали requests, pytest, ruff, black, их нужно зафиксировать.

Пример requirements.txt:

requests
pytest
ruff
black

В командных проектах версии обычно фиксируют точнее, но для учебного проекта этого достаточно.

Точка входа и аккуратный main

Хорошая привычка: делать точку входа через main().

def main() -> None:
    print("Hello from project")

if __name__ == "__main__":
    main()

Так ваш код:

  • можно импортировать в тестах
  • не запускается сам по себе при импорте

Мини-проекты, которые хорошо смотрятся в портфолио

Ниже идеи, которые напрямую используют темы курса.

  • Консольный трекер задач

    • хранение задач в JSON-файле
    • функции для CRUD-операций
    • тесты на бизнес-логику
  • Клиент к публичному API

    • requests + обработка ошибок
    • сохранение результата в файл
    • отчёт по данным (агрегации списков/словарей)
  • Мини-магазин

    • товары и заказ через ООП
    • скидки через композицию
    • тесты на расчёт итоговой цены

Критерий “как у Junior”: проект должен быть небольшим, но аккуратно оформленным и проверяемым.

Как связать все инструменты в один рабочий процесс

Ниже реалистичный цикл разработки фичи на учебном проекте.

  1. Создайте ветку под задачу:
git checkout -b feature/report-json
  1. Реализуйте код небольшими шагами, делайте коммиты.

  2. Запустите форматирование и линтинг:

black .
ruff check .
  1. Прогоните тесты:
pytest
  1. Запушьте ветку и откройте Pull Request (если ведёте проект на GitHub).

Такой процесс показывает, что вы умеете работать по стандартам индустрии, а не просто писать “скрипты”.

Что дальше

После этого урока у вас есть важный набор “инструментов джуна”:

  • Git как базовый способ работы с историей и командой
  • pytest как способ подтверждать корректность
  • стиль кода через PEP 8 и автоинструменты
  • оформление проекта так, чтобы его можно было запускать и проверять

Дальше лучший путь — взять 1–2 мини-проекта из идей выше и довести их до состояния: чистая структура, README, зависимости, тесты и аккуратная история в Git.