Руководство по Python
Глава 1. Введение в Python
1. Язык программирования Python
Python — высокоуровневый язык общего назначения с динамической типизацией и автоматическим управлением памятью. На нём пишут скрипты автоматизации, веб-сервисы, настольные приложения, инструменты анализа данных и машинного обучения. Язык поддерживает процедурный, объектно-ориентированный и функциональный стили, а его стандартная библиотека и сторонние пакеты образуют большую экосистему.
В наиболее распространённой реализации CPython исходный файл сначала компилируется в промежуточный байткод, который затем исполняет виртуальная машина Python. Обычно эти этапы незаметны: разработчик передаёт интерпретатору файл .py и сразу получает результат. Динамическая типизация означает, что тип принадлежит объекту, а имя в программе в разные моменты может ссылаться на объекты разных типов.
Гайд и примеры ориентированы на Python 3.10 или новее: далее используются match, zip(strict=True) и dataclass(slots=True). Установленную версию проверяют командами python --version или python3 --version.
message = "Hello, Python!"
year = 2026
print(message)
print(type(year)) # <class 'int'>Сам интерпретатор содержит стандартную библиотеку. Дополнительные возможности устанавливаются пакетами: Django и FastAPI применяют в веб-разработке, NumPy и pandas — при обработке данных, PyTorch — в машинном обучении, а Tkinter и PySide — для графических интерфейсов.
2. Установка Python и первая программа на Windows
Для Windows установщик загружают с python.org/downloads. В мастере установки важно включить добавление Python в PATH; после установки версию можно проверить в PowerShell или cmd. Python Launcher (py) помогает выбирать установленную версию интерпретатора.
python --version
py --versionСоздадим hello.py:
name = input("Ваше имя: ")
print("Привет,", name)Запуск из каталога с файлом:
python hello.pyИнтерактивный режим запускается командой python: выражения выполняются сразу после ввода. Для выхода можно выполнить exit() или нажать Ctrl+Z, затем Enter. Для реальных программ удобнее хранить код в .py-файлах, чтобы его можно было повторять, тестировать и версионировать.
3. Установка Python и первая программа на MacOS
На macOS актуальный установщик можно получить с python.org/downloads. После установки Python 3 обычно вызывается командой python3, поскольку имя python может отсутствовать или относиться к другому окружению.
python3 --versionФайл hello.py может содержать тот же код:
name = input("Ваше имя: ")
print("Привет,", name)Перейдём в каталог программы и запустим файл:
cd ~/Documents/python
python3 hello.pyКоманда запуска должна использовать тот интерпретатор, для которого позже будут устанавливаться зависимости. Проверить его путь можно командой which python3.
4. Установка Python и первая программа на Linux
Во многих дистрибутивах Linux Python 3 уже установлен. Сначала стоит проверить версию, а при необходимости установить пакет средствами своего дистрибутива. Пример для Debian и Ubuntu:
python3 --version
sudo apt-get update
sudo apt-get install python3Интерактивный режим запускается командой python3. Для выхода достаточно exit() или Ctrl+D.
python3Обычная программа хранится в файле:
name = input("Введите имя: ")
print("Привет,", name)python3 hello.pyСистемный Python может использоваться самой ОС, поэтому не следует вручную заменять его файлы или устанавливать в него проектные пакеты с повышенными правами. Проектные зависимости безопаснее изолировать виртуальной средой.
5. Управление версиями Python на Windows, MacOS и Linux
Несколько версий нужны, когда проекты поддерживают разные диапазоны Python. Версию следует выбирать явно и проверять до создания виртуального окружения: окружение запоминает интерпретатор, которым было создано.
В Windows Python Launcher показывает установленные версии и запускает нужную:
py -0p
py -3.13 --version
py -3.13 hello.pyВ macOS и Linux параллельно установленные интерпретаторы обычно доступны под именами с минорной версией:
python3.13 --version
python3.13 hello.pyНе стоит переназначать системный /usr/bin/python3: это способно нарушить системные инструменты. Для разработки удобен менеджер версий, например pyenv: он хранит версии отдельно и выбирает одну глобально либо для конкретного каталога. После установки самого pyenv типичный сценарий выглядит так:
pyenv install 3.13.5
pyenv local 3.13.5
python --version6. Первая программа в PyCharm
PyCharm — кроссплатформенная IDE с подсветкой синтаксиса, автодополнением, запуском, тестами и отладчиком. При создании проекта нужно проверить выбранный Python Interpreter: это может быть системный интерпретатор или отдельная виртуальная среда проекта.
В main.py поместим программу:
name = input("Введите ваше имя: ")
print("Привет,", name)Файл запускается через команду Run или зелёную стрелку. PyCharm создаёт конфигурацию запуска, где зафиксированы файл, рабочий каталог, интерпретатор и переменные окружения. Если IDE запускает не ту версию Python, сначала следует проверить именно конфигурацию интерпретатора проекта.
7. Python в Visual Studio
Полноценная поддержка Python в Visual Studio предназначена для Windows. В Visual Studio Installer выбирается workload «Python development», после чего можно создать проект Python Application, выбрать окружение и запустить стартовый файл. Не следует путать Visual Studio с кроссплатформенным редактором Visual Studio Code.
print("Hello Python from Visual Studio!")В структуре проекта Visual Studio показывает исходные файлы, используемые Python Environments, зависимости и пути поиска модулей. Запуск зелёной кнопкой использует выбранное окружение; тот же файл остаётся обычным .py-скриптом и может выполняться из терминала.
Глава 2. Основы Python
1. Введение в написание программ
Программа состоит из инструкций и выражений. Python использует переводы строк и отступы как часть синтаксиса: вложенный блок должен иметь одинаковый отступ, обычно четыре пробела. Точка с запятой допустима, но в обычном коде одну инструкцию размещают на одной строке.
name = "Ада" # однострочный комментарий
if name:
print("Привет,", name)Имена чувствительны к регистру: name и Name — разные имена. Комментарий начинается с #. Тройные кавычки создают строковый литерал, а не специальный «многострочный комментарий»; такие строки применяют, в частности, как документацию модулей, классов и функций.
def greet(name):
"""Возвращает приветствие для пользователя."""
return f"Привет, {name}!"
print(greet("Мира"))2. Переменные и типы данных
Переменная в Python — имя, связанное с объектом. Предварительно объявлять тип не нужно: присваивание создаёт или переназначает связь. Идентификатор может содержать буквы, цифры и _, но не начинается с цифры и не совпадает с ключевым словом.
Основные скалярные типы: bool, int, float, complex, str и NoneType. Целые числа имеют произвольную точность, а float обычно реализован двоичным числом двойной точности, поэтому не все десятичные дроби представимы точно.
active = True
count = 42
price = 19.95
title = "Python"
missing = None
print(type(price)) # <class 'float'>
print(isinstance(count, int)) # TrueМожно присвоить несколько значений одновременно. Константы технически не защищены от изменения, но по соглашению записываются прописными буквами. Аннотация типа сообщает намерение инструментам и читателю, но сама по себе не запрещает присвоить объект другого типа.
x, y = 10, 20
MAX_RETRIES: int = 33. Консольный ввод и вывод
input() печатает приглашение, читает строку до Enter и всегда возвращает str. Если нужно число, строку преобразуют явно. Ошибку неверного ввода позже можно обработать исключением.
name = input("Имя: ")
age = int(input("Возраст: "))
print(f"{name}, через год вам будет {age + 1}")print() принимает несколько объектов. Параметр sep задаёт разделитель, end — окончание вывода; по умолчанию это пробел и перевод строки соответственно.
print("2026", "08", "23", sep="-")
print("Загрузка", end="... ")
print("готово")4. Арифметические операции с числами
Python поддерживает +, -, *, /, //, % и **. Оператор / всегда возвращает вещественный результат, // выполняет деление с округлением вниз, % даёт остаток, а ** возводит в степень. Унарные + и - задают знак.
a = 17
b = 5
print(a + b) # 22
print(a / b) # 3.4
print(a // b) # 3
print(a % b) # 2
print(b ** 2) # 25Приоритет можно сделать явным скобками. Для float следует учитывать погрешность двоичного представления: сравнение результатов вычислений часто делают через math.isclose(), а для точных денежных расчётов используют decimal.Decimal.
from math import isclose
result = 0.1 + 0.2
print(isclose(result, 0.3)) # True5. Поразрядные операции с числами
Поразрядные операторы работают с двоичным представлением целых чисел: & оставляет биты, установленные в обоих операндах; | — хотя бы в одном; ^ — только в одном; ~ инвертирует; << и >> сдвигают биты. Они применяются для масок, флагов и низкоуровневых протоколов.
READ = 0b001
WRITE = 0b010
EXECUTE = 0b100
permissions = READ | WRITE
print(bool(permissions & WRITE)) # True
print(bool(permissions & EXECUTE)) # False
permissions ^= WRITE
print(bin(permissions)) # 0b1Для неотрицательных чисел сдвиг влево на n позиций соответствует умножению на 2 ** n, вправо — целочисленному делению на эту степень. Отрицательные целые ведут себя как числа с бесконечным знаковым расширением, поэтому их битовые операции нужно трактовать осторожно.
6. Условные выражения
Сравнения ==, !=, <, <=, > и >= возвращают bool. Операторы and, or, not объединяют условия; сравнения можно связывать в цепочки. Проверка принадлежности выполняется через in, а идентичности объектов — через is.
age = 25
has_ticket = True
allowed = 18 <= age < 65 and has_ticket
print(allowed) # True
print("py" in "python") # True== сравнивает значения, is — являются ли две ссылки одним объектом. is обычно используют с одиночными объектами вроде None: value is None. and и or вычисляются лениво и возвращают один из операндов, а не обязательно bool.
nickname = ""
display_name = nickname or "Гость"
print(display_name)7. Условная конструкция if
if выполняет блок, когда условие истинно; elif проверяет следующие варианты, else обрабатывает остаточный случай. Отступы определяют границы блоков.
score = 82
if score >= 90:
grade = "A"
elif score >= 75:
grade = "B"
elif score >= 60:
grade = "C"
else:
grade = "F"
print(grade)В условии ложными считаются False, None, числовой ноль и пустые коллекции или строки; остальные объекты обычно истинны. Условное выражение выбирает одно из двух значений: status = "adult" if age >= 18 else "minor".
8. Циклы
while повторяет блок, пока условие истинно. for перебирает элементы любого итерируемого объекта; для последовательности чисел часто используется range(). break завершает ближайший цикл, continue переходит к следующей итерации.
total = 0
for number in range(1, 6):
if number == 3:
continue
total += number
print(total) # 12У цикла может быть else: этот блок выполняется, если цикл завершился без break. Это удобно, например, при поиске.
values = [4, 7, 10]
for value in values:
if value % 2 == 1:
print("Найдено нечётное:", value)
break
else:
print("Нечётных чисел нет")9. Функции
Функция объединяет повторно используемые инструкции. Она определяется через def; тело выполняется только при вызове. Имя функции следует выбирать как действие, а небольшая функция обычно решает одну задачу.
def print_banner():
print("=== Python ===")
print_banner()
print_banner()Пустое тело синтаксически недопустимо, поэтому временную заглушку записывают оператором pass. Он ничего не делает и лишь позволяет оставить блок пустым; это не реализация поведения.
def planned_feature():
passФункцию нужно определить до того места, где её вызов выполняется. Документирующая строка помещается первой инструкцией тела и доступна через help() или атрибут __doc__.
10. Параметры функции
Параметры объявляются в определении функции, аргументы передаются при вызове. Аргументы можно передавать по позиции или по имени; параметры со значением по умолчанию должны следовать после обязательных.
def describe(name, age=18, *, city="Москва"):
return f"{name}, {age}, {city}"
print(describe("Анна", city="Казань"))Символ / завершает позиционно-обязательные параметры, а * начинает параметры, доступные только по имени. *args собирает лишние позиционные аргументы в кортеж. Изменяемые объекты не следует использовать как значения по умолчанию: они создаются один раз при определении функции.
def collect(value, items=None):
if items is None:
items = []
items.append(value)
return items
def average(*numbers):
return sum(numbers) / len(numbers)
print(collect(3))
print(average(2, 4, 6))11. Оператор return и возвращение результата из функции
return немедленно завершает функцию и передаёт значение вызывающему коду. Если return отсутствует или указан без выражения, результатом будет None. Возвращаемое значение можно сохранить, передать дальше или проигнорировать.
def divide(dividend, divisor):
if divisor == 0:
return None
return dividend / divisor
result = divide(10, 4)
print(result) # 2.5Несколько результатов обычно возвращаются как один кортеж, который можно сразу распаковать.
def min_max(numbers):
return min(numbers), max(numbers)
minimum, maximum = min_max([8, 2, 11])
print(minimum, maximum)12. Функция как тип, параметр и результат другой функции
Функции в Python — объекты первого класса: их можно присваивать переменным, хранить в коллекциях, передавать другим функциям и возвращать из функций. Имя без скобок означает сам объект функции, со скобками — её вызов.
def square(number):
return number * number
operation = square
print(operation(5)) # 25Функция, принимающая другую функцию, называется функцией высшего порядка. Такой приём позволяет отделить алгоритм от конкретной операции.
def apply_to_all(values, transform):
return [transform(value) for value in values]
def double(number):
return number * 2
print(apply_to_all([1, 2, 3], double))13. Лямбда-выражения
lambda создаёт небольшую анонимную функцию из одного выражения. Параметры записываются до двоеточия, результат выражения возвращается автоматически. Лямбды удобны как короткие ключи сортировки или одноразовые преобразования.
users = [("Анна", 31), ("Борис", 24), ("Вера", 28)]
users.sort(key=lambda user: user[1])
print(users)В lambda нельзя поместить несколько инструкций, присваивание или полноценный try. Если логика требует имени, документации или нескольких шагов, обычная def-функция читается лучше.
14. Преобразование типов
Встроенные функции int(), float(), str() и bool() создают значение нужного типа, если исходный объект допускает преобразование. input() возвращает строку, поэтому числа из консоли почти всегда приходится преобразовывать явно.
count_text = "12"
count = int(count_text)
price = float("19.90")
print(count + 3) # 15
print(str(price))Для int() можно указать основание системы счисления. Невозможное преобразование строки создаёт ValueError; потеря дробной части при int(3.9) происходит усечением к нулю. bool() возвращает False для нуля, None и пустых контейнеров или строк.
binary = int("101101", 2)
print(binary) # 45
print(bool([])) # False
print(bool("0")) # True: строка непустая15. Область видимости переменных
Поиск имени следует правилу LEGB: Local, Enclosing, Global, Built-in. Присваивание внутри функции по умолчанию создаёт локальное имя и не меняет одноимённую глобальную переменную.
rate = 10
def calculate(value):
rate = 2 # локальное имя
return value * rate
print(calculate(5)) # 10
print(rate) # 10global разрешает переназначить глобальное имя, а nonlocal — имя во внешней функции. Частое изменение глобального состояния усложняет тестирование, поэтому обычно значения передают параметрами и возвращают через return.
16. Замыкания
Замыкание — функция, которая сохраняет доступ к именам из окружения, где была создана, даже после завершения внешней функции. Так можно создавать функции с настроенным поведением и хранить небольшое приватное состояние.
def make_multiplier(factor):
def multiply(value):
return value * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))Чтобы изменять сохранённое имя, применяется nonlocal.
def make_counter():
count = 0
def next_value():
nonlocal count
count += 1
return count
return next_value
counter = make_counter()
print(counter(), counter()) # 1 217. Декораторы
Декоратор получает вызываемый объект и возвращает замену — обычно функцию-обёртку. Запись @decorator над функцией эквивалентна function = decorator(function). Обёртка может выполнять действия до и после вызова, проверять аргументы или преобразовывать результат.
from functools import wraps
def traced(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Вызов {function.__name__}")
return function(*args, **kwargs)
return wrapper
@traced
def add(left, right):
return left + right
print(add(2, 3))*args и **kwargs сохраняют универсальность обёртки, а functools.wraps переносит имя и документацию исходной функции. Если декоратору нужны настройки, создают внешнюю функцию, которая принимает эти настройки и возвращает сам декоратор.
Глава 3. Объектно-ориентированное программирование
1. Классы и объекты
Класс описывает новый тип: набор данных и операций над ними. Объект, или экземпляр, — конкретное значение этого типа. Класс создаётся инструкцией class, методы определяются как функции внутри него, а первый параметр обычного метода по соглашению называется self и ссылается на текущий экземпляр.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Меня зовут {self.name}, мне {self.age}"
person = Person("Анна", 30)
print(person.greet())Имя __init__ окружено двойными подчёркиваниями, потому что это специальный метод протокола модели данных Python. Такие методы часто называют dunder-методами. При выражении Person(...) Python сначала создаёт экземпляр через __new__, а затем вызывает __init__ для его начальной настройки. Поэтому __init__ привычно называют конструктором, но точнее это инициализатор уже созданного объекта; он должен вернуть None.
Атрибуты self.name и self.age принадлежат отдельному экземпляру. Тело класса не может быть пустым: для временного пустого класса используют pass, который не выполняет действий.
class Placeholder:
pass
item = Placeholder()
item.value = 10 # атрибут можно добавить динамически2. Инкапсуляция, атрибуты и свойства
Инкапсуляция объединяет состояние и операции и ограничивает прямую зависимость внешнего кода от внутреннего устройства объекта. В Python это в основном соглашения, а не абсолютная защита: имя _balance сообщает «внутренняя деталь», а __balance включает name mangling — преобразование имени примерно в _Account__balance, которое защищает прежде всего от случайных конфликтов в наследниках.
Свойство предоставляет управляемый доступ как к обычному атрибуту. Декоратор @property определяет чтение, @name.setter — присваивание. Это позволяет добавить проверку, не меняя публичный синтаксис пользователя класса.
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = 0
self.balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Баланс не может быть отрицательным")
self.__balance = value
account = Account("Анна", 100)
account.balance += 50
print(account.balance)Без setter свойство доступно только для чтения. В Python обычно начинают с простых публичных атрибутов и вводят свойства, когда действительно появляется инвариант или вычисляемое значение.
3. Наследование
Наследование создаёт класс на основе другого класса. Производный класс получает доступ к методам базового и может добавлять собственное поведение. Отношение должно выражать «является»: Employee является Person. Для включения возможностей без такого отношения чаще подходит композиция.
class Person:
def __init__(self, name):
self.name = name
def describe(self):
return self.name
class Employee(Person):
def __init__(self, name, role):
super().__init__(name)
self.role = role
def work(self):
return f"{self.name} работает как {self.role}"
employee = Employee("Ирина", "разработчик")
print(employee.describe())
print(employee.work())super() предоставляет следующую реализацию по порядку разрешения методов (MRO), а не просто «родителя». Это важно при множественном наследовании. Проверить отношения можно через isinstance(employee, Person) и issubclass(Employee, Person).
Python поддерживает множественное наследование, но его стоит применять осознанно: маленькие классы-примеси удобны, а пересекающееся состояние и сложные иерархии затрудняют понимание MRO.
4. Переопределение функционала базового класса
Переопределение происходит, когда наследник объявляет метод с тем же именем. Вызов через объект наследника найдёт новую реализацию; при необходимости она может расширить базовую через super().
class Notification:
def send(self, message):
return f"Сообщение: {message}"
class EmailNotification(Notification):
def send(self, message):
prepared = super().send(message)
return f"Email отправлен — {prepared}"
notification = EmailNotification()
print(notification.send("Сборка завершена"))Так проявляется полиморфизм: код может вызывать общий метод send, не зная конкретный класс объекта. В Python совместимость часто определяется поддерживаемым поведением, а не обязательным наследованием — это называют duck typing.
Если наследник определяет собственный __init__, базовый __init__ автоматически не вызывается. Нужный вызов делают явно через super().__init__(...), иначе базовая часть объекта может остаться неинициализированной.
5. Атрибуты классов и статические методы
Атрибут класса хранится на самом классе и обычно разделяется всеми экземплярами. Атрибут экземпляра хранится отдельно и при совпадении имени перекрывает атрибут класса для этого объекта. Изменяемый атрибут класса вроде списка тоже общий, что нередко становится источником ошибок.
class User:
species = "human"
created = 0
def __init__(self, name):
self.name = name
type(self).created += 1
first = User("Анна")
second = User("Борис")
print(User.created) # 2@classmethod получает класс в параметре cls и удобен для альтернативных конструкторов, учитывающих наследование. @staticmethod не получает автоматически ни экземпляр, ни класс; это функция, логически относящаяся к пространству имён класса.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, value):
return cls((value - 32) * 5 / 9)
@staticmethod
def is_freezing(value):
return value <= 0
temperature = Temperature.from_fahrenheit(32)
print(temperature.celsius)
print(Temperature.is_freezing(temperature.celsius))6. Класс object. Строковое представление объекта
Все классы Python прямо или косвенно наследуют object, поэтому получают базовые специальные методы. str(obj) и print(obj) используют obj.__str__() для понятного пользователю текста. repr(obj) и отображение объекта в интерактивной консоли используют obj.__repr__() для однозначного диагностического представления.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name}: {self.price:.2f} ₽"
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price!r})"
product = Product("Книга", 799.0)
print(str(product))
print(repr(product))Если __str__ не определён, Python использует __repr__. Оба метода должны возвращать строку. Для __repr__ полезен идеал «текст, из которого можно восстановить объект», но для сложных или небезопасных случаев достаточно информативного диагностического вида. Секреты и персональные данные в представление включать нельзя.
Двойные подчёркивания означают, что методы вызываются синтаксисом и встроенными функциями языка. Обычно пишут str(product), а не вызывают product.__str__() напрямую.
7. Перегрузка операторов
Операторы Python делегируют работу специальным методам. Например, left + right пробует left.__add__(right), == связано с __eq__, проверка истинности — с __bool__ или __len__, а индексирование — с __getitem__. Это позволяет встроить пользовательский тип в обычный синтаксис.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
print(Vector(1, 2) + Vector(3, 4))
print(Vector(1, 2) == Vector(1, 2))Для неподдерживаемого типа бинарный метод должен вернуть NotImplemented, а не выбросить его как исключение: тогда Python сможет попробовать отражённую операцию второго объекта или сформировать корректный TypeError. Перегруженный оператор должен сохранять ожидаемый смысл; неожиданное поведение делает API трудным для чтения.
8. Абстрактные классы и методы
Абстрактный базовый класс задаёт обязательный интерфейс для наследников. Класс наследуется от abc.ABC, а незавершённые методы помечаются @abstractmethod. Пока наследник не реализует все абстрактные методы, создать его экземпляр нельзя.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key, value):
pass
@abstractmethod
def load(self, key):
pass
class MemoryStorage(Storage):
def __init__(self):
self._data = {}
def save(self, key, value):
self._data[key] = value
def load(self, key):
return self._data[key]
storage = MemoryStorage()
storage.save("language", "Python")
print(storage.load("language"))pass в абстрактном методе — синтаксическая заглушка: фактическую работу обязан предоставить наследник. Абстрактный метод может иметь и базовую реализацию, которую наследник вызывает через super(). Для простых контрактов без общего состояния в современном Python также часто подходят структурные протоколы typing.Protocol.
Глава 4. Обработка ошибок и исключений
1. Конструкция try…except…finally
Исключение прерывает обычный поток выполнения и переносит управление ближайшему подходящему обработчику. В блок try помещают минимальную операцию, которая может завершиться ожидаемой ошибкой; except обрабатывает её, else выполняется только без исключения, а finally — всегда, даже при return.
def parse_ratio(text):
try:
left, right = text.split("/")
result = int(left) / int(right)
except (ValueError, ZeroDivisionError):
return None
else:
return result
finally:
print("Попытка разбора завершена")
print(parse_ratio("10/4"))
print(parse_ratio("10/0"))finally применяют для обязательного освобождения ресурса, однако для файлов и блокировок чаще удобнее контекстный менеджер with. Пустой except: ловит даже системные сигналы завершения и обычно слишком широк; практический верхний уровень для прикладных ошибок — except Exception.
2. except и обработка разных типов исключений
Обработчики перечисляют от частного к общему: первый совпавший except получает управление. Частые исключения включают ValueError для недопустимого значения, TypeError для неверного типа операции, KeyError и IndexError для отсутствующего элемента, ZeroDivisionError, OSError для системных ошибок и ModuleNotFoundError для импорта.
def ratio_from_mapping(data):
try:
numerator = int(data["numerator"])
denominator = int(data["denominator"])
return numerator / denominator
except KeyError as error:
print("Нет ключа:", error)
except ValueError as error:
print("Не число:", error)
except ZeroDivisionError:
print("Делить на ноль нельзя")
return None
print(ratio_from_mapping({"numerator": "8", "denominator": "2"}))Конструкция except SomeError as error сохраняет объект исключения: в нём есть сообщение и иногда дополнительные атрибуты. Объединять типы в кортеж стоит лишь тогда, когда для них действительно одинаковая реакция. Не следует подавлять исключение без исправления ситуации или полезного сообщения: это скрывает дефекты.
3. Генерация исключений и создание своих типов исключений
raise явно сообщает, что функция не может выполнить контракт. Лучше выбирать конкретный встроенный тип: ValueError для неверного значения, TypeError для неподдерживаемого типа. Голый raise внутри except повторно поднимает текущее исключение с исходным traceback.
def set_age(age):
if not 1 <= age <= 120:
raise ValueError("Возраст должен быть от 1 до 120")
return age
try:
set_age(-5)
except ValueError as error:
print(error)Собственные исключения обычно наследуются от Exception и выражают ошибку предметной области. Полезные данные можно сохранить атрибутами; базовый класс получит текст сообщения через super().__init__().
class InsufficientFundsError(Exception):
def __init__(self, balance, requested):
self.balance = balance
self.requested = requested
super().__init__(
f"Недостаточно средств: баланс {balance}, запрошено {requested}"
)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(100, 150)
except InsufficientFundsError as error:
print(error)При преобразовании одной ошибки в другую запись raise DomainError(...) from error сохраняет явную цепочку причин. Исключения должны обозначать исключительные ситуации, а не заменять обычные ветвления там, где отсутствие результата ожидаемо.
Глава 5. Списки, кортежи и словари
1. Списки
list — изменяемая упорядоченная последовательность. Элементы могут иметь разные типы, повторяться и извлекаться по индексу; отрицательные индексы считают от конца. Срез items[start:stop:step] создаёт новый список, а правая граница не включается.
languages = ["Python", "Rust", "Go"]
languages.append("Java")
languages[1] = "TypeScript"
print(languages[0]) # Python
print(languages[-1]) # Java
print(languages[1:3])Основные операции изменения: append() добавляет один объект, extend() — элементы итерируемого объекта, insert() вставляет по индексу, remove() удаляет первое равное значение, pop() удаляет и возвращает элемент, clear() очищает список. len(), in, index() и count() помогают исследовать содержимое.
numbers = [4, 1, 3]
numbers.extend([2, 3])
last = numbers.pop()
numbers.sort()
print(numbers) # [1, 2, 3, 4]
print(last, 3 in numbers)Присваивание copy = original не копирует список: оба имени указывают на один объект. Поверхностную копию создают original.copy() или original[:]; вложенные изменяемые объекты при этом остаются общими. Для независимой копии всей вложенной структуры существует copy.deepcopy().
Сортировка list.sort() меняет список на месте и возвращает None, а sorted() создаёт новый список из любого итерируемого объекта. Параметры key и reverse задают ключ и направление.
users = [{"name": "Анна", "age": 30}, {"name": "Борис", "age": 22}]
ordered = sorted(users, key=lambda user: user["age"])
print(ordered)2. Кортежи
tuple — упорядоченная неизменяемая последовательность. После создания нельзя заменить, добавить или удалить её элемент. Неизменяемость делает кортеж удобным для фиксированной записи и позволяет использовать его как ключ словаря, если все вложенные элементы тоже хешируемы.
point = (10, 20)
rgb = 255, 128, 0
single = (42,) # запятая создаёт кортеж из одного элемента
print(point[0])
print(rgb[-1])
print(len(single))Скобки часто можно опустить: определяющей является запятая. Кортеж поддерживает индексы, срезы, перебор, in, count() и index(), но не изменяющие методы списка. Сам кортеж неизменяем, однако может содержать изменяемый объект.
record = ("task", ["new"])
record[1].append("urgent")
print(record) # сам список внутри кортежа изменился3. Диапазоны
range представляет неизменяемую последовательность целых чисел и хранит границы, а не готовый список всех значений. Формы вызова: range(stop), range(start, stop) и range(start, stop, step). Правая граница исключается, шаг не может быть нулём.
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(2, 8, 2))) # [2, 4, 6]
print(list(range(5, 0, -1))) # [5, 4, 3, 2, 1]Чаще всего range() используется в for, когда нужны именно номера. Если есть готовая коллекция, лучше перебирать её элементы напрямую, а для индекса вместе со значением использовать enumerate().
names = ["Анна", "Борис", "Вера"]
for index, name in enumerate(names, start=1):
print(index, name)4. Словари
dict — изменяемое отображение уникальных хешируемых ключей в значения. В современных версиях Python словарь сохраняет порядок вставки, однако его основное назначение — быстрый доступ по ключу, а не индексам позиции.
user = {"name": "Анна", "age": 30}
user["city"] = "Казань"
user["age"] = 31
print(user["name"])
print(user.get("email", "не указан"))mapping[key] создаёт KeyError, если ключа нет; get() возвращает None или заданное значение по умолчанию. setdefault() добавляет значение только при отсутствии ключа, update() объединяет данные, pop() удаляет ключ и возвращает значение. Оператор in проверяет ключи.
Методы keys(), values() и items() возвращают динамические представления словаря. Для перебора пар используют items().
scores = {"Анна": 10, "Борис": 7}
for name, score in scores.items():
print(f"{name}: {score}")
scores.update({"Вера": 9})
removed = scores.pop("Борис")
print(removed)Ключ должен иметь стабильный хеш: подходят строки, числа и кортежи из хешируемых элементов, но не списки и словари. При поверхностном копировании через copy() вложенные изменяемые значения остаются разделяемыми.
5. Множества
set — изменяемая неупорядоченная коллекция уникальных хешируемых элементов. Множество удобно для устранения дублей, быстрых проверок принадлежности и математических операций. Пустое множество создаётся только как set(), поскольку {} означает пустой словарь.
tags = {"python", "backend", "python"}
tags.add("api")
tags.discard("unknown") # ошибки нет, даже если элемента нет
print(tags)
print("backend" in tags)remove() в отличие от discard() создаёт KeyError для отсутствующего элемента. Объединение записывается a | b, пересечение — a & b, разность — a - b, симметрическая разность — a ^ b. Также доступны проверки подмножества <= и надмножества >=.
backend = {"python", "sql", "http"}
data = {"python", "sql", "pandas"}
print(backend & data) # {'python', 'sql'}
print(backend | data)
print(backend - data) # {'http'}frozenset — неизменяемый вариант множества; его можно использовать как ключ словаря или элемент другого множества.
6. List comprehension
List comprehension компактно создаёт список из итерируемого источника. Базовая форма: [expression for item in source]; после цикла можно добавить условие отбора. Выражение вычисляется только для прошедших фильтр элементов.
squares = [number ** 2 for number in range(1, 6)]
even_squares = [number ** 2 for number in range(1, 11) if number % 2 == 0]
print(squares)
print(even_squares)Условное выражение перед for не фильтрует, а выбирает значение для каждого элемента.
labels = ["even" if number % 2 == 0 else "odd" for number in range(5)]
print(labels)Вложенные comprehensions допустимы, но сложную логику лучше раскрывать в обычные циклы. Похожий синтаксис создаёт множество {expression for ...}, словарь {key: value for ...} или ленивое генераторное выражение (expression for ...).
7. Упаковка и распаковка
При упаковке несколько значений объединяются в кортеж; при распаковке элементы итерируемого объекта присваиваются нескольким целям. Количество обычных целей должно совпадать с количеством элементов.
coordinates = 10, 20 # упаковка
x, y = coordinates # распаковка
x, y = y, x # обмен без временной переменной
print(x, y)Одна цель со звёздочкой собирает оставшиеся элементы в новый список. Она может стоять в начале, середине или конце, но в одном присваивании допускается только одна такая цель.
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5Звёздочка также распаковывает итерируемые объекты внутри списков, кортежей и множеств, а ** распаковывает пары ключ–значение внутри нового словаря. Более позднее значение одинакового ключа перекрывает раннее.
defaults = {"theme": "light", "page_size": 20}
custom = {"theme": "dark"}
settings = {**defaults, **custom}
numbers = [0, *range(1, 4), 4]
print(settings)
print(numbers)8. Упаковка и распаковка в параметрах функций
В определении функции *args собирает лишние позиционные аргументы в кортеж, а **kwargs — именованные аргументы в словарь. Имена args и kwargs являются соглашением; значение имеют звёздочки.
def build_message(prefix, *parts, separator=" ", **metadata):
text = separator.join(parts)
return f"{prefix}: {text} | {metadata}"
print(build_message("INFO", "server", "started", port=8000))При вызове *iterable раскладывает элементы по позиционным параметрам, а **mapping — значения по совпадающим именам параметров. Это удобно для адаптации коллекций к существующему API.
def rectangle_area(width, height):
return width * height
sides = (5, 8)
options = {"width": 4, "height": 7}
print(rectangle_area(*sides))
print(rectangle_area(**options))При ** ключи должны быть строками и соответствовать допустимым именам параметров; повторная передача одного аргумента приводит к TypeError. Слишком универсальная сигнатура затрудняет понимание API, поэтому *args и **kwargs используют, когда набор аргументов действительно переменный или функция прозрачно делегирует вызов.
9. Функция zip
zip() лениво объединяет несколько итерируемых объектов в последовательность кортежей: первый элемент каждого источника, затем второй и так далее. По умолчанию результат заканчивается вместе с самым коротким источником.
names = ["Анна", "Борис", "Вера"]
scores = [10, 7, 9]
for name, score in zip(names, scores):
print(name, score)
score_by_name = dict(zip(names, scores))
print(score_by_name)Начиная с Python 3.10 параметр strict=True создаёт ValueError, если длины источников различаются. Это полезно, когда молчаливое усечение означало бы потерю данных.
columns = ["id", "name"]
row = [1, "Анна"]
record = dict(zip(columns, row, strict=True))
print(record)Обратная операция тоже строится через распаковку: x_values, y_values = zip(*points). Поскольку zip возвращает итератор, после одного полного прохода он исчерпан; если нужны повторные проходы, результат превращают в list или создают zip заново.
Глава 6. Модули и пакеты
1. Определение и подключение модулей
Модуль — это файл .py, а пакет — каталог с модулями. При первом import Python находит модуль через систему импорта, создаёт для него объект, выполняет верхнеуровневый код и помещает результат в sys.modules. Повторный импорт в том же процессе обычно возвращает уже созданный объект, поэтому на уровне модуля не стоит выполнять неожиданные сетевые запросы или другую тяжёлую работу.
import module сохраняет пространство имён и делает зависимости заметными. from module import name добавляет выбранное имя в текущую область видимости, а from module import * может незаметно перезаписать имена, поэтому в прикладном коде его избегают. Поиск начинается среди встроенных модулей и продолжается по путям из sys.path; туда входят, в частности, каталог запуска, стандартная библиотека и пакеты активного окружения.
# formatter.py — отдельный модуль
PREFIX = "INFO"
def format_message(text: str) -> str:
return f"[{PREFIX}] {text}"# main.py — самостоятельный файл рядом с formatter.py
import formatter
def main() -> None:
print(formatter.format_message("server started"))
if __name__ == "__main__":
main()При запуске файла его __name__ равен "__main__", а при импорте — полному имени модуля. Проверка выше отделяет определения, пригодные для импорта, от кода запуска. Современный пакет может быть namespace-пакетом без __init__.py, но обычный __init__.py по-прежнему полезен: он явно обозначает пакет и позволяет сформировать его публичный API. Относительные импорты (from .models import User) применяют внутри пакета; точкой входа пакет удобнее запускать командой python3 -m package.module.
2. Генерация байткода модулей
Перед выполнением CPython разбирает исходный текст, строит синтаксическое дерево и компилирует его в байткод для виртуальной машины Python. Это не машинный код процессора. При импорте байткод модуля может кэшироваться в __pycache__ как .pyc; имя файла включает версию интерпретатора, потому что формат байткода не является стабильным между версиями Python.
Кэш ускоряет загрузку, но не меняет семантику программы. Python сверяет его с исходником и пересоздаёт, когда он устарел; удалять __pycache__ для обычного запуска не требуется. Главный скрипт при простом запуске обычно не получает .pyc, а импортируемые модули — получают, если каталог доступен для записи. Предварительно скомпилировать дерево исходников можно стандартным модулем compileall.
python3 -m compileall srcИнструкции конкретной функции удобно исследовать через dis. Такой вывод объясняет работу CPython, но не является контрактом языка и может измениться после обновления интерпретатора.
import dis
def total(a: int, b: int) -> int:
return a + b
dis.dis(total)3. Модуль random
random генерирует псевдослучайные значения: последовательность определяется внутренним состоянием генератора. Это удобно для симуляций, игр, перемешивания и тестовых данных, но непригодно для паролей, токенов и ключей. seed() позволяет воспроизвести эксперимент, а не сделать его безопаснее.
Основные операции: randint(a, b) включает обе границы; randrange() работает как случайный вариант range; choice() выбирает один элемент; choices() выбирает с возвращением; sample() — без возвращения; shuffle() перемешивает изменяемую последовательность на месте.
from random import Random
rng = Random(42) # отдельный воспроизводимый генератор
teams = ["red", "blue", "green", "yellow"]
print(rng.choice(teams))
print(rng.sample(teams, k=2))
print(rng.randint(1, 6))Создание собственного Random не меняет глобальное состояние модуля и упрощает тестирование: объект генератора можно передать как зависимость.
4. Модуль secrets
secrets получает случайность из криптографически стойкого источника операционной системы. Его выбирают для токенов сброса пароля, session ID, одноразовых ссылок и других значений, которые не должен угадывать атакующий. Функции token_bytes, token_hex и token_urlsafe возвращают один и тот же тип энтропии в разных представлениях.
import secrets
reset_token = secrets.token_urlsafe(32)
verification_code = f"{secrets.randbelow(1_000_000):06d}"
print(reset_token)
print(verification_code)При сравнении секретов используют secrets.compare_digest(): она уменьшает утечку информации через различия во времени сравнения. Токен всё равно следует хранить безопасно, ограничивать срок его жизни и по возможности сохранять в базе только хеш.
5. Математические функции и модуль math
Встроенные abs, round, min, max, sum закрывают базовые операции. Модуль math добавляет функции над вещественными числами: корни, степени, логарифмы, тригонометрию, округление, комбинаторику и константы pi, e, tau. Большинство функций возвращают float; для комплексных чисел существует cmath, а для поэлементных вычислений над массивами обычно применяют NumPy.
import math
radius = 3.5
area = math.pi * radius**2
print(math.floor(area))
print(math.ceil(area))
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9))
print(math.gcd(84, 30))Из-за двоичного представления float результаты нельзя всегда сравнивать через ==; для приблизительного сравнения предназначен math.isclose(). round() использует округление к ближайшему чётному при точной середине и не решает задачу точной денежной арифметики — для неё нужен Decimal и явно выбранное правило округления.
6. Модуль locale
locale связывает форматирование чисел, денежных сумм и некоторые правила сравнения строк с региональными настройками ОС. setlocale(locale.LC_ALL, "") просит использовать локаль пользователя; конкретные имена вроде ru_RU.UTF-8 зависят от системы и могут отсутствовать.
import locale
try:
locale.setlocale(locale.LC_ALL, "")
except locale.Error:
pass # сохраняем стандартную C-локаль
print(locale.format_string("%.2f", 12345.67, grouping=True))
print(locale.strxfrm("ёж"))Локаль — глобальная настройка процесса, поэтому менять её на каждый HTTP-запрос в многопоточном сервере опасно. В backend-приложениях формат обычно выбирают явно на границе системы, а для полноценной интернационализации используют специализированные библиотеки, например Babel.
7. Модуль decimal
Decimal хранит десятичные числа и позволяет управлять точностью и округлением. Его применяют там, где ошибка двоичного float недопустима: деньги, тарифы, налоговые расчёты. Значение лучше создавать из строки, потому что Decimal(0.1) сначала получает уже неточное двоичное число.
from decimal import Decimal, ROUND_HALF_UP, localcontext
price = Decimal("19.90")
quantity = 3
subtotal = price * quantity
total = (subtotal * Decimal("1.20")).quantize(
Decimal("0.01"),
rounding=ROUND_HALF_UP,
)
with localcontext() as context:
context.prec = 6
ratio = Decimal("2") / Decimal("7")
print(total)
print(ratio)Контекст задаёт точность, ловушки и правило округления. localcontext() временно меняет их только внутри блока. Decimal нельзя бездумно смешивать с float, а способ округления должен быть частью бизнес-правила, а не случайным параметром представления.
8. Модуль dataclasses. Data-классы
@dataclass генерирует по аннотированным полям шаблонные методы: обычно __init__, __repr__ и __eq__. Это удобно для DTO, конфигурации и небольших объектов-значений. Аннотация поля сама по себе не проверяет тип во время выполнения — проверка остаётся за кодом, статическим анализатором или библиотекой валидации.
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Order:
id: int
items: tuple[str, ...] = field(default_factory=tuple)
@property
def size(self) -> int:
return len(self.items)
order = Order(10, ("book", "pen"))
print(order)
print(order.size)frozen=True запрещает обычное присваивание полям после создания и помогает моделировать значение, но не делает глубоко неизменяемыми вложенные изменяемые объекты. slots=True ограничивает набор атрибутов и часто уменьшает память. Для списка или словаря по умолчанию нужен default_factory, иначе разные экземпляры могли бы разделить один изменяемый объект. Дополнительную нормализацию после сгенерированного __init__ выполняют в __post_init__().
9. Модуль timeit
timeit многократно выполняет небольшой фрагмент и измеряет суммарное время подходящим для платформы монотонным таймером. Повторы уменьшают влияние случайного шума. Для функции проще передать вызываемый объект: так не нужны строковый код и отдельный setup.
from timeit import repeat
def build_text() -> str:
return ",".join(str(number) for number in range(100))
measurements = repeat(build_text, repeat=5, number=10_000)
print(f"best: {min(measurements):.4f} s")Сравнивать следует одинаковый объём работы и несколько запусков; обычно берут минимальное время как результат с наименьшим внешним шумом. Микробенчмарк не заменяет профилирование реального приложения: он ничего не говорит о сетевых задержках, памяти или поведении под нагрузкой.
10. Установка пакетов, pip и виртуальная среда
pip устанавливает дистрибутивы из индекса пакетов в выбранное окружение. Виртуальная среда создаёт отдельный каталог с интерпретатором-ссылкой и собственным набором зависимостей, чтобы проекты не влияли друг на друга. Надёжнее вызывать python3 -m pip: тогда видно, какому именно интерпретатору принадлежит pip.
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install "httpx>=0.27,<1"
python3 -m pip freeze > requirements.txt
python3 -m pip install -r requirements.txt
deactivateАктивация лишь меняет PATH; можно вызывать .venv/bin/python напрямую. requirements.txt фиксирует устанавливаемый набор, но для библиотек и современных приложений метаданные проекта обычно хранят в pyproject.toml, а воспроизводимые точные версии — в lock-файле выбранного менеджера. Не устанавливайте зависимости проекта глобально и не добавляйте .venv в Git.
11. Пакетный менеджер uv
uv объединяет управление версиями Python, виртуальными средами, зависимостями, lock-файлом и запуском команд. В проектном режиме зависимости описываются в pyproject.toml, а точное разрешение сохраняется в uv.lock. Команда uv run синхронизирует окружение при необходимости и запускает процесс в контексте проекта.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv init sample-app
cd sample-app
uv add httpx
uv add --dev pytest ruff
uv run python main.py
uv run pytest
uv sync --lockedДля уже существующего скрипта можно создать окружение через uv venv и активировать .venv обычным способом. uv add изменяет декларацию проекта и lock-файл, uv remove удаляет зависимость, uv lock пересчитывает lock-файл, а uv sync --locked отказывается незаметно его менять — это полезно в CI. Установочный скрипт следует применять только после проверки источника; на рабочей машине допустим и пакетный менеджер ОС.
12. Модуль pdb и отладка программы
pdb останавливает выполнение и предоставляет интерактивный доступ к текущему стеку, аргументам и локальным переменным. Скрипт можно запустить под отладчиком командой python3 -m pdb app.py или поставить точку в коде встроенной функцией breakpoint().
def average(numbers: list[float]) -> float:
breakpoint() # удалите после диагностики
return sum(numbers) / len(numbers)
print(average([10.0, 20.0, 30.0]))Основные команды: n выполняет строку без захода в вызов, s заходит внутрь, c продолжает до следующей точки, p expr вычисляет выражение, pp expr печатает его подробно, a показывает аргументы, w — стек, l — соседние строки, b добавляет breakpoint, q завершает сессию. Команды выполняются в состоянии остановленной программы, поэтому выражения с побочными эффектами способны это состояние изменить.
python3 -m pdb app.pyПосле необработанного исключения в интерактивной сессии Python 3.13+ можно вызвать pdb.pm() для посмертной отладки. В production нельзя оставлять интерактивную остановку: вместо неё нужны структурированные логи, traceback и мониторинг ошибок.
13. Модуль sched и выполнение действий по расписанию
sched.scheduler хранит очередь событий и вызывает функции в назначенное время. enter() задаёт задержку, enterabs() — абсолютный момент по часам планировщика, а меньшее значение priority означает больший приоритет при одинаковом времени. run() блокирует текущий поток, пока очередь не опустеет.
import sched
import time
def announce(message: str) -> None:
print(f"{time.strftime('%H:%M:%S')} {message}")
queue = sched.scheduler(time.monotonic, time.sleep)
queue.enter(0.1, priority=2, action=announce, argument=("second",))
queue.enter(0.1, priority=1, action=announce, argument=("first",))
queue.run()Событие возвращает дескриптор, который можно передать в cancel(), пока оно не началось. sched живёт только внутри процесса: после рестарта очередь исчезает, длительная задача задерживает последующие, а повтор нужно планировать заново. Для фоновых заданий веб-сервиса обычно используют устойчивую очередь или внешний планировщик; в асинхронной программе — механизмы её event loop.
Глава 7. Строки
1. Работа со строками
str — неизменяемая последовательность Unicode-символов. Одинарные и двойные кавычки равноправны, тройные позволяют записывать многострочный текст. Экранирование начинается с \, а префикс r создаёт raw-строку, удобную для регулярных выражений и путей, хотя завершающий одиночный обратный слеш в ней всё равно невозможен.
Индекс возвращает строку длины один, отрицательный индекс считает с конца, а срез [start:stop:step] создаёт новую строку и не включает stop. Поскольку строка неизменяема, присвоить символ по индексу нельзя; изменения выражаются созданием нового значения. Частое сложение строк в цикле может создавать много промежуточных объектов, поэтому коллекцию фрагментов обычно соединяют через str.join().
title = "Программирование"
print(title[0]) # П
print(title[-3:]) # ние
print(title[::2]) # каждый второй символ
print("мир" in title.lower())
words = ["Python", "работает", "с", "Unicode"]
sentence = " ".join(words)
print(sentence)Unicode-строка и байты — разные типы. encode() превращает текст в байты выбранной кодировки, а decode() выполняет обратное преобразование. Кодировку необходимо согласовать на границе системы.
text = "Привет"
payload = text.encode("utf-8")
print(payload)
print(payload.decode("utf-8"))2. Основные методы строк
Методы строк не меняют исходное значение. strip, lstrip, rstrip убирают символы по краям; lower, upper, casefold меняют регистр; replace заменяет фрагменты; split разбивает строку; join собирает последовательность. Для поиска доступны find (возвращает -1) и index (выбрасывает ValueError), а для проверок — startswith, endswith, isdigit, isalpha и другие is....
raw = " Alice, ADMIN "
name, role = (part.strip() for part in raw.split(",", maxsplit=1))
print(name.casefold() == "alice")
print(role.lower().replace("admin", "editor"))
print("report.csv".endswith((".csv", ".tsv")))strip(".txt") не удаляет суффикс: он удаляет с обоих краёв любое сочетание перечисленных символов. Для настоящих префикса и суффикса применяют removeprefix() и removesuffix(). При сравнении пользовательского Unicode-текста иногда нужна нормализация из unicodedata, а casefold() надёжнее lower() для регистронезависимого сравнения. Это всё равно не заменяет правила конкретного языка и предметной области.
from unicodedata import normalize
first = normalize("NFC", "cafe\u0301").casefold()
second = normalize("NFC", "CAFÉ").casefold()
print(first == second)
print("archive.tar.gz".removesuffix(".gz"))3. Форматирование
F-строки вычисляют выражения в фигурных скобках во время выполнения и обычно являются самым читаемым способом собрать сообщение. После : задаётся спецификация: ширина, выравнивание, знак, разделитель разрядов, точность и тип. !r использует repr() и удобен при диагностике, а синтаксис name= выводит и выражение, и его значение.
name = "Alice"
amount = 12_345.678
ratio = 0.8734
print(f"{name:<10} | {amount:>12,.2f} | {ratio:.1%}")
print(f"{name=}, {len(name)=}")
print(f"debug value: {name!r}")str.format() полезен для заранее подготовленного шаблона, а старый оператор % встречается в существующем коде и в отложенном форматировании стандартного logging. Нельзя передавать непроверенный пользовательский текст как форматную строку: фигурные скобки интерпретируются, а не выводятся буквально. Для локализованных сообщений шаблон и подстановка обычно разделены.
template = "Пользователь {name} создал {count:d} заказа"
message = template.format(name="Bob", count=3)
print(message)Глава 8. Pattern matching
1. Конструкция match
Структурное сопоставление с образцом появилось в Python 3.10. match вычисляет выражение один раз, проверяет case сверху вниз и выполняет только первую подходящую ветку. Это не просто аналог switch: образец может одновременно проверить форму данных и связать их части с новыми переменными.
Литералы сравниваются со значением, | объединяет альтернативы, а _ означает любой объект без сохранения. Голое имя в образце не сравнивается с одноимённой переменной — оно захватывает значение, поэтому константы указывают квалифицированно, например case HTTPStatus.NOT_FOUND.
def http_message(status: int) -> str:
match status:
case 200 | 201:
return "success"
case 401:
return "authentication required"
case 404:
return "not found"
case _:
return "unexpected status"
print(http_message(201))2. Кортежи в pattern matching
Последовательностный образец проверяет количество и позиции элементов, а имена внутри него захватывают значения. Круглые скобки делают запись похожей на кортеж, но здесь важна структура последовательности. Ветка (name, age) подходит только к двум элементам; литерал на позиции добавляет точное условие; _ игнорирует элемент.
def describe_user(user: tuple[object, ...]) -> str:
match user:
case ("admin", name):
return f"administrator {name}"
case (name, age) if isinstance(age, int):
return f"{name}, {age}"
case (name, *details):
return f"{name}: {details}"
case _:
return "invalid user"
print(describe_user(("admin", "Alice")))
print(describe_user(("Bob", 31, "Berlin")))*rest собирает оставшиеся элементы в список, а *_ принимает их без сохранения. Альтернативы через | должны связывать одинаковый набор имён, иначе дальнейший код не мог бы знать, какие переменные существуют.
3. Массивы в pattern matching
Квадратные скобки в образце описывают последовательность, а не требуют именно list. Такой образец может совпасть и с кортежем, и с другими объектами протокола последовательности; строки и байтовые последовательности намеренно не разбираются на отдельные элементы. *middle позволяет сопоставлять переменную длину.
def route(parts: list[str]) -> str:
match parts:
case ["users"]:
return "all users"
case ["users", user_id]:
return f"user {user_id}"
case ["files", *path] if path:
return "/".join(path)
case _:
return "unknown route"
print(route(["users", "42"]))
print(route(["files", "images", "logo.png"]))Образец не изменяет исходную последовательность. Он только проверяет её и, если ветка выбрана, создаёт привязки имён для тела case.
4. Словари в pattern matching
Образец отображения проверяет наличие перечисленных ключей и сопоставляет их значения. В отличие от последовательности, дополнительные ключи не мешают совпадению. **rest собирает остальные пары в новый словарь, а отсутствие обязательного ключа делает ветку неподходящей.
def handle_event(event: dict[str, object]) -> str:
match event:
case {"type": "user.created", "payload": {"id": int(user_id)}}:
return f"created user {user_id}"
case {"type": event_type, **metadata}:
return f"other event {event_type}: {metadata}"
case _:
return "invalid event"
print(handle_event({"type": "user.created", "payload": {"id": 7}}))Захват int(user_id) здесь одновременно проверяет тип значения и сохраняет само целое число. Сопоставление удобно для уже разобранных структур, но не заменяет полноценную проверку внешнего JSON: сообщения об ошибках, ограничения значений и преобразования лучше поручить отдельному слою валидации.
5. Классы в pattern matching
Классовый образец сначала проверяет тип через isinstance, затем сопоставляет выбранные атрибуты. Именованные поля указываются явно. Позиционная форма зависит от __match_args__; @dataclass обычно формирует его по полям, если это поведение не отключено.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
def location(point: object) -> str:
match point:
case Point(0, 0):
return "origin"
case Point(x=0, y=y):
return f"vertical axis at {y}"
case Point(x, y):
return f"point {x}, {y}"
case _:
return "not a point"
print(location(Point(0, 5)))Именованные образцы устойчивее к изменению порядка полей. Получение свойства во время сопоставления способно вызвать его код, поэтому свойства с побочными эффектами делают поведение match неочевидным.
6. guards или ограничения шаблонов
Guard — дополнительное условие if после образца. Оно вычисляется только после структурного совпадения, поэтому может использовать захваченные значения. Если условие ложно, Python продолжает со следующего case.
def price_rule(item: tuple[str, int]) -> str:
match item:
case (name, count) if count <= 0:
return f"invalid count for {name}"
case (name, count) if count >= 10:
return f"bulk order: {name}"
case (name, _):
return f"regular order: {name}"
print(price_rule(("notebook", 12)))Guard подходит для ограничений, которые неудобно выразить формой: диапазона, связи между полями, вызова чистого предиката. Условия с изменением состояния затрудняют чтение, поскольку они могут выполниться для структурно подходящей, но в итоге отвергнутой ветки.
7. Установка псевдонимов и паттерн AS
as сохраняет целиком значение, которое совпало с подобразцом. Это полезно, когда отдельные части нужны для проверки, а весь объект — для последующей передачи. Скобки позволяют назначить псевдоним всей альтернативе, а не только её последнему элементу.
def normalize_command(command: object) -> tuple[str, object]:
match command:
case (("create" | "update") as action, {"id": int()} as payload):
return action, payload
case (["start", *options] | ["run", *options]) as original:
return "execute", {"original": original, "options": options}
case _:
return "unknown", command
print(normalize_command(("update", {"id": 5})))В первой ветке action получает совпавший литерал, а payload — весь словарь после проверки обязательного id. Псевдоним не создаёт копию: он ссылается на исходный объект.
Глава 9. Работа с файлами
1. Открытие и закрытие файлов
open(path, mode, ...) возвращает файловый объект. Основные режимы: r — чтение, w — запись с обнулением существующего файла, a — добавление в конец, x — создание только нового файла; + разрешает и чтение, и запись, b выбирает бинарный режим. В текстовом режиме Python декодирует байты и поэтому важны encoding, errors и обработка переводов строк.
Файл занимает ресурс ОС и должен быть закрыт даже при исключении. Конструкция with работает через протокол контекстного менеджера: вызывает __enter__() перед блоком и гарантированно вызывает __exit__(exc_type, exc_value, traceback) при выходе. Файловый объект возвращает себя из __enter__, а в __exit__ закрывается. Если __exit__ вернёт истинное значение, исключение будет подавлено; файловый менеджер его не подавляет.
from pathlib import Path
path = Path("notes.txt")
with path.open("w", encoding="utf-8") as file:
file.write("первая строка\n")
print(file.closed) # TrueРучной эквивалент требует try/finally, поэтому его легко написать неправильно. Для путей предпочтителен pathlib.Path: он не привязывает код к строковым разделителям конкретной ОС. Относительный путь считается от текущего рабочего каталога процесса, который не обязательно совпадает с каталогом скрипта.
2. Текстовые файлы
write() записывает одну строку и возвращает число записанных символов; перевод строки автоматически не добавляется. writelines() принимает последовательность строк и тоже не вставляет \n. Для чтения есть read(), readline() и readlines(), но большой файл лучше перебирать напрямую: файловый объект является итератором строк и не загружает всё содержимое в память.
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "messages.txt"
with path.open("w", encoding="utf-8") as file:
file.write("alpha\n")
file.writelines(["beta\n", "gamma\n"])
with path.open(encoding="utf-8") as file:
for number, line in enumerate(file, start=1):
print(number, line.rstrip("\n"))Открытый файл хранит текущую позицию. tell() сообщает её, seek() перемещает; после записи в режиме w+ перед чтением обычно нужен seek(0). В текстовом режиме произвольное смещение сложнее из-за кодировки, поэтому надёжны позиции, ранее полученные через tell(), и переход в начало seek(0).
3. Файлы CSV
CSV — семейство табличных текстовых форматов: разделитель, кавычки и правила экранирования могут различаться. Модуль csv корректно обрабатывает поля с запятыми, кавычками и переводами строк, в отличие от ручного split(","). При открытии файла для csv указывают newline="", чтобы модуль сам управлял переводами строк.
import csv
from io import StringIO
buffer = StringIO(newline="")
writer = csv.DictWriter(buffer, fieldnames=["name", "age"])
writer.writeheader()
writer.writerows([
{"name": "Alice", "age": 30},
{"name": "Bob, Jr.", "age": 24},
])
buffer.seek(0)
for row in csv.DictReader(buffer):
print(row["name"], int(row["age"]))reader возвращает строки — преобразование чисел, дат и пустых значений выполняет приложение. DictReader использует первую строку как имена полей, DictWriter записывает словари в заданном порядке. Для нестандартного формата передают delimiter, quotechar или зарегистрированный dialect. Данные из недоверенного CSV могут быть опасны при открытии в табличном редакторе из-за формул, начинающихся с =, +, - или @; это учитывают при экспорте.
4. Бинарные файлы
Режимы rb, wb, ab работают с bytes, а не со str; кодирование текста здесь выполняется явно. Бинарный режим нужен для изображений, архивов, сетевых пакетов и собственных форматов. Большие файлы копируют блоками, чтобы не удерживать всё содержимое в памяти.
from io import BytesIO
stream = BytesIO()
stream.write((513).to_bytes(2, byteorder="big"))
stream.write("OK".encode("utf-8"))
stream.seek(0)
number = int.from_bytes(stream.read(2), byteorder="big")
text = stream.read().decode("utf-8")
print(number, text)Для сериализации произвольных Python-объектов стандартная библиотека предлагает pickle: он сохраняет структуру объектов, но привязан к Python и может выполнить код при загрузке. Никогда не вызывайте pickle.load() для недоверенных данных. Для обмена между сервисами чаще выбирают JSON, MessagePack или схему вроде Protocol Buffers; для простых числовых структур подходит также struct.
import pickle
payload = pickle.dumps({"name": "Alice", "roles": ["admin"]})
restored = pickle.loads(payload) # допустимо только для доверенного payload
print(restored)5. Модуль shelve
shelve предоставляет похожее на словарь постоянное хранилище: ключи — строки, значения сериализуются через pickle, а база обычно строится поверх доступной реализации dbm. Открытая shelf поддерживает контекстный менеджер; изменения сохраняются на диск при синхронизации или закрытии.
import shelve
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
database = f"{directory}/settings"
with shelve.open(database) as storage:
storage["theme"] = "dark"
storage["limits"] = {"requests": 100}
with shelve.open(database, flag="r") as storage:
print(storage["theme"])
print(storage.get("limits"))Из-за pickle shelf нельзя открывать из недоверенного источника. Это не серверная база данных: формат и набор создаваемых файлов зависят от платформы, конкурентная запись не поддерживается как полноценная транзакционная модель, сложных запросов нет. Для изменяемого объекта, извлечённого из shelf, обычно нужно присвоить значение обратно либо открыть хранилище с writeback=True; второй вариант кэширует объекты и может расходовать много памяти.
6. Модуль OS и работа с файловой системой
os предоставляет низкоуровневые функции ОС: текущий каталог, переменные окружения, обход каталогов, создание и удаление файлов, права доступа и процессы. os.path выполняет операции со строковыми путями. В новом коде операции с путями часто проще выражать через pathlib, оставляя os для возможностей уровня системы.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
root = Path(directory)
reports = root / "reports"
reports.mkdir(parents=True)
report = reports / "daily.txt"
report.write_text("done\n", encoding="utf-8")
print(report.exists(), report.stat().st_size)
print(os.fspath(report))
report.rename(reports / "latest.txt")Перед удалением или рекурсивной обработкой путь нужно разрешить и проверить: ошибка в корневом каталоге способна затронуть чужие данные. Переменные окружения читают через os.environ или os.getenv; они всегда строки и отсутствие значения надо обрабатывать явно. Секреты не следует выводить в логи.
7. Программа подсчета слов
Подсчёт слов объединяет чтение файла, нормализацию строк и словарь частот. Простое split() считает пунктуацию частью слова; регулярное выражение позволяет явно определить допустимые символы. casefold() лучше подходит для регистронезависимого Unicode-сравнения.
import re
from collections import Counter
def count_words(text: str) -> Counter[str]:
words = re.findall(r"[^\W\d_]+(?:[-'][^\W\d_]+)*", text.casefold())
return Counter(words)
sample = "Python прост. Python — выразителен; python практичен."
for word, count in count_words(sample).most_common():
print(f"{word}: {count}")Для реального большого файла обрабатывают строки по одной и обновляют общий Counter, а не читают весь текст. Определение «слова» зависит от задачи: числа, дефисы, апострофы, разные системы письма и Unicode-комбинации требуют осознанной токенизации. Для естественного языка регулярного выражения может быть недостаточно.
8. Запись и чтение архивных zip-файлов
zipfile.ZipFile читает и создаёт ZIP-архивы. Режим w создаёт архив заново, a добавляет записи; параметр compression=ZIP_DEFLATED включает обычное сжатие. write() добавляет существующий файл, а writestr() — данные из памяти. Объект архива является контекстным менеджером.
from io import BytesIO
from zipfile import ZIP_DEFLATED, ZipFile
archive = BytesIO()
with ZipFile(archive, "w", compression=ZIP_DEFLATED) as zip_file:
zip_file.writestr("docs/readme.txt", "Hello from archive\n")
zip_file.writestr("data/value.txt", "42")
archive.seek(0)
with ZipFile(archive) as zip_file:
print(zip_file.namelist())
print(zip_file.read("data/value.txt").decode("utf-8"))Перед extract() или extractall() недоверенный архив проверяют: имена могут пытаться выйти за целевой каталог, архив может разрастись до огромного объёма, а символические ссылки и число записей требуют ограничений. Если содержимое нужно только прочитать, безопаснее открыть конкретную запись через ZipFile.open() без распаковки всего архива.
Глава 10. Работа с датами и временем
1. Модуль datetime
datetime содержит несколько разных типов: date — календарная дата, time — время суток, datetime — их сочетание, timedelta — длительность, tzinfo — интерфейс часового пояса. Объекты неизменяемы. datetime.now() без зоны создаёт naive-значение, которое не сообщает, к какому часовому поясу относится; для системного момента обычно используют aware-время.
from datetime import date, datetime, time, timezone
today = date.today()
opening = time(hour=9, minute=30)
now_utc = datetime.now(timezone.utc)
meeting = datetime.combine(today, opening).astimezone()
print(today.isoformat())
print(opening.isoformat(timespec="minutes"))
print(now_utc.isoformat())
print(meeting.isoformat())fromisoformat() разбирает ISO-представление, strptime(text, format) — строку известного формата, strftime(format) форматирует. Формат %Y-%m-%d означает четырёхзначный год, месяц и день. Unix timestamp — число секунд от эпохи; datetime.fromtimestamp(value, tz=timezone.utc) сразу приписывает явную зону.
from datetime import datetime, timezone
created_at = datetime.fromisoformat("2026-08-23T10:15:00+03:00")
as_utc = created_at.astimezone(timezone.utc)
print(as_utc.strftime("%Y-%m-%d %H:%M:%S %Z"))
print(as_utc.timestamp())2. Операции с датами
timedelta представляет длительность в днях, секундах и микросекундах и поддерживает сложение, вычитание и сравнение. Разность двух datetime даёт timedelta. replace() создаёт новое значение с заменёнными полями — исходный объект не меняется.
from datetime import datetime, timedelta, timezone
started_at = datetime(2026, 8, 23, 9, 0, tzinfo=timezone.utc)
deadline = started_at + timedelta(days=2, hours=6)
remaining = deadline - started_at
print(deadline.isoformat())
print(remaining.total_seconds())
print(deadline > started_at)Для реальных региональных зон стандартная библиотека предоставляет zoneinfo.ZoneInfo, использующую базу IANA. Она учитывает исторические смещения и переходы на летнее время, в отличие от фиксированного timezone(timedelta(...)). На некоторых системах данные зон требуют зависимости tzdata.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
instant = datetime(2026, 8, 23, 12, 0, tzinfo=timezone.utc)
moscow = instant.astimezone(ZoneInfo("Europe/Moscow"))
new_york = instant.astimezone(ZoneInfo("America/New_York"))
print(moscow.isoformat())
print(new_york.isoformat())В backend обычно сохраняют момент времени в UTC вместе с зоной или в типе БД, который однозначно представляет instant, а локальную зону применяют при выводе. Календарное «через день в 09:00» и длительность «через 24 часа» — разные требования около смены смещения; бизнес-правило должно явно выбрать нужную семантику. Нельзя напрямую сравнивать naive- и aware-datetime.
Руководство по веб-фреймворку FastAPI
FastAPI — ASGI-фреймворк для HTTP API. Он использует аннотации типов Python как источник правил валидации, сериализации и OpenAPI-схемы, а интерактивная документация обычно доступна по /docs и /redoc.
Глава 1. Основы FastAPI
1. Что такое FastAPI
FastAPI строится поверх Starlette и Pydantic. Starlette отвечает за ASGI, маршрутизацию и HTTP-примитивы, а Pydantic — за разбор и проверку данных. Для разработки достаточно приложения и ASGI-сервера Uvicorn.
mkdir fastapi-demo
cd fastapi-demo
uv init
uv add "fastapi[standard]"# main.py
from fastapi import FastAPI
app = FastAPI(title="Demo API")
@app.get("/")
async def root() -> dict[str, str]:
return {"message": "Hello, FastAPI!"}uv run fastapi dev main.pyРежим dev следит за изменениями и перезапускает сервер, поэтому предназначен для локальной разработки. После запуска приложение доступно по http://127.0.0.1:8000, а Swagger UI — по http://127.0.0.1:8000/docs.
2. Класс FastAPI и обработка запроса
Объект FastAPI — ASGI-приложение и реестр маршрутов. Декораторы get, post, put, patch и delete связывают сочетание HTTP-метода и пути с функцией-обработчиком. Обычный def FastAPI запускает в пуле потоков, а async def — в event loop; асинхронный обработчик не должен выполнять долгий блокирующий код.
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/jobs")
async def create_job() -> dict[str, str]:
return {"state": "queued"}Словари, списки и Pydantic-модели FastAPI преобразует в JSON. Уникальность определяется парой «метод + путь»: GET /jobs и POST /jobs могут иметь разные обработчики.
3. Отправка ответа
Обычно обработчик возвращает данные, а FastAPI сам создаёт JSONResponse. Явные классы ответа нужны, когда требуется другой MIME-тип, заголовки или полный контроль над телом. Параметр response_class также документирует тип ответа в OpenAPI.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
app = FastAPI()
@app.get("/json")
def json_data() -> dict[str, int]:
return {"answer": 42}
@app.get("/text", response_class=PlainTextResponse)
def text() -> str:
return "plain text"
@app.get("/page", response_class=HTMLResponse)
def page() -> str:
return "<h1>Hello</h1>"
@app.get("/empty", status_code=204)
def empty() -> Response:
return Response(status_code=204)Не вставляйте в HTML непроверенные пользовательские строки: HTMLResponse не экранирует их автоматически.
4. Отправка файлов с сервера
FileResponse передаёт файл потоково и выставляет полезные заголовки, включая размер и время изменения. Путь должен формироваться сервером или строго проверяться: непосредственная подстановка пользовательского имени создаёт уязвимость path traversal.
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
app = FastAPI()
REPORTS = Path("reports").resolve()
@app.get("/reports/{name}", response_class=FileResponse)
def download_report(name: str) -> FileResponse:
path = (REPORTS / name).resolve()
if REPORTS not in path.parents or not path.is_file():
raise HTTPException(status_code=404, detail="Файл не найден")
return FileResponse(path, filename=path.name)filename задаёт имя для скачивания. Для данных, которые создаются на лету, чаще подходят Response или StreamingResponse.
5. Параметры пути
Фрагмент {name} в маршруте становится параметром обработчика. Аннотация определяет преобразование и проверку: строка "12" станет int, а некорректное значение приведёт к ответу 422. Ограничения задаются через Path.
from typing import Annotated
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(
user_id: Annotated[int, Path(ge=1, description="Положительный ID")],
) -> dict[str, int]:
return {"user_id": user_id}Статические пути объявляйте до динамических: /users/me должен регистрироваться раньше /users/{user_id}, иначе слово me будет воспринято как значение параметра.
6. Параметры строки запроса
Параметр функции, которого нет в шаблоне пути, по умолчанию читается из query string. Значение без None и без default обязательно; str | None = None — необязательно. Query добавляет ограничения и метаданные.
from typing import Annotated
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def list_items(
q: Annotated[str | None, Query(min_length=2, max_length=50)] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
tags: Annotated[list[str] | None, Query()] = None,
) -> dict[str, object]:
return {"q": q, "limit": limit, "tags": tags or []}Запрос /items?q=py&limit=10&tags=web&tags=api создаст список tags. Необязательность задаётся типом и значением по умолчанию, а не только Query.
7. Отправка статусных кодов
Успешный статус удобно объявлять в декораторе. Ошибки прерывают обработку через HTTPException; поле detail сериализуется в JSON. Символические константы status понятнее необъяснённых чисел.
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
items = {1: "keyboard"}
@app.post("/items", status_code=status.HTTP_201_CREATED)
def create_item(name: str) -> dict[str, object]:
item_id = max(items, default=0) + 1
items[item_id] = name
return {"id": item_id, "name": name}
@app.get("/items/{item_id}")
def get_item(item_id: int) -> dict[str, object]:
if item_id not in items:
raise HTTPException(status_code=404, detail="Объект не найден")
return {"id": item_id, "name": items[item_id]}Код должен соответствовать фактическому результату: создание обычно возвращает 201, удаление без тела — 204, ошибка входных данных FastAPI — 422.
8. Переадресация
RedirectResponse отправляет заголовок Location. Статус 307 сохраняет исходный HTTP-метод; после успешного POST формы обычно нужен 303 See Other, который заставляет браузер выполнить GET.
from fastapi import FastAPI, status
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/old-docs")
def old_docs() -> RedirectResponse:
return RedirectResponse("/docs", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@app.post("/signup")
def signup() -> RedirectResponse:
# Здесь могла бы быть запись пользователя в БД.
return RedirectResponse("/welcome", status_code=status.HTTP_303_SEE_OTHER)Для URL, поступивших от клиента, используйте белый список допустимых адресов, иначе возникает open redirect.
9. Статические файлы
StaticFiles обслуживает CSS, JavaScript и изображения из каталога. Он монтируется как отдельное ASGI-приложение; name можно использовать при построении URL в шаблонах.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static", check_dir=True), name="static")project/
├── main.py
└── static/
└── app.cssПосле запуска файл static/app.css доступен как /static/app.css. В production крупные статические каталоги обычно отдаёт CDN или reverse proxy, а не процесс API.
10. Получение данных запроса
JSON-тело можно описать отдельными значениями через Body. Параметр embed=True требует объект с именованным полем, а не «голое» значение. Для реального API предпочтительнее именованная Pydantic-модель: она переиспользуется в документации и типах.
from typing import Annotated
from fastapi import Body, FastAPI
app = FastAPI()
@app.post("/greet")
def greet(
name: Annotated[str, Body(embed=True, min_length=1)],
) -> dict[str, str]:
return {"message": f"Привет, {name}!"}curl -X POST http://127.0.0.1:8000/greet \
-H 'Content-Type: application/json' \
-d '{"name":"Анна"}'Низкоуровневый Request позволяет читать необработанное тело, но после чтения его нужно самостоятельно декодировать и валидировать.
11. Получение данных запроса в виде объекта класса
Класс-наследник BaseModel описывает форму JSON, преобразует типы и выдаёт структурированные ошибки. Field добавляет ограничения. Для изменяемых значений по умолчанию используйте default_factory.
from typing import Annotated
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class Address(BaseModel):
city: str
street: str
class PersonIn(BaseModel):
name: Annotated[str, Field(min_length=2, max_length=50)]
age: Annotated[int | None, Field(ge=0, le=130)] = None
languages: list[str] = Field(default_factory=list)
address: Address | None = None
@app.post("/people")
def create_person(person: PersonIn) -> PersonIn:
return personМодель запроса не обязана совпадать с моделью ответа: раздельные схемы помогают не вернуть пароль, внутренние флаги или другие закрытые поля.
12. Создание простейшего API
Минимальный REST-подобный API связывает методы HTTP с CRUD-операциями. В примере данные находятся в памяти и исчезают после перезапуска; это демонстрация маршрутов, а не архитектура production-приложения.
from fastapi import FastAPI, HTTPException, Response, status
from pydantic import BaseModel, Field
app = FastAPI()
class UserIn(BaseModel):
name: str = Field(min_length=1)
class User(UserIn):
id: int
users: dict[int, User] = {}
@app.get("/users", response_model=list[User])
def list_users() -> list[User]:
return list(users.values())
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int) -> User:
if user_id not in users:
raise HTTPException(404, "Пользователь не найден")
return users[user_id]
@app.post("/users", response_model=User, status_code=201)
def create_user(data: UserIn) -> User:
user_id = max(users, default=0) + 1
user = User(id=user_id, **data.model_dump())
users[user_id] = user
return user
@app.put("/users/{user_id}", response_model=User)
def replace_user(user_id: int, data: UserIn) -> User:
if user_id not in users:
raise HTTPException(404, "Пользователь не найден")
users[user_id] = User(id=user_id, **data.model_dump())
return users[user_id]
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(user_id: int) -> Response:
if users.pop(user_id, None) is None:
raise HTTPException(404, "Пользователь не найден")
return Response(status_code=204)В нескольких worker-процессах такой словарь не является общей базой. Следующий раздел заменяет его на SQLAlchemy.
13. Отправка и получение заголовков
Header извлекает заголовок и по умолчанию преобразует подчёркивания в дефисы: параметр user_agent читает User-Agent. Заголовки ответа устанавливаются через объект Response или конструктор конкретного ответа.
from typing import Annotated
from fastapi import FastAPI, Header, Response
app = FastAPI()
@app.get("/request-info")
def request_info(
response: Response,
user_agent: Annotated[str | None, Header()] = None,
x_request_id: Annotated[str | None, Header()] = None,
) -> dict[str, str | None]:
response.headers["X-API-Version"] = "1"
return {"user_agent": user_agent, "request_id": x_request_id}Не доверяйте заголовкам идентификации, пришедшим напрямую из интернета. Например, X-Forwarded-For имеет смысл только от доверенного proxy.
14. Куки
Cookie читает cookie, а Response.set_cookie отправляет Set-Cookie. Cookie — всего лишь данные клиента, поэтому идентификатор сессии должен быть случайным, ограниченным по времени и проверяться на сервере.
from typing import Annotated
from fastapi import Cookie, FastAPI, Response
app = FastAPI()
@app.post("/session")
def create_session(response: Response) -> dict[str, str]:
response.set_cookie(
key="session_id",
value="demo-token",
httponly=True,
secure=True,
samesite="lax",
max_age=3600,
)
return {"status": "created"}
@app.get("/session")
def read_session(
session_id: Annotated[str | None, Cookie()] = None,
) -> dict[str, bool]:
return {"authenticated": session_id is not None}
@app.delete("/session")
def delete_session(response: Response) -> dict[str, str]:
response.delete_cookie("session_id")
return {"status": "deleted"}secure=True требует HTTPS. Строка demo-token приведена только для формы примера; настоящий токен создают криптографически стойко.
15. Отправка форм
Поля HTML-форм читаются через Form, загруженные файлы — через UploadFile. Для multipart/form-data нужен пакет python-multipart (он входит в стандартный набор зависимостей FastAPI, но при минимальной установке добавляется отдельно). UploadFile не загружает весь большой файл в память сразу.
from typing import Annotated
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
app = FastAPI()
@app.post("/profiles")
async def create_profile(
name: Annotated[str, Form(min_length=1)],
avatar: Annotated[UploadFile, File()],
) -> dict[str, str]:
if avatar.content_type not in {"image/png", "image/jpeg"}:
raise HTTPException(415, "Поддерживаются PNG и JPEG")
first_chunk = await avatar.read(64 * 1024)
await avatar.close()
return {"name": name, "filename": avatar.filename or "", "read": str(len(first_chunk))}curl -X POST http://127.0.0.1:8000/profiles \
-F 'name=Анна' \
-F 'avatar=@avatar.png'В production проверяют фактический формат, размер и безопасное имя; Content-Type и исходное имя задаёт клиент.
Глава 2. Работа с базой данных через SQLAlchemy
1. Подключение к базе данных и создание таблиц
SQLAlchemy 2 разделяет Engine — фабрику подключений — и Session — единицу работы с ORM-объектами. Сессия создаётся на запрос и обязательно закрывается. Здесь используется синхронный SQLite и обычные def-обработчики; для async-драйвера нужны create_async_engine и AsyncSession во всём пути вызовов.
uv add sqlalchemy# db.py
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
engine = create_engine(
"sqlite:///app.db",
connect_args={"check_same_thread": False},
)
SessionFactory = sessionmaker(bind=engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
def get_session() -> Generator[Session, None, None]:
with SessionFactory() as session:
yield session# models.py
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from db import Base, engine
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
if __name__ == "__main__":
Base.metadata.create_all(engine)uv run python models.pycreate_all удобен в учебном проекте и здесь запускается отдельной командой, а не как побочный эффект импорта. Изменение существующей схемы в приложении выполняют миграциями, например Alembic.
2. Взаимодействие с базой данных
В SQLAlchemy 2 запрос строится через select, а результат исполняется сессией. flush отправляет изменения в текущую транзакцию, commit подтверждает её, refresh перечитывает строку. При исключении незавершённая транзакция должна быть откачена; контекст Session.begin() делает это автоматически.
# Фрагмент: использует User и SessionFactory из предыдущего примера.
from sqlalchemy import select
from db import SessionFactory
from models import User
with SessionFactory.begin() as session:
session.add_all([User(name="Ada"), User(name="Grace")])
with SessionFactory() as session:
users = session.scalars(
select(User).where(User.name.startswith("A")).order_by(User.id)
).all()
for user in users:
print(user.id, user.name)
with SessionFactory.begin() as session:
user = session.get(User, 1)
if user is not None:
user.name = "Ada Lovelace"
with SessionFactory.begin() as session:
user = session.get(User, 2)
if user is not None:
session.delete(user)Параметры SQLAlchemy связывает отдельно от SQL-текста. Не собирайте SQL конкатенацией строк.
3. Базовые операции с базой данных в веб-приложении
FastAPI dependency создаёт отдельную сессию для каждого запроса. Входная схема отделена от выходной; from_attributes=True разрешает Pydantic читать ORM-атрибуты. Коммит остаётся в обработчике, поэтому граница транзакции видна явно.
# Фрагмент: использует get_session и ORM-модель User из предыдущих примеров.
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, Response, status
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from db import get_session
from models import User
app = FastAPI()
DbSession = Annotated[Session, Depends(get_session)]
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
class UserRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
@app.get("/users", response_model=list[UserRead])
def list_users(session: DbSession) -> list[User]:
return list(session.scalars(select(User).order_by(User.id)))
@app.post("/users", response_model=UserRead, status_code=201)
def create_user(data: UserCreate, session: DbSession) -> User:
user = User(name=data.name)
session.add(user)
session.commit()
session.refresh(user)
return user
@app.put("/users/{user_id}", response_model=UserRead)
def update_user(user_id: int, data: UserCreate, session: DbSession) -> User:
user = session.get(User, user_id)
if user is None:
raise HTTPException(404, "Пользователь не найден")
user.name = data.name
session.commit()
session.refresh(user)
return user
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(user_id: int, session: DbSession) -> Response:
user = session.get(User, user_id)
if user is None:
raise HTTPException(404, "Пользователь не найден")
session.delete(user)
session.commit()
return Response(status_code=204)В более крупном сервисе обработчики передают работу use case или service-слою, но жизненный цикл сессии и границы транзакции должны оставаться однозначными.
Руководство по веб-фреймворку Django
Материал основан на руководстве Metanit по Django и собран вокруг одного небольшого проекта config с приложением hello. Примеры используют актуальный стиль Django: отдельный urls.py приложения, именованные маршруты, шаблоны и статические файлы в пространстве имён приложения.
Глава 1. Введение в Django
1. Что такое Django
Django — полнофункциональный Python-фреймворк для серверных веб-приложений. Он уже содержит маршрутизацию, шаблонизатор, ORM, формы, аутентификацию, административный интерфейс, защиту от распространённых атак и инструменты тестирования. Поэтому разработчик собирает приложение из готовых согласованных компонентов, а не подбирает отдельную библиотеку для каждой базовой задачи.
Django описывает свою организацию термином MVT:
- Model задаёт данные и операции с ними;
- View принимает
HttpRequest, выполняет прикладную работу и возвращаетHttpResponse; - Template превращает переданный контекст в HTML;
- URL dispatcher сопоставляет адрес с view.
Название немного отличается от MVC: Django-view по роли ближе к контроллеру, а Template — к представлению. Типичный путь запроса выглядит так: URL → view → при необходимости model → template → HTTP-ответ.
Минимальная view может вернуть ответ без модели и шаблона.
hello/views.py
from django.http import HttpRequest, HttpResponse
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("Hello, Django!")hello/urls.py
from django.urls import path
from . import views
app_name = "hello"
urlpatterns = [
path("", views.index, name="index"),
]2. Установка и настройка Django
Зависимости проекта лучше изолировать в виртуальном окружении. Оно создаёт собственный интерпретатор и каталог пакетов, поэтому разные проекты могут использовать разные версии Django. Команды ниже подходят для Linux и macOS.
mkdir django-guide
cd django-guide
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install DjangoПока окружение активно, команды python и pip относятся к нему. Вызов через python -m pip дополнительно гарантирует, что используется pip именно выбранного интерпретатора.
Проверить установку можно без входа в интерактивную консоль:
python -m django --versionВерсию также можно получить из Python:
import django
print(django.get_version())Чтобы завершить работу с окружением:
deactivateДля воспроизводимого проекта зависимости фиксируют в отдельном файле или менеджере зависимостей. Простейший вариант с pip:
python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt3. Создание первого проекта
Проект Django хранит общую конфигурацию сайта. Команда startproject создаёт пакет настроек; точка в конце помещает manage.py в текущую директорию и не создаёт лишний внешний каталог.
django-admin startproject config .
python manage.py runserverРазработческий сервер обычно доступен по адресу http://127.0.0.1:8000/. Он перезапускается при изменении Python-кода и предназначен только для локальной разработки.
Получится такая основа:
django-guide/
├── manage.py
└── config/
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.pymanage.pyзапускает команды в контексте проекта;settings.pyсодержит приложения, middleware, базу данных, шаблоны и другие настройки;urls.py— корневая таблица маршрутов;wsgi.py— точка входа для WSGI-серверов;asgi.py— точка входа для ASGI-серверов и асинхронных протоколов;__init__.pyделает каталог Python-пакетом.
Полезная проверка конфигурации:
python manage.py checkПорт и адрес можно изменить:
python manage.py runserver 127.0.0.1:80804. Создание первого приложения
Проект состоит из приложений — пакетов с отдельной функциональностью. Например, каталог товаров, блог и учётные записи можно оформить как разные приложения. Приложение допускает повторное использование, а проект объединяет приложения и задаёт общие настройки.
Создадим приложение hello:
python manage.py startapp hellohello/
├── migrations/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py
└── views.pymodels.py хранит модели, views.py — обработчики запросов, admin.py — регистрацию моделей в админке, apps.py — конфигурацию приложения, tests.py — тесты, а migrations/ — историю изменений схемы БД.
Приложение регистрируют в настройках через его конфигурацию.
config/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"hello.apps.HelloConfig",
]Добавим ответ и локальные маршруты приложения.
hello/views.py
from django.http import HttpRequest, HttpResponse
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("Hello, Django!")hello/urls.py
from django.urls import path
from . import views
app_name = "hello"
urlpatterns = [
path("", views.index, name="index"),
]config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("hello.urls")),
]После python manage.py runserver корневой URL вернёт строку Hello, Django!.
Глава 2. Представления и маршрутизация
1. Обработка запроса
View — вызываемый объект, который получает HttpRequest и обязан вернуть HttpResponse либо совместимый ответ. Для простых страниц удобно использовать функции-представления. Их обычно размещают в views.py, а затем связывают с маршрутами.
hello/views.py
from django.http import HttpRequest, HttpResponse
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("<h1>Главная</h1>")
def about(request: HttpRequest) -> HttpResponse:
return HttpResponse("<h1>О сайте</h1>")
def contacts(request: HttpRequest) -> HttpResponse:
return HttpResponse("<h1>Контакты</h1>")hello/urls.py
from django.urls import path
from . import views
app_name = "hello"
urlpatterns = [
path("", views.index, name="index"),
path("about/", views.about, name="about"),
path("contacts/", views.contacts, name="contacts"),
]Django проверяет urlpatterns сверху вниз и вызывает view первого совпавшего маршрута. Параметр request содержит метод, путь, заголовки, GET/POST-данные, cookies и файлы. Даже если view пока не использует запрос, параметр всё равно нужен.
Строковый HTML через HttpResponse подходит для демонстрации. В обычных страницах разметку выносят в шаблоны, чтобы не смешивать Python и представление.
2. Определение маршрутов и функции path и re_path
path() создаёт маршрут из читаемого шаблона:
path(route, view, kwargs=None, name=None)route— путь без начального/;view— обработчик;kwargs— постоянные дополнительные аргументы view;name— имя для обратного построения URL.
Большинству приложений достаточно path(). re_path() нужен, когда встроенных конвертеров недостаточно и адрес действительно удобнее выразить регулярным выражением.
hello/urls.py
from django.urls import path, re_path
from . import views
app_name = "hello"
urlpatterns = [
path("", views.index, name="index"),
path("about/", views.about, name="about"),
re_path(r"^archive/(?P<year>\d{4})/$", views.archive, name="archive"),
]hello/views.py
from django.http import HttpRequest, HttpResponse
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("Главная")
def about(request: HttpRequest) -> HttpResponse:
return HttpResponse("О сайте")
def archive(request: HttpRequest, year: str) -> HttpResponse:
return HttpResponse(f"Архив за {year} год")Порядок важен: сначала размещают конкретные шаблоны, затем более общие. Регулярное выражение желательно ограничивать ^ и $, иначе оно может принять лишний хвост пути. Имена маршрутов позволяют не зашивать URL в код:
from django.urls import reverse
url = reverse("hello:archive", kwargs={"year": "2026"})3. Получение данных запроса. HttpRequest
HttpRequest описывает входящий HTTP-запрос. Часто используются:
request.method—GET,POSTи другие методы;request.pathиrequest.get_full_path()— путь без и со строкой запроса;request.GETиrequest.POST— объектыQueryDict;request.body— необработанное тело в байтах;request.FILES— загруженные файлы;request.COOKIES— cookies клиента;request.headers— удобный регистронезависимый доступ к заголовкам;request.META— низкоуровневые WSGI/CGI-переменные.
Не стоит обращаться к необязательному заголовку через квадратные скобки: отсутствие ключа вызовет KeyError. Метод .get() позволяет задать резервное значение.
hello/views.py
from django.http import HttpRequest, JsonResponse
def request_info(request: HttpRequest) -> JsonResponse:
return JsonResponse(
{
"method": request.method,
"path": request.path,
"full_path": request.get_full_path(),
"host": request.get_host(),
"user_agent": request.headers.get("User-Agent", "unknown"),
}
)hello/urls.py
urlpatterns = [
path("request-info/", views.request_info, name="request-info"),
]Данным запроса нельзя доверять: типы, диапазоны и разрешения проверяются до использования. Адрес клиента из REMOTE_ADDR или proxy-заголовков тоже требует корректной настройки доверенных прокси.
4. HttpResponse и отправка ответа
HttpResponse хранит тело ответа, статус, тип содержимого и заголовки. Основные аргументы конструктора:
HttpResponse(
content=b"",
content_type=None,
status=200,
reason=None,
charset=None,
headers=None,
)Строка преобразуется в байты с выбранной кодировкой. По умолчанию тип — text/html; charset=utf-8. Для текста, файлов и JSON лучше указывать корректный Content-Type или использовать специализированный класс ответа.
hello/views.py
from django.http import HttpRequest, HttpResponse
def health(request: HttpRequest) -> HttpResponse:
return HttpResponse(
"ok\n",
content_type="text/plain; charset=utf-8",
headers={"Cache-Control": "no-store"},
)
def created(request: HttpRequest) -> HttpResponse:
return HttpResponse("Создано", status=201)Заголовок можно установить и после создания объекта:
def ready(request: HttpRequest) -> HttpResponse:
response = HttpResponse("Готово")
response["X-Request-Source"] = "django-guide"
return responseТекст в reason меняет reason phrase, но клиентская логика должна опираться на числовой статус. Заголовки и тело должны соответствовать HTTP-семантике: например, ответ 204 No Content не должен содержать тело.
5. Параметры представлений
Динамические части пути передаются в view как именованные аргументы. Синтаксис path() — <converter:name>. Встроенные конвертеры:
str— непустая строка без/, используется по умолчанию;int— неотрицательное целое число;slug— ASCII-буквы, цифры,_и-;uuid— UUID;path— строка, которая может содержать/.
hello/views.py
from uuid import UUID
from django.http import HttpRequest, JsonResponse
def user_detail(
request: HttpRequest,
user_id: int,
section: str = "profile",
) -> JsonResponse:
return JsonResponse({"user_id": user_id, "section": section})
def document(request: HttpRequest, document_id: UUID) -> JsonResponse:
return JsonResponse({"document_id": str(document_id)})hello/urls.py
urlpatterns = [
path("users/<int:user_id>/", views.user_detail, name="user-detail"),
path(
"users/<int:user_id>/<slug:section>/",
views.user_detail,
name="user-section",
),
path("documents/<uuid:document_id>/", views.document, name="document"),
]Необязательный сегмент описывают отдельным маршрутом, а значение по умолчанию — параметром функции. Имена в URL должны точно совпадать с параметрами view. Если формат не соответствует конвертеру, маршрут не совпадёт и Django вернёт 404 ещё до вызова view.
Для сложного шаблона можно использовать именованные группы re_path():
re_path(r"^users/(?P<name>[A-Za-z]+)/$", views.user_by_name)6. Вложенные маршруты и функция include
include() передаёт оставшуюся часть пути другой таблице маршрутов. Это позволяет приложению владеть своими URL и не раздувать корневой config/urls.py.
config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("hello.urls")),
]hello/urls.py
from django.urls import path
from . import views
app_name = "hello"
product_patterns = [
path("", views.product_detail, name="product-detail"),
path("comments/", views.product_comments, name="product-comments"),
]
urlpatterns = [
path("", views.index, name="index"),
path("products/<int:product_id>/", include(product_patterns)),
]hello/views.py
from django.http import HttpRequest, HttpResponse
def product_detail(request: HttpRequest, product_id: int) -> HttpResponse:
return HttpResponse(f"Товар {product_id}")
def product_comments(request: HttpRequest, product_id: int) -> HttpResponse:
return HttpResponse(f"Комментарии к товару {product_id}")Параметры родительского пути передаются во все вложенные view. В примере обе функции получают product_id, хотя во вложенных шаблонах он повторно не объявлен. На практике чаще создают полноценный hello/urls.py, а локальный список применяют для маленькой логически связанной группы.
7. Параметры строки запроса
Строка запроса идёт после ? и не участвует в выборе маршрута: /users/?name=Ann&tag=python&tag=web. Django помещает параметры в request.GET, который является QueryDict; значения в нём остаются строками.
hello/views.py
from django.http import HttpRequest, JsonResponse
def user_search(request: HttpRequest) -> JsonResponse:
name = request.GET.get("name", "")
tags = request.GET.getlist("tag")
try:
limit = min(max(int(request.GET.get("limit", "20")), 1), 100)
except ValueError:
return JsonResponse({"error": "limit must be an integer"}, status=400)
return JsonResponse({"name": name, "tags": tags, "limit": limit})hello/urls.py
urlpatterns = [
path("users/", views.user_search, name="user-search"),
].get(key, default) возвращает одно значение, а .getlist(key) — все повторения ключа. Значение по умолчанию применяется, когда ключ отсутствует, но не преобразует тип. Поэтому числа, даты и перечисления нужно валидировать явно. Query-параметры подходят для фильтрации, сортировки и пагинации; идентификатор конкретного ресурса обычно помещают в путь.
8. Переадресация и отправка статусных кодов
Перенаправление сообщает клиенту новый адрес. redirect() может принять URL, имя маршрута или объект с get_absolute_url(). По умолчанию это временный редирект; permanent=True создаёт постоянный.
hello/views.py
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect
def old_about(request: HttpRequest) -> HttpResponse:
return redirect("hello:about", permanent=True)
def age_gate(request: HttpRequest, age: int) -> HttpResponse:
if age > 120:
return HttpResponseBadRequest("Некорректный возраст")
if age < 18:
return HttpResponse("Доступ запрещён", status=403)
return HttpResponse("Доступ разрешён")hello/urls.py
urlpatterns = [
path("old-about/", views.old_about, name="old-about"),
path("access/<int:age>/", views.age_gate, name="age-gate"),
]У Django есть классы для распространённых ответов: HttpResponseBadRequest (400), HttpResponseForbidden (403), HttpResponseNotFound (404), HttpResponseNotAllowed (405), HttpResponseGone (410), HttpResponseServerError (500). Часто удобнее использовать Http404, get_object_or_404() или общий HttpResponse(status=...).
from django.http import Http404
def item(request: HttpRequest, item_id: int) -> HttpResponse:
if item_id > 100:
raise Http404("Товар не найден")
return HttpResponse(f"Товар {item_id}")Постоянный редирект браузеры и поисковики могут кэшировать, поэтому его применяют только когда старый URL действительно заменён навсегда.
9. Отправка json
JsonResponse сериализует данные, устанавливает Content-Type: application/json и наследуется от HttpResponse. По умолчанию верхнеуровневым значением должен быть словарь — это контролирует safe=True.
hello/views.py
from django.http import HttpRequest, JsonResponse
def profile(request: HttpRequest) -> JsonResponse:
return JsonResponse({"name": "Ann", "skills": ["Python", "Django"]})
def tags(request: HttpRequest) -> JsonResponse:
return JsonResponse(["python", "django"], safe=False)Для настройки стандартного json.dumps() используется json_dumps_params:
def greeting(request: HttpRequest) -> JsonResponse:
return JsonResponse(
{"message": "Привет"},
json_dumps_params={"ensure_ascii": False},
)Для своего типа можно определить encoder. На практике границу API часто проще сделать явной и сначала преобразовать доменный объект в словарь.
from dataclasses import asdict, dataclass
@dataclass(frozen=True)
class Person:
name: str
age: int
def person(request: HttpRequest) -> JsonResponse:
return JsonResponse(asdict(Person(name="Bob", age=41)))JsonResponse не валидирует схему и не превращает Django в полноценный API-фреймворк: обработку входного JSON, ошибки, авторизацию и документирование контракта нужно организовать отдельно.
10. Отправка и получение кук
Cookie — небольшое значение, которое сервер устанавливает в ответе, а браузер затем отправляет с подходящими запросами. В cookie не следует хранить секреты или большие данные: клиент видит и может удалить обычное значение.
hello/views.py
from django.http import HttpRequest, HttpResponse
def set_theme(request: HttpRequest) -> HttpResponse:
theme = request.GET.get("theme", "light")
if theme not in {"light", "dark"}:
theme = "light"
response = HttpResponse(f"Тема: {theme}")
response.set_cookie(
"theme",
theme,
max_age=30 * 24 * 60 * 60,
secure=True,
httponly=True,
samesite="Lax",
)
return response
def get_theme(request: HttpRequest) -> HttpResponse:
theme = request.COOKIES.get("theme", "light")
return HttpResponse(f"Тема: {theme}")
def clear_theme(request: HttpRequest) -> HttpResponse:
response = HttpResponse("Настройка удалена")
response.delete_cookie("theme", samesite="Lax")
return responsemax_ageзадаёт срок жизни в секундах;secure=Trueразрешает отправку только по HTTPS;httponly=Trueзакрывает доступ из JavaScript;samesite="Lax"ограничивает часть межсайтовых отправок;pathиdomainзадают область действия.
Django умеет подписывать cookie:
response.set_signed_cookie("preference", "compact", salt="ui", httponly=True)
value = request.get_signed_cookie("preference", salt="ui", default="normal")Подпись обнаруживает изменение значения, но не шифрует его: содержимое остаётся читаемым. Secure-cookie не отправится по обычному HTTP, поэтому для локальной разработки это нужно учитывать.
Глава 3. Шаблоны
1. Создание и использование шаблонов
Шаблон хранит HTML отдельно от Python-кода и допускает вставку данных через Django Template Language. При APP_DIRS=True движок ищет шаблоны в каталогах templates установленных приложений. Чтобы одинаковые имена разных приложений не конфликтовали, внутри создают дополнительный namespace-каталог.
hello/
├── templates/
│ └── hello/
│ └── index.html
├── urls.py
└── views.pyhello/templates/hello/index.html
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Django Guide</title>
</head>
<body>
<h1>Hello, Django!</h1>
</body>
</html>hello/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
def index(request: HttpRequest) -> HttpResponse:
return render(request, "hello/index.html")render() загружает шаблон, рендерит его с контекстом и возвращает HttpResponse. Полная форма вызова:
render(request, template_name, context=None, content_type=None, status=None, using=None)TemplateResponse выполняет рендеринг позже, когда middleware уже получило объект ответа; это полезно для расширяемой обработки ответа.
from django.template.response import TemplateResponse
def about(request: HttpRequest) -> TemplateResponse:
return TemplateResponse(request, "hello/about.html")2. Передача данных в шаблоны
Контекст — словарь значений, доступных шаблону. Переменная выводится как {{ name }}. Точка служит универсальным доступом: Django по очереди пробует ключ словаря, атрибут или метод без аргументов, затем индекс.
hello/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
def profile_page(request: HttpRequest) -> HttpResponse:
context = {
"title": "Профиль",
"user_card": {"name": "Ann", "age": 29},
"skills": ["Python", "Django", "SQL"],
"address": ("Тверская", 10),
}
return render(request, "hello/profile.html", context)hello/templates/hello/profile.html
<h1>{{ title }}</h1>
<p>{{ user_card.name }}, {{ user_card.age }}</p>
<p>Первый навык: {{ skills.0 }}</p>
<p>Адрес: {{ address.0 }}, дом {{ address.1 }}</p>В шаблон можно передавать экземпляры классов:
from dataclasses import dataclass
from django.shortcuts import render
@dataclass(frozen=True)
class Author:
name: str
def author_page(request):
return render(request, "hello/profile.html", {"author": Author("Ann")})<p>Автор: {{ author.name }}</p>Шаблон не должен содержать прикладную логику или выполнять произвольные методы. Подготовку данных, запросы к БД и валидацию делают во view или отдельном сервисе, а шаблону передают уже понятную структуру для отображения.
3. Встроенные теги шаблонов
Теги записываются как {% ... %} и управляют рендерингом. Наиболее часто нужны условия, циклы, временные переменные и комментарии.
hello/views.py
from django.shortcuts import render
def courses(request):
return render(
request,
"hello/courses.html",
{"courses": ["Python", "Django"], "is_admin": False},
)hello/templates/hello/courses.html
{% comment %}Этот комментарий не попадёт в ответ.{% endcomment %}
{% if is_admin %}
<p>Режим администратора</p>
{% else %}
<p>Обычный режим</p>
{% endif %}
<ul>
{% for course in courses %}
<li>{{ forloop.counter }}. {{ course }}</li>
{% empty %}
<li>Курсов пока нет</li>
{% endfor %}
</ul>
{% with total=courses|length %}
<p>Всего: {{ total }}</p>
{% endwith %}
<p>Год: {% now "Y" %}</p>В if доступны сравнения, in, and, or, not, но скобки не поддерживаются так же свободно, как в Python: сложное условие лучше подготовить заранее. В цикле доступны forloop.counter, counter0, first, last, revcounter и parentloop.
По умолчанию переменные HTML-экранируются, что защищает от внедрения разметки:
{{ user_text }}
{% autoescape off %}{{ trusted_html }}{% endautoescape %}Отключать autoescape для пользовательских данных нельзя. Если приложению действительно нужен доверенный HTML, его происхождение и очистка должны быть явно контролируемы.
4. Фильтры шаблонов
Фильтр преобразует значение при выводе: {{ value|filter:argument }}. Фильтры можно объединять в цепочку, результат одного становится входом следующего.
hello/templates/hello/filters.html
<p>{{ name|default:"Гость"|capfirst }}</p>
<p>{{ description|truncatechars:80 }}</p>
<p>Цена: {{ price|floatformat:2 }}</p>
<p>Создано: {{ created_at|date:"d.m.Y H:i" }}</p>
<p>Теги: {{ tags|join:", " }}</p>
<p>Первые три: {{ tags|slice:":3"|join:", " }}</p>
<p>Количество: {{ tags|length }}</p>Часто используемые фильтры:
defaultподставляет значение для любого ложного объекта, аdefault_if_none— только дляNone;capfirst,lower,upper,cutизменяют строки;addпытается сложить числа, а при неудаче — строки или коллекции;floatformatуправляет количеством знаков и локализованным выводом;dateформатирует дату и время;join,slice,length,first,lastработают с последовательностями;escapeэкранирует HTML,safeпомечает значение доверенным.
hello/views.py
from datetime import datetime
from django.shortcuts import render
def filters_demo(request):
return render(
request,
"hello/filters.html",
{
"name": "django",
"description": "Фреймворк для веб-приложений",
"price": 1234.5,
"created_at": datetime.now(),
"tags": ["python", "web", "django", "templates"],
},
)Фильтры предназначены для отображения. Сложные вычисления, изменение данных и бизнес-правила следует выполнять до рендеринга.
5. Статические файлы
CSS, JavaScript, изображения и шрифты не рендерятся шаблонизатором. Приложение django.contrib.staticfiles находит их, а тег static строит URL. Как и шаблоны, файлы приложения лучше помещать в namespace.
hello/
└── static/
└── hello/
├── css/
│ └── site.css
└── images/
└── logo.svghello/static/hello/css/site.css
body {
max-width: 60rem;
margin: 2rem auto;
font-family: system-ui, sans-serif;
}
h1 {
color: navy;
}hello/templates/hello/index.html
{% load static %}
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="{% static 'hello/css/site.css' %}">
<title>Django Guide</title>
</head>
<body>
<img src="{% static 'hello/images/logo.svg' %}" alt="Логотип">
<h1>Hello, Django!</h1>
</body>
</html>Базовая настройка содержит:
config/settings.py
STATIC_URL = "static/"Общие исходные файлы проекта можно добавить через STATICFILES_DIRS:
STATICFILES_DIRS = [BASE_DIR / "static"]В production Django обычно не раздаёт статику разработческим сервером. Команда collectstatic собирает найденные файлы в STATIC_ROOT, после чего их обслуживает веб-сервер, CDN или специализированный middleware.
python manage.py collectstatic6. TemplateView
TemplateView — class-based view для страницы, которой достаточно отрендерить шаблон. Для полностью статической страницы класс можно настроить прямо в маршруте.
hello/urls.py
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path(
"about/",
TemplateView.as_view(template_name="hello/about.html"),
name="about",
),
]as_view() превращает класс в вызываемый обработчик, понятный маршрутизатору. Постоянный контекст можно передать через extra_context, но для динамических данных лучше создать подкласс и переопределить get_context_data().
hello/views.py
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "hello/about.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "О проекте"
context["features"] = ["views", "templates", "static"]
return contexthello/urls.py
from .views import AboutView
urlpatterns = [
path("about/", AboutView.as_view(), name="about"),
]hello/templates/hello/about.html
<h1>{{ title }}</h1>
<p>{{ features|join:", " }}</p>Если обработчику нужна сложная ветвящаяся логика, простая функция-view часто остаётся понятнее.
7. Конфигурация шаблонов
Настройка TEMPLATES описывает движки и места поиска шаблонов.
config/settings.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]BACKENDвыбирает движок;DIRSзадаёт общие каталоги проекта;APP_DIRS=Trueвключает поиск внутриtemplates/установленных приложений;OPTIONSсодержит дополнительные параметры и context processors.
После такой настройки общий шаблон templates/base.html доступен как base.html, а шаблон hello/templates/hello/index.html — как hello/index.html.
Context processor — функция, добавляющая значения в контекст каждого шаблона, отрендеренного с RequestContext. Она удобна для небольших действительно глобальных данных, но не должна выполнять тяжёлый запрос к БД на каждой странице.
hello/context_processors.py
def project_meta(request):
return {"project_name": "Django Guide"}Регистрация:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"hello.context_processors.project_meta",
],
},
}
]Теперь {{ project_name }} доступен в шаблонах, отрендеренных через render().
8. Расширение шаблонов и фильтр extends
Наследование шаблонов убирает повторение общей разметки. Базовый шаблон задаёт каркас и именованные блоки, дочерний подключает его через {% extends %} и переопределяет нужные блоки. Несмотря на название темы, extends — тег, а не фильтр.
templates/base.html
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>{% block title %}Django Guide{% endblock %}</title>
</head>
<body>
<nav>
<a href="{% url 'hello:index' %}">Главная</a>
<a href="{% url 'hello:about' %}">О проекте</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>Учебный проект</footer>
</body>
</html>hello/templates/hello/index.html
{% extends "base.html" %}
{% block title %}Главная · Django Guide{% endblock %}
{% block content %}
<h1>Главная страница</h1>
<p>Первый проект на Django.</p>
{% endblock %}extends должен быть первым шаблонным тегом дочернего файла. Необъявленный дочерним шаблоном блок сохраняет содержимое базового. Содержимое родительского блока можно дополнить:
{% block content %}
{{ block.super }}
<p>Дополнительный текст.</p>
{% endblock %}Именованные маршруты в {% url %} устойчивее жёстко записанных /about/: при изменении маршрута шаблон менять не потребуется.
9. Вложенные шаблоны и фильтр include
include вставляет отрендеренный фрагмент в текущую страницу. Это подходит для повторяющихся карточек, баннеров, меню и других небольших компонентов. Несмотря на название темы, include также является тегом.
hello/templates/hello/_banner.html
<aside class="banner">
<strong>{{ title }}</strong>
{% if text %}<span>{{ text }}</span>{% endif %}
</aside>hello/templates/hello/index.html
{% extends "base.html" %}
{% block content %}
<h1>Главная</h1>
{% include "hello/_banner.html" with title="Python" text="Django Guide" %}
{% endblock %}По умолчанию вложенный шаблон получает весь контекст родителя. only создаёт более явный контракт и передаёт только перечисленные значения:
{% include "hello/_banner.html" with title=course.name text=course.summary only %}Имя фрагмента может приходить из переменной:
{% include selected_partial %}Наследование и включение решают разные задачи: extends формирует каркас целой страницы, а include переиспользует фрагмент внутри уже выбранного каркаса. Слишком сложный partial с множеством условий лучше заменить отдельной подготовкой контекста, custom template tag или самостоятельным view-компонентом.
Глава 4. Работа с формами
1. Отправка форм
HTML-форма обычно отправляет данные методом POST. В Django пары name=value попадают в request.POST — объект QueryDict. Одиночное значение читается через get(), а несколько элементов с одинаковым name — через getlist(). Полученные строки нельзя считать проверенными данными: преобразование типов и проверку ограничений лучше поручить Django Form.
Во все внутренние POST-формы добавляют {% csrf_token %}. Токен связывает форму с пользовательской сессией и позволяет middleware отклонить поддельный запрос с другого сайта.
<!-- library/templates/library/preferences.html -->
<form method="post">
{% csrf_token %}
<label>Имя <input name="reader_name" required></label>
<label><input type="checkbox" name="genres" value="sci-fi"> Научная фантастика</label>
<label><input type="checkbox" name="genres" value="history"> История</label>
<button type="submit">Сохранить</button>
</form># library/views.py
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import render
def preferences(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
name = request.POST.get("reader_name", "")
genres = request.POST.getlist("genres")
return JsonResponse({"name": name, "genres": genres})
return render(request, "library/preferences.html")Если форма загружает файлы, ей нужен enctype="multipart/form-data", а файлы читаются из request.FILES, не из request.POST.
2. Определение форм Django
Класс, унаследованный от forms.Form, декларативно описывает поля, HTML-виджеты, преобразование входных строк в Python-типы и правила валидации. Форму обычно размещают в forms.py, передают в шаблон и связывают с данными только при POST-запросе. Несвязанная форма показывает пустые поля, связанная хранит отправленные значения и ошибки.
# library/forms.py
from django import forms
class BookRequestForm(forms.Form):
title = forms.CharField(label="Название", max_length=200)
copies = forms.IntegerField(label="Экземпляров", min_value=1, max_value=10)# library/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from .forms import BookRequestForm
def request_book(request: HttpRequest) -> HttpResponse:
data = request.POST if request.method == "POST" else None
form = BookRequestForm(data)
if request.method == "POST" and form.is_valid():
title = form.cleaned_data["title"]
copies = form.cleaned_data["copies"]
return HttpResponse(f"Запрошено: {title}, {copies} шт.")
return render(request, "library/book_request.html", {"form": form})<!-- library/templates/library/book_request.html -->
<form method="post" novalidate>
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Отправить</button>
</form>Шаблон выводит ту же связанную форму повторно, поэтому пользователь видит введённые значения и сообщения об ошибках. После успешной записи в базу обычно применяют схему POST/Redirect/GET, чтобы обновление страницы не повторяло POST.
3. Типы полей формы
Поле формы отвечает сразу за три вещи: принимает исходную строку, преобразует её в Python-значение и проверяет ограничения. Часто используются:
CharField,EmailField,URLField,SlugField,UUIDField— строки определённого формата;IntegerField,FloatField,DecimalField— числа; для денег предпочтителенDecimalField;BooleanField— флажок;DateField,TimeField,DateTimeField,DurationField— дата и время;ChoiceField,TypedChoiceField,MultipleChoiceField— выбор из разрешённого набора;FileField,ImageField— загруженные файлы;ImageFieldтребует Pillow;JSONField— JSON, преобразуемый в Python-структуру.
Виджет определяет HTML-представление поля, но не его тип данных. Например, один CharField может рендериться однострочным TextInput, многострочным Textarea или скрытым HiddenInput.
# library/forms.py
from django import forms
class BookFilterForm(forms.Form):
query = forms.CharField(required=False, max_length=100)
min_year = forms.IntegerField(required=False, min_value=1450)
published_after = forms.DateField(
required=False,
widget=forms.DateInput(attrs={"type": "date"}),
)
format = forms.TypedChoiceField(
required=False,
choices=[("", "Любой"), ("paper", "Бумага"), ("ebook", "Электронная")],
coerce=str,
empty_value="",
)
languages = forms.MultipleChoiceField(
required=False,
choices=[("ru", "Русский"), ("en", "Английский")],
widget=forms.CheckboxSelectMultiple,
)ChoiceField проверяет, что клиент не подменил значение произвольной строкой. MultipleChoiceField возвращает список, а DateField — datetime.date, поэтому после is_valid() ручной разбор строк не нужен.
4. Настройка формы и ее полей
Общие параметры поля управляют подписью и поведением формы: label задаёт метку, help_text — подсказку, initial — начальное значение, required=False разрешает пустой ввод, disabled=True запрещает менять поле, а widget выбирает HTML-элемент. Порядок задаётся объявлением полей или field_order.
initial — не значение по умолчанию для базы и не доверенные данные. Оно лишь заполняет несвязанную форму. В связанной форме приоритет имеет ввод пользователя.
# library/forms.py
from datetime import date
from django import forms
class BookForm(forms.Form):
title = forms.CharField(
label="Название",
min_length=2,
max_length=200,
help_text="Укажите название с обложки.",
widget=forms.TextInput(attrs={"autocomplete": "off"}),
)
description = forms.CharField(
label="Описание",
required=False,
widget=forms.Textarea(attrs={"rows": 4}),
)
published_on = forms.DateField(
label="Дата публикации",
initial=date.today,
widget=forms.DateInput(attrs={"type": "date"}),
)
field_order = ["title", "published_on", "description"]Форму целиком можно вывести как form.as_div, form.as_p, form.as_ul или form.as_table. Эти способы удобны для прототипа. Для реального интерфейса чаще рендерят поля явно, чтобы контролировать разметку, ошибки и доступность.
5. Валидация данных
Клиентские атрибуты required, minlength, max улучшают интерфейс, но их легко обойти. Источником истины служит серверная проверка form.is_valid(). Она приводит данные к нужным типам, заполняет cleaned_data и errors и возвращает False, если хотя бы одно правило нарушено.
Валидацию одного поля размещают в clean_<имя>(); правила, зависящие от нескольких полей, — в clean(). Об ошибке сообщают через forms.ValidationError. В clean() следует сначала вызвать super().clean() и учитывать, что невалидного поля может не быть в словаре.
# library/forms.py
from django import forms
class BookPeriodForm(forms.Form):
title = forms.CharField(min_length=2, max_length=200)
first_year = forms.IntegerField(min_value=1450)
last_year = forms.IntegerField(required=False, min_value=1450)
def clean_title(self) -> str:
title = self.cleaned_data["title"].strip()
if title.casefold() == "unknown":
raise forms.ValidationError("Укажите настоящее название.")
return title
def clean(self) -> dict[str, object]:
cleaned = super().clean()
first = cleaned.get("first_year")
last = cleaned.get("last_year")
if isinstance(first, int) and isinstance(last, int) and last < first:
self.add_error("last_year", "Последний год не может быть меньше первого.")
return cleaned# library/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from .forms import BookPeriodForm
def book_period(request: HttpRequest) -> HttpResponse:
data = request.POST if request.method == "POST" else None
form = BookPeriodForm(data)
if request.method == "POST" and form.is_valid():
return HttpResponse(str(form.cleaned_data))
return render(request, "library/book_period.html", {"form": form})Читать cleaned_data нужно только после успешного is_valid(). Если форма невалидна, её повторный вывод автоматически покажет ошибки рядом с полями.
6. Детальная настройка полей формы
В шаблоне каждое поле становится BoundField: оно объединяет описание поля, введённое значение и ошибки конкретного экземпляра формы. Доступны field.label_tag, field.help_text, field.errors, field.id_for_label, field.value и сам {{ field }}. Итерация по форме позволяет построить собственную разметку без ручного перечисления всех полей.
<!-- library/templates/library/book_form.html -->
<form method="post" novalidate>
{% csrf_token %}
{% if form.non_field_errors %}
<div class="form-errors">{{ form.non_field_errors }}</div>
{% endif %}
{% for field in form %}
<div class="form-row{% if field.errors %} form-row--invalid{% endif %}">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
{% for error in field.errors %}
<p class="field-error">{{ error }}</p>
{% endfor %}
</div>
{% endfor %}
<button type="submit">Сохранить</button>
</form>Ошибки, не относящиеся к одному полю, попадают в form.non_field_errors; например, их создаёт ValidationError из clean(). Django экранирует обычные значения в шаблоне, поэтому не следует помечать пользовательский ввод как safe.
7. Стилизация полей форм
CSS-классы и другие HTML-атрибуты передают виджету через attrs. Это презентационный слой: серверные ограничения всё равно задаются параметрами поля и валидаторами. Общий класс можно назначить в __init__, не повторяя attrs у каждого объявления.
# library/forms.py
from django import forms
class StyledBookForm(forms.Form):
title = forms.CharField(label="Название", max_length=200)
synopsis = forms.CharField(
label="Аннотация",
required=False,
widget=forms.Textarea(attrs={"rows": 5}),
)
cover = forms.ImageField(label="Обложка", required=False)
def __init__(self, *args: object, **kwargs: object) -> None:
super().__init__(*args, **kwargs)
for field in self.fields.values():
current = field.widget.attrs.get("class", "")
field.widget.attrs["class"] = f"control {current}".strip()<!-- library/templates/library/styled_book.html -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_div }}
<button class="button" type="submit">Добавить книгу</button>
</form>/* library/static/library/forms.css */
.control { display: block; width: 100%; padding: .5rem; }
.form-row--invalid .control { border-color: #b42318; }
.field-error { color: #b42318; }При загрузке файла в POST-ветке форму создают как StyledBookForm(request.POST, request.FILES). Для готовых дизайн-систем можно использовать специализированный renderer или библиотеку форм, но базовый механизм остаётся тем же: поле валидирует данные, виджет формирует HTML.
Глава 5. Модели
1. Подключение к базе данных
Настройки соединений находятся в словаре DATABASES файла settings.py. Соединение default обязательно. Для локального знакомства подходит SQLite: сервер не нужен, база хранится в одном файле. В production обычно используют PostgreSQL и берут пароль и другие параметры из переменных окружения, а не записывают секреты в репозиторий.
# config/settings.py — SQLite
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}# config/settings.py — PostgreSQL
import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.getenv("POSTGRES_HOST", "127.0.0.1"),
"PORT": os.getenv("POSTGRES_PORT", "5432"),
"CONN_MAX_AGE": 60,
"CONN_HEALTH_CHECKS": True,
}
}После изменения моделей сначала создают файлы миграций, затем применяют их:
python -m pip install "psycopg[binary]" # драйвер PostgreSQL
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrationsМиграции являются версионируемой историей схемы и должны попадать в репозиторий. Настройки ATOMIC_REQUESTS, AUTOCOMMIT, OPTIONS, TIME_ZONE, TEST и несколько именованных соединений применяют осознанно: они меняют транзакционное поведение, драйвер или тестовую базу.
2. Создание моделей
Модель — класс-наследник django.db.models.Model. Поля класса описывают столбцы, а экземпляр — одну строку таблицы. Если первичный ключ не задан, Django добавляет id согласно DEFAULT_AUTO_FIELD. Метод __str__() задаёт человекочитаемое представление объекта в shell, admin и списках выбора.
# library/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=120)
email = models.EmailField(unique=True)
def __str__(self) -> str:
return self.name
class Book(models.Model):
author = models.ForeignKey(
Author,
on_delete=models.PROTECT,
related_name="books",
)
title = models.CharField(max_length=200)
published_on = models.DateField(null=True, blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
ordering = ["title"]
constraints = [
models.UniqueConstraint(
fields=["author", "title"],
name="unique_book_title_per_author",
)
]
def __str__(self) -> str:
return self.titleПосле объявления выполняют makemigrations и migrate. blank=True относится к валидации формы, null=True — к хранению SQL NULL; для строк обычно предпочитают пустую строку вместо одновременного использования двух вариантов «нет значения».
3. Типы полей моделей
Тип поля определяет Python-тип, столбец базы, базовую валидацию и стандартный виджет ModelForm. Основные группы:
- строки:
CharField,TextField,EmailField,SlugField,URLField,UUIDField; - числа:
IntegerField,BigIntegerField,PositiveIntegerField,DecimalField,FloatField; - время:
DateField,TimeField,DateTimeField,DurationField; - прочее:
BooleanField,BinaryField,JSONField,FileField,ImageField; - связи:
ForeignKey,ManyToManyField,OneToOneField.
Значимые параметры: primary_key, unique, db_index, default, null, blank, choices, editable, validators, db_column, а для файлов — upload_to. Для изменяемого значения по умолчанию передают функцию, например default=dict, а не готовый словарь. auto_now_add фиксирует момент создания, auto_now — каждого сохранения; если нужен пользовательский контроль, лучше default=timezone.now.
# library/models.py
import uuid
from django.db import models
from django.utils import timezone
class Edition(models.Model):
class Format(models.TextChoices):
PAPER = "paper", "Бумажная"
EBOOK = "ebook", "Электронная"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
isbn = models.CharField(max_length=17, unique=True)
format = models.CharField(max_length=10, choices=Format, default=Format.PAPER)
pages = models.PositiveIntegerField()
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(default=timezone.now, editable=False)Ограничения, которые важны для целостности данных, следует дублировать на уровне базы через unique, CheckConstraint и UniqueConstraint, а не оставлять только в форме.
4. QuerySet API
Менеджер objects создаёт QuerySet — ленивое описание запроса. Методы filter(), exclude(), order_by(), select_related() возвращают новый QuerySet и могут объединяться в цепочку. SQL обычно выполняется только при вычислении результата: итерации, преобразовании в list, срезе с шагом, len, bool или сериализации. count() и exists() выполняют специализированные запросы.
# library/services.py
from .models import Book
def recent_expensive_books():
books = (
Book.objects
.select_related("author")
.filter(published_on__year__gte=2020, price__gte=1000)
.exclude(title__icontains="черновик")
.order_by("-published_on", "title")
)
# До итерации запрос обычно не выполнялся.
return [(book.title, book.author.name) for book in books[:20]]select_related() присоединяет одиночные связи (ForeignKey, OneToOneField) через JOIN. prefetch_related() делает дополнительные запросы и связывает коллекции (ManyToManyField, обратный ForeignKey) в Python. Без них обращение к связи в цикле часто создаёт проблему N+1 запросов.
У многих методов есть асинхронные варианты: aget, acreate, aexists, acount, а QuerySet поддерживает async for. Ленивые методы вроде filter() отдельного await не требуют. Транзакционные участки ORM по-прежнему часто оформляют синхронной функцией и вызывают через sync_to_async.
5. Создание и получение объектов модели
create() создаёт и сразу сохраняет объект; альтернативный путь — конструктор плюс save(). bulk_create() вставляет коллекцию эффективнее, но не вызывает пользовательский save() и сигналы для каждого объекта. Для получения применяют all(), filter() и get().
# Примеры для Django shell
from library.models import Author, Book
author = Author.objects.create(name="Ursula Le Guin", email="ursula@example.org")
book = Book(author=author, title="The Dispossessed", price="1290.00")
book.full_clean()
book.save()
books = Book.objects.bulk_create([
Book(author=author, title="A Wizard of Earthsea", price="990.00"),
Book(author=author, title="The Left Hand of Darkness", price="1190.00"),
])
all_books = Book.objects.all()
earthsea = Book.objects.get(author=author, title="A Wizard of Earthsea")
cheap = Book.objects.filter(price__lt=1000)get() должен вернуть ровно одну строку: при отсутствии возникает Book.DoesNotExist, при нескольких совпадениях — Book.MultipleObjectsReturned. Во view для поиска по URL обычно используют get_object_or_404(Book, pk=book_id).
get_or_create() возвращает (object, created) и полезен вместе с уникальным ограничением базы. Без такого ограничения параллельные запросы могут создать дубли.
author, created = Author.objects.get_or_create(
email="octavia@example.org",
defaults={"name": "Octavia Butler"},
)6. Редактирование и удаление объектов модели
Загруженный объект изменяют и сохраняют методом save(). update_fields ограничивает список обновляемых столбцов. QuerySet.update() выполняет прямой SQL UPDATE без загрузки объектов и без вызова их save(); выражение F() вычисляется в базе и защищает от потерянного обновления при операции вида «увеличить на один».
from decimal import Decimal
from django.db.models import F
from library.models import Book
book = Book.objects.get(pk=1)
book.title = "Новое название"
book.save(update_fields=["title"])
changed = Book.objects.filter(author_id=3).update(price=F("price") * Decimal("1.05"))update_or_create() ищет объект, затем обновляет его или создаёт. bulk_update() обновляет набор экземпляров выбранными полями. Как и другие bulk-операции, эти методы обходят часть объектного жизненного цикла, поэтому подходят не для каждой модели.
book, created = Book.objects.update_or_create(
author_id=3,
title="Kindred",
defaults={"price": "1350.00"},
)
deleted_count, details = Book.objects.filter(title__icontains="черновик").delete()delete() возвращает количество удалённых объектов и детализацию по моделям. Итог для связанных строк определяется on_delete: CASCADE, PROTECT, RESTRICT, SET_NULL, SET_DEFAULT или DO_NOTHING.
7. Фильтрация
Условие строится как поле__lookup=значение. Без lookup используется точное равенство. Часто нужны exact/iexact, contains/icontains, startswith, endswith, in, range, gt/gte/lt/lte, isnull, regex, а для дат — date, year, month, day, hour и другие компоненты.
from django.db.models import Q
from library.models import Book
books = Book.objects.filter(
Q(title__icontains="python") | Q(author__name__icontains="lutz"),
price__range=(500, 2500),
).exclude(published_on__isnull=True)
recent = Book.objects.filter(
published_on__year__gte=2020,
author_id__in=[1, 2, 3],
)Именованные аргументы соединяются через AND. Объекты Q добавляют OR, NOT (~Q(...)) и группировку. При фильтрации по связи путь продолжается через двойное подчёркивание: author__name__icontains. Условия всегда передаются как параметры ORM; соединять пользовательский ввод с SQL-строкой не нужно.
8. values и values_list и сортировка
order_by("field") сортирует по возрастанию, order_by("-field") — по убыванию; несколько полей задают устойчивый порядок. values() возвращает словари, values_list() — кортежи, а values_list("field", flat=True) — плоскую последовательность. Они полезны, когда не нужны полноценные экземпляры модели.
from library.models import Book
cards = list(
Book.objects
.select_related("author")
.order_by("author__name", "-published_on", "title")
.values("id", "title", "author__name", "price")[:50]
)
titles = list(
Book.objects
.order_by("title")
.values_list("title", flat=True)
)После values() QuerySet остаётся ленивым. Выбирайте только нужные столбцы, но не используйте проекцию автоматически: если дальше нужна логика модели, удобнее получить экземпляры. Порядок без order_by() база не гарантирует, даже если строки часто кажутся отсортированными по id.
9. Операции с множествами
distinct() устраняет повторяющиеся строки. Он особенно нужен после JOIN по отношениям «многие ко многим», где одна книга может появиться несколько раз. union(), intersection() и difference() соответствуют SQL UNION, INTERSECT и EXCEPT; выборки должны иметь совместимые типы и одинаковое число столбцов. Поддержка последних двух операций зависит от СУБД.
from library.models import Book
fiction = Book.objects.filter(tags__name="fiction").values_list("id", flat=True)
award = Book.objects.filter(tags__name="award").values_list("id", flat=True)
all_ids = fiction.union(award)
both_ids = fiction.intersection(award)
fiction_without_award = fiction.difference(award)
unique_authors = Book.objects.values_list("author__name", flat=True).distinct()union() по умолчанию удаляет дубли; union(other, all=True) сохраняет их. После комбинирования разрешён не весь QuerySet API, поэтому сложную дальнейшую обработку иногда проще выразить через Q, подзапросы или отдельный запрос.
10. Получение отдельных объектов и проверка их наличия
first() и last() возвращают объект либо None; перед ними лучше явно задать order_by(). earliest() и latest() выбирают объект по указанным полям и при пустом наборе выбрасывают DoesNotExist. Поле по умолчанию можно определить в Meta.get_latest_by.
exists() проверяет наличие хотя бы одной строки, не загружая все объекты. contains(obj) проверяет, входит ли сохранённый объект в QuerySet. Если затем всё равно предстоит полностью перебрать маленькую выборку, отдельный exists() может оказаться лишним вторым запросом.
from library.models import Book
newest = (
Book.objects
.exclude(published_on__isnull=True)
.latest("published_on")
)
first_by_title = Book.objects.order_by("title").first()
has_expensive = Book.objects.filter(price__gte=5000).exists()
if first_by_title is not None:
is_recent = Book.objects.filter(published_on__year__gte=2020).contains(first_by_title)Асинхронные аналоги называются afirst(), alast(), aearliest(), alatest(), aexists() и acontains().
11. Агрегатные операции
count() превращается в SQL COUNT; len(queryset) считает уже загруженный набор, но иначе сначала загружает все строки. aggregate() вычисляет одно итоговое значение для всей выборки и возвращает словарь. annotate() добавляет вычисленное значение каждой строке или группе.
from django.db.models import Avg, Count, Max, Min, Sum
from library.models import Author, Book
summary = Book.objects.aggregate(
books=Count("id"),
average_price=Avg("price"),
cheapest=Min("price"),
most_expensive=Max("price"),
total_price=Sum("price"),
)
authors = (
Author.objects
.annotate(book_count=Count("books"))
.filter(book_count__gte=3)
.order_by("-book_count")
)При агрегации через несколько отношений JOIN может размножить строки. Проверяйте SQL и при необходимости используйте Count(..., distinct=True), подзапросы или раздельные вычисления. Порядок annotate() и filter() влияет на результат.
12. Выполнение SQL-выражений
Сырой SQL нужен редко: QuerySet переносимее и сохраняет знания модели. Model.objects.raw() выполняет SELECT и возвращает RawQuerySet экземпляров модели; в результате должен присутствовать первичный ключ. Для произвольных SELECT/INSERT/UPDATE/DELETE используют connection.cursor().
Параметры передают отдельно через params или второй аргумент execute(). Нельзя собирать SQL из пользовательского ввода f-строкой, % или format() — это создаёт SQL-инъекцию. Django использует плейсхолдер %s независимо от конкретного драйвера.
from django.db import connection
from library.models import Book
needle = "Python"
books = Book.objects.raw(
"SELECT id, title, price FROM library_book WHERE title LIKE %s",
[f"%{needle}%"],
)
with connection.cursor() as cursor:
cursor.execute(
"SELECT author_id, COUNT(*) FROM library_book "
"WHERE price >= %s GROUP BY author_id",
[1000],
)
rows = cursor.fetchall()Имена таблиц и столбцов параметризовать нельзя. Если их нужно выбирать динамически, используйте заранее разрешённый список и connection.ops.quote_name(). Изменяющие несколько таблиц операции оборачивают в transaction.atomic().
13. CRUD. Все базовые операции с моделями в веб-приложении
CRUD объединяет создание, чтение, изменение и удаление. Современный минимальный вариант использует ModelForm, get_object_or_404, именованные маршруты и POST для операций, меняющих состояние. После успешного POST выполняют redirect.
# library/forms.py
from django import forms
from .models import Book
class BookModelForm(forms.ModelForm):
class Meta:
model = Book
fields = ["author", "title", "published_on", "price"]
widgets = {"published_on": forms.DateInput(attrs={"type": "date"})}# library/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from .forms import BookModelForm
from .models import Book
def book_list(request: HttpRequest) -> HttpResponse:
books = Book.objects.select_related("author").all()
return render(request, "library/book_list.html", {"books": books})
def book_create(request: HttpRequest) -> HttpResponse:
data = request.POST if request.method == "POST" else None
form = BookModelForm(data)
if request.method == "POST" and form.is_valid():
form.save()
return redirect("book-list")
return render(request, "library/book_form.html", {"form": form})
def book_edit(request: HttpRequest, pk: int) -> HttpResponse:
book = get_object_or_404(Book, pk=pk)
data = request.POST if request.method == "POST" else None
form = BookModelForm(data, instance=book)
if request.method == "POST" and form.is_valid():
form.save()
return redirect("book-list")
return render(request, "library/book_form.html", {"form": form})
@require_POST
def book_delete(request: HttpRequest, pk: int) -> HttpResponse:
get_object_or_404(Book, pk=pk).delete()
return redirect("book-list")# library/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("books/", views.book_list, name="book-list"),
path("books/new/", views.book_create, name="book-create"),
path("books/<int:pk>/edit/", views.book_edit, name="book-edit"),
path("books/<int:pk>/delete/", views.book_delete, name="book-delete"),
]Удаление по GET опасно: сканер ссылок или предварительная загрузка страницы не должны менять данные. Для реального проекта добавляют авторизацию, проверку прав и при необходимости подтверждение удаления.
14. Отношение один ко многим (One to Many)
Связь «один автор — много книг» задаёт ForeignKey на стороне многих. on_delete описывает поведение зависимых строк при удалении автора. related_name даёт понятный обратный менеджер вместо стандартного book_set.
# library/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=120)
class Book(models.Model):
author = models.ForeignKey(
Author,
on_delete=models.PROTECT,
related_name="books",
)
title = models.CharField(max_length=200)from library.models import Author, Book
author = Author.objects.create(name="Terry Pratchett")
book = author.books.create(title="Guards! Guards!")
same_author = book.author
all_books = author.books.all()
matching = Book.objects.filter(author__name__icontains="pratchett")CASCADE удаляет зависимые строки, PROTECT запрещает удаление объекта с зависимостями, RESTRICT учитывает каскад текущей операции, SET_NULL требует null=True, SET_DEFAULT — значения default, а DO_NOTHING перекладывает контроль целостности на базу. Выбор — часть бизнес-правил, не косметическая настройка.
15. Практический пример связи один ко многим
При выводе списка книг вместе с авторами используют select_related("author"), иначе обращение к book.author в каждой строке может выполнить отдельный запрос. При создании книги ModelForm построит поле выбора по ForeignKey автоматически.
# library/forms.py
from django import forms
from .models import Book
class RelatedBookForm(forms.ModelForm):
class Meta:
model = Book
fields = ["author", "title"]# library/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from .forms import RelatedBookForm
from .models import Book
def books(request: HttpRequest) -> HttpResponse:
rows = Book.objects.select_related("author").order_by("title")
return render(request, "library/books.html", {"books": rows})
def add_book(request: HttpRequest) -> HttpResponse:
data = request.POST if request.method == "POST" else None
form = RelatedBookForm(data)
if request.method == "POST" and form.is_valid():
form.save()
return redirect("book-list")
return render(request, "library/book_form.html", {"form": form})<!-- library/templates/library/books.html -->
<ul>
{% for book in books %}
<li>{{ book.title }} — {{ book.author.name }}</li>
{% empty %}
<li>Книг пока нет.</li>
{% endfor %}
</ul>Если автор задаётся контекстом URL, его безопаснее получить через get_object_or_404 и присвоить сервером, а не доверять скрытому author_id из формы.
16. Отношение многие ко многим (Many to Many)
Эта тема описывает устройство связи: одна книга имеет несколько тегов, а один тег относится к нескольким книгам. ManyToManyField создаёт промежуточную таблицу. Объект книги сначала нужно сохранить, и только затем вызывать менеджер tags.
# library/models.py
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self) -> str:
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name="books", blank=True)from library.models import Book, Tag
book = Book.objects.create(title="Designing Data-Intensive Applications")
database = Tag.objects.create(name="databases")
backend = Tag.objects.create(name="backend")
book.tags.add(database, backend)
book.tags.remove(backend)
book.tags.set([database])
book_tags = book.tags.all()
tagged_books = database.books.all()add(), remove(), clear() и set() меняют связи, а не сами связанные объекты. Для выборок с коллекциями используют prefetch_related("tags").
Если связь хранит собственные данные — например роль автора, порядок имени на обложке или дату участия — объявляют промежуточную модель и передают её в through.
# library/models.py — отдельный пример through-модели
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=120)
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField(
Author,
through="Contribution",
related_name="books",
)
class Contribution(models.Model):
book = models.ForeignKey("Book", on_delete=models.CASCADE)
author = models.ForeignKey("Author", on_delete=models.CASCADE)
role = models.CharField(max_length=30, default="author")
position = models.PositiveSmallIntegerField(default=1)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["book", "author", "role"],
name="unique_book_contribution",
)
]17. Отношение многие ко многим (Many to Many)
Вторая одноимённая тема — практическая работа со связью через форму. Пользователь выбирает несколько тегов, ModelMultipleChoiceField проверяет их идентификаторы, а ModelForm.save() обновляет промежуточную таблицу. Это безопаснее ручного чтения request.POST.getlist().
# library/forms.py
from django import forms
from .models import Book, Tag
class TaggedBookForm(forms.ModelForm):
tags = forms.ModelMultipleChoiceField(
queryset=Tag.objects.order_by("name"),
required=False,
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Book
fields = ["title", "tags"]# library/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from .forms import TaggedBookForm
def tagged_book_create(request: HttpRequest) -> HttpResponse:
data = request.POST if request.method == "POST" else None
form = TaggedBookForm(data)
if request.method == "POST" and form.is_valid():
book = form.save()
return redirect("book-edit", pk=book.pk)
return render(request, "library/tagged_book_form.html", {"form": form})При form.save(commit=False) запись ManyToMany нельзя сохранить сразу: сначала сохраняют экземпляр, затем вызывают form.save_m2m().
book = form.save(commit=False)
book.title = book.title.strip()
book.save()
form.save_m2m()Для явной модели through дополнительные поля связи создают через саму промежуточную модель либо передают through_defaults в поддерживаемые методы менеджера. На списках заранее вызывают prefetch_related("tags"), чтобы не получить N+1 запросов.
18. Отношение один к одному (One to one)
OneToOneField — внешний ключ с уникальностью: каждой основной записи соответствует не более одной зависимой. Так удобно вынести редкие или специализированные данные, например профиль автора. Обратное имя лучше задать явно через related_name.
# library/models.py
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=120)
class AuthorProfile(models.Model):
author = models.OneToOneField(
Author,
on_delete=models.CASCADE,
related_name="profile",
)
biography = models.TextField(blank=True)
website = models.URLField(blank=True)from library.models import Author, AuthorProfile
author = Author.objects.create(name="N. K. Jemisin")
profile = AuthorProfile.objects.create(
author=author,
website="https://example.org/authors/jemisin",
)
same_author = profile.author
same_profile = author.profile
authors_with_site = Author.objects.filter(profile__website__startswith="https://")Если профиль опционален, обратный доступ author.profile может выбросить AuthorProfile.DoesNotExist; это нужно учитывать или получать данные через запрос. Для совместной загрузки используют select_related("profile"). Указывать primary_key=True на поле связи необязательно: отдельный первичный ключ часто удобнее, а уникальность отношения Django всё равно обеспечивает.
Руководство по работе с базами данных в Python
Python работает с реляционными базами как напрямую через DB-API-драйверы, так и через ORM. Независимо от инструмента важны параметризованные запросы, явные транзакции, своевременное закрытие ресурсов и миграции схемы.
Глава 1. SQLite
Подключение к SQLite
SQLite хранит базу в одном файле и не требует отдельного сервера. Встроенный модуль sqlite3 реализует Python DB-API. connect создаёт файл при отсутствии, :memory: создаёт временную базу в памяти. Соединение предоставляет курсор, методы execute, executemany, fetchone и fetchall.
import sqlite3
from contextlib import closing
with closing(sqlite3.connect("app.db")) as connection:
connection.row_factory = sqlite3.Row
with connection: # commit при успехе, rollback при исключении
connection.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER CHECK (age >= 0)
)
"""
)
version = connection.execute("SELECT sqlite_version()").fetchone()[0]
print(version)Контекст самой Connection управляет транзакцией, но не закрывает соединение, поэтому выше используется closing. sqlite3.Row позволяет читать столбцы по именам. Одно соединение по умолчанию нельзя использовать из другого потока; безопаснее выдавать отдельное соединение каждой операции или запросу.
Основные операции с данными в SQLite
Значения всегда передаются отдельно от SQL через placeholders ?. Это корректно экранирует значения и предотвращает SQL injection. Изменяющие операции объединяйте в транзакцию.
import sqlite3
from contextlib import closing
with closing(sqlite3.connect("app.db")) as connection:
connection.row_factory = sqlite3.Row
with connection:
connection.executemany(
"INSERT INTO users(name, age) VALUES (?, ?)",
[("Ada", 36), ("Grace", 40)],
)
rows = connection.execute(
"SELECT id, name, age FROM users WHERE age >= ? ORDER BY name",
(18,),
).fetchall()
for row in rows:
print(dict(row))
with connection:
connection.execute(
"UPDATE users SET age = ? WHERE name = ?",
(37, "Ada"),
)
connection.execute("DELETE FROM users WHERE name = ?", ("Grace",))Имя таблицы или столбца нельзя подставить через ?: динамические идентификаторы выбирают из заранее разрешённого набора. SQLite хорошо подходит для локальных инструментов и умеренной нагрузки, но запись сериализуется и требует продуманного concurrency.
Глава 2. PostgreSQL
Модуль psycopg. Подключение к серверу PostgreSQL
Современный пакет Psycopg 3 импортируется как psycopg. Вариант binary удобен для разработки; способ установки для production выбирают по официальной документации. Строку подключения хранят вне исходного кода.
uv add "psycopg[binary]"
export DATABASE_URL='postgresql://app:change-me@localhost:5432/appdb'import os
import psycopg
database_url = os.environ["DATABASE_URL"]
with psycopg.connect(database_url) as connection:
with connection.cursor() as cursor:
cursor.execute("SELECT current_database(), current_user")
print(cursor.fetchone())Контекст соединения подтверждает транзакцию при успехе, откатывает при исключении и закрывает соединение. Для web-приложения используют пул (psycopg_pool) с ограниченным числом подключений, а не новое неограниченное соединение на каждое действие.
Создание базы данных и таблицы в PostgreSQL
Базу часто создаёт администратор или инфраструктурный скрипт. В Linux/macOS локальный пользователь PostgreSQL может выполнить:
createdb appdbDDL для таблицы выполняется обычной транзакцией. IDENTITY — современный способ генерировать числовой первичный ключ.
import os
import psycopg
with psycopg.connect(os.environ["DATABASE_URL"]) as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
"""
)CREATE DATABASE нельзя выполнять внутри обычной транзакции, поэтому для этой операции используют createdb, psql или соединение с autocommit=True. Изменение схемы развёрнутого приложения оформляют версионированными миграциями.
Добавление данных
Psycopg использует placeholder %s для любого типа значения; не ставьте вокруг него кавычки. executemany добавляет несколько наборов, а RETURNING сразу возвращает созданные сервером поля.
import os
import psycopg
with psycopg.connect(os.environ["DATABASE_URL"]) as connection:
with connection.cursor() as cursor:
cursor.execute(
"""
INSERT INTO users(name, email)
VALUES (%s, %s)
RETURNING id, created_at
""",
("Ada", "ada@example.com"),
)
user_id, created_at = cursor.fetchone()
print(user_id, created_at)
cursor.executemany(
"INSERT INTO users(name, email) VALUES (%s, %s)",
[
("Grace", "grace@example.com"),
("Linus", "linus@example.com"),
],
)Весь блок находится в одной транзакции: если вторая вставка нарушит ограничение, изменения будут откачены. Для повторяемой команды полезны ON CONFLICT DO NOTHING или ON CONFLICT ... DO UPDATE, если такая семантика действительно нужна бизнес-операции.
Получение данных их БД PostgreSQL
После SELECT курсор можно итерировать или читать через fetchone, fetchmany, fetchall. Фабрика dict_row возвращает словари вместо позиционных кортежей. Ограничивайте выборку и выбирайте только необходимые столбцы.
import os
import psycopg
from psycopg.rows import dict_row
with psycopg.connect(os.environ["DATABASE_URL"], row_factory=dict_row) as connection:
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, name, email
FROM users
WHERE name ILIKE %s
ORDER BY id
LIMIT %s
""",
("a%", 50),
)
for row in cursor:
print(row["id"], row["email"])
cursor.execute("SELECT id, name FROM users WHERE id = %s", (1,))
user = cursor.fetchone() # dict или Nonefetchall может занять много памяти. Для больших результатов используют порции или server-side cursor; для API — пагинацию и стабильную сортировку.
Обновление и удаление данных
UPDATE и DELETE также параметризуются. RETURNING позволяет отличить успешное изменение от отсутствующей строки без отдельного предварительного SELECT, что уменьшает число запросов и окно гонки.
import os
import psycopg
with psycopg.connect(os.environ["DATABASE_URL"]) as connection:
with connection.cursor() as cursor:
cursor.execute(
"UPDATE users SET name = %s WHERE id = %s RETURNING id, name",
("Ada Lovelace", 1),
)
updated = cursor.fetchone()
if updated is None:
print("Пользователь не найден")
cursor.execute(
"DELETE FROM users WHERE email = %s RETURNING id",
("linus@example.com",),
)
deleted = cursor.fetchone()Не делайте UPDATE или DELETE без осознанного WHERE. Для конкурентных составных изменений выбирают подходящий уровень изоляции либо блокировку SELECT ... FOR UPDATE и обрабатывают повтор транзакции.
Глава 3. SQL ORM SQLAlchemy
Подключение к базе данных через SQLAlchemy
SQLAlchemy поддерживает Core и ORM поверх одного Engine. URL включает диалект и драйвер. echo=True полезен для обучения, но может раскрыть данные в логах, поэтому не включается бездумно в production.
uv add sqlalchemyfrom sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
engine = create_engine("sqlite:///app.db", echo=False)
SessionFactory = sessionmaker(bind=engine, expire_on_commit=False)
with engine.connect() as connection:
print(connection.scalar(text("SELECT 1")))
with SessionFactory() as session:
assert isinstance(session, Session)Engine создают один раз на процесс: внутри него работает пул соединений. Session короткоживущая, не потокобезопасна и не должна разделяться между одновременными запросами. Для PostgreSQL URL, например, имеет вид postgresql+psycopg://....
Определение моделей в SQLAlchemy
Declarative-модель связывает класс с таблицей. Аннотации Mapped[...] описывают Python-тип и nullability, mapped_column — детали столбца. Имя таблицы задаёт __tablename__.
from datetime import datetime
from sqlalchemy import DateTime, String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(320), unique=True, index=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
)# Фрагмент: engine создаётся как в предыдущей теме.
Base.metadata.create_all(engine)create_all создаёт отсутствующие таблицы, но не является системой миграций. Для развития схемы применяют Alembic и проверяют миграции отдельно от ORM-моделей.
Основные операции с данными в SQLAlchemy
Сессия отслеживает ORM-объекты и синхронизирует изменения при flush/commit. Стиль SQLAlchemy 2 использует select, update и delete, а не устаревший session.query.
# Фрагмент: использует User и SessionFactory из предыдущих тем.
from sqlalchemy import delete, select, update
with SessionFactory.begin() as session:
session.add_all(
[
User(name="Ada", email="ada@example.com"),
User(name="Grace", email="grace@example.com"),
]
)
with SessionFactory() as session:
users = session.scalars(
select(User).where(User.name.ilike("a%")).order_by(User.id)
).all()
one_user = session.get(User, 1)
with SessionFactory.begin() as session:
session.execute(
update(User).where(User.id == 1).values(name="Ada Lovelace")
)
session.execute(delete(User).where(User.email == "grace@example.com"))Session.begin() подтверждает транзакцию или откатывает её при исключении. Массовые update/delete не вызывают доменную логику объектов; когда важны инварианты, сначала загружайте сущность и изменяйте её явно.
Модели с отношением один-ко-многим
Связь «один пользователь — много задач» состоит из внешнего ключа на стороне Task и двух relationship. back_populates явно связывает обе стороны. Опции cascade определяют только поведение ORM и должны согласовываться с внешним ключом в БД.
from __future__ import annotations
from sqlalchemy import ForeignKey, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
tasks: Mapped[list[Task]] = relationship(
back_populates="owner",
cascade="all, delete-orphan",
)
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
owner: Mapped[User] = relationship(back_populates="tasks")# Фрагмент: использует настроенную SessionFactory.
from sqlalchemy import select
from sqlalchemy.orm import selectinload
with SessionFactory.begin() as session:
session.add(User(name="Ada", tasks=[Task(title="Write notes")]))
with SessionFactory() as session:
users = session.scalars(
select(User).options(selectinload(User.tasks))
).all()selectinload предотвращает N+1 запрос при переборе коллекций. В API не возвращайте ORM-граф без явной схемы: циклические отношения и ленивые загрузки могут породить лишние запросы или ошибки после закрытия сессии.
Руководство по сетевому программированию на Python
Модуль socket даёт низкоуровневый доступ к TCP и UDP, а http.server — учебную реализацию HTTP-сервера. TCP передаёт поток байтов, а не отдельные сообщения: границы сообщений, кодировку, таймауты и максимальный размер задаёт протокол приложения.
Глава 1. Клиент-серверные приложения
Сокеты. Создание клиента
TCP-клиент создаёт соединение, отправляет байты и читает ответ. send и recv могут обработать только часть данных: для отправки всего буфера используйте sendall, а для чтения сообщения — собственный framing. Ни один размер буфера не превращает один recv в «получить одно сообщение».
# client.py: протокол «4 байта длины + UTF-8 payload».
import socket
import struct
def recv_exact(sock: socket.socket, size: int) -> bytes:
chunks = bytearray()
while len(chunks) < size:
chunk = sock.recv(size - len(chunks))
if not chunk:
raise ConnectionError("Соединение закрыто до конца сообщения")
chunks.extend(chunk)
return bytes(chunks)
payload = "hello".encode("utf-8")
with socket.create_connection(("127.0.0.1", 9000), timeout=5) as client:
client.sendall(struct.pack("!I", len(payload)) + payload)
response_size = struct.unpack("!I", recv_exact(client, 4))[0]
if response_size > 1_000_000:
raise ValueError("Слишком большой ответ")
response = recv_exact(client, response_size)
print(response.decode("utf-8"))!I означает четырёхбайтовое беззнаковое число в сетевом порядке байтов. Таймаут не гарантирует успешное завершение, но не даёт зависнуть навсегда.
Сокеты. Создание сервера
TCP-сервер проходит цепочку socket → bind → listen → accept. accept возвращает новый сокет конкретного клиента; слушающий сокет остаётся принимать следующие соединения. SO_REUSEADDR облегчает быстрый перезапуск локального сервера.
# server.py: совместим с клиентом из предыдущей темы.
import socket
import struct
def recv_exact(sock: socket.socket, size: int) -> bytes:
data = bytearray()
while len(data) < size:
chunk = sock.recv(size - len(data))
if not chunk:
raise ConnectionError("Неполное сообщение")
data.extend(chunk)
return bytes(data)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 9000))
server.listen()
print("Listening on 127.0.0.1:9000")
connection, address = server.accept()
with connection:
connection.settimeout(10)
size = struct.unpack("!I", recv_exact(connection, 4))[0]
if size > 1_000_000:
raise ValueError("Слишком большое сообщение")
message = recv_exact(connection, size).decode("utf-8")
reply = message.upper().encode("utf-8")
connection.sendall(struct.pack("!I", len(reply)) + reply)Привязка к 127.0.0.1 принимает только локальные соединения. 0.0.0.0 публикует порт на всех интерфейсах и требует firewall, аутентификации и безопасного протокола.
Многопоточное клиент-серверное приложение
Последовательный сервер держит остальных клиентов в очереди, пока обслуживает одного. Для блокирующих сокетов можно передавать соединения ограниченному пулу потоков. Потоки делят память, поэтому общее изменяемое состояние защищают синхронизацией.
import socket
from concurrent.futures import ThreadPoolExecutor
def handle_client(connection: socket.socket, address: tuple[str, int]) -> None:
with connection:
connection.settimeout(10)
data = connection.recv(4096)
if data:
connection.sendall(data.upper())
print("served", address)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 9000))
server.listen(128)
with ThreadPoolExecutor(max_workers=20) as pool:
while True:
connection, address = server.accept()
pool.submit(handle_client, connection, address)Этот компактный сервер по-прежнему считает один recv одним запросом и годится только для короткой демонстрации. Настоящий протокол должен иметь framing, лимиты, обработку ошибок и штатную остановку. Для большого числа соединений часто используют asyncio или готовый серверный фреймворк.
Отправка файлов
Получатель должен знать длину файла или иной признак конца. В примере сервер сначала отправляет восьмибайтовый размер, затем содержимое; клиент читает ровно указанное число байтов. Нельзя считать, что recv(65536) вернёт полный файл.
# file_server.py
import socket
import struct
from pathlib import Path
path = Path("report.pdf")
size = path.stat().st_size
with socket.socket() as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 9001))
server.listen(1)
connection, _ = server.accept()
with connection, path.open("rb") as source:
connection.sendall(struct.pack("!Q", size))
while chunk := source.read(64 * 1024):
connection.sendall(chunk)# file_client.py
import socket
import struct
def recv_exact(sock: socket.socket, size: int) -> bytes:
data = bytearray()
while len(data) < size:
chunk = sock.recv(min(64 * 1024, size - len(data)))
if not chunk:
raise ConnectionError("Файл получен не полностью")
data.extend(chunk)
return bytes(data)
with socket.create_connection(("127.0.0.1", 9001), timeout=10) as client:
size = struct.unpack("!Q", recv_exact(client, 8))[0]
if size > 100 * 1024 * 1024:
raise ValueError("Файл превышает лимит")
with open("download.pdf", "wb") as target:
remaining = size
while remaining:
chunk = client.recv(min(64 * 1024, remaining))
if not chunk:
raise ConnectionError("Файл получен не полностью")
target.write(chunk)
remaining -= len(chunk)Для недоверенной сети добавляют аутентификацию, шифрование и проверку хеша. Протокол также должен определять имя, тип файла и поведение при прерывании передачи.
Глава 2. HTTP-сервера
Создание локального HTTP-сервера
ThreadingHTTPServer и BaseHTTPRequestHandler позволяют изучить устройство HTTP. Обработчик пишет статус, заголовки, пустую строку и тело. Этот сервер не заменяет production WSGI/ASGI-сервер: у него нет необходимых ограничений, middleware, наблюдаемости и полноценной защиты от враждебного трафика.
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
body = b'{"status":"ok"}'
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
server = ThreadingHTTPServer(("127.0.0.1", 8080), Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()python server.py
curl -i http://127.0.0.1:8080/Маршрутизация
self.path содержит и путь, и query string. Разберите его через urllib.parse, затем явно сопоставьте нормализованный путь с обработчиком. Для сложного приложения ручная цепочка условий быстро уступает готовому фреймворку.
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
class Handler(BaseHTTPRequestHandler):
def send_json(self, status: int, payload: object) -> None:
body = json.dumps(payload, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
url = urlsplit(self.path)
if url.path == "/":
self.send_json(200, {"page": "home"})
elif url.path == "/hello":
name = parse_qs(url.query).get("name", ["guest"])[0]
self.send_json(200, {"message": f"Hello, {name}"})
else:
self.send_json(404, {"error": "not found"})
ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()Не используйте строку URL как путь к файлу без безопасного разрешения: percent-encoding и .. могут обойти наивные проверки.
Отправка файлов
Серверу нужно определить MIME-тип, длину и безопасно открыть только разрешённый файл. Проверка после resolve() не даёт выйти из публичного каталога через ../.
import mimetypes
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlsplit
PUBLIC = Path("public").resolve()
class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
relative = unquote(urlsplit(self.path).path).lstrip("/")
path = (PUBLIC / relative).resolve()
if PUBLIC not in path.parents or not path.is_file():
self.send_error(404, "File not found")
return
content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(path.stat().st_size))
self.end_headers()
with path.open("rb") as source:
while chunk := source.read(64 * 1024):
self.wfile.write(chunk)
ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()Это учебный пример: он не реализует range requests, кеширование и оптимизированную передачу. Для публичной статики используйте специализированный HTTP-сервер или CDN.
Отправка html-страниц и создание веб-сайта
Небольшой статический сайт можно отдавать из каталога, подставляя index.html для /. HTML читается как байты: сервер не исполняет шаблоны и не экранирует пользовательские значения.
site/
├── index.html
└── about.html<!-- site/index.html -->
<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<title>Учебный сайт</title>
<h1>Главная</h1>
<a href="/about.html">О сайте</a>
</html>from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
handler = partial(SimpleHTTPRequestHandler, directory="site")
server = ThreadingHTTPServer(("127.0.0.1", 8080), handler)
try:
server.serve_forever()
finally:
server.server_close()SimpleHTTPRequestHandler удобен для локального просмотра сборки. Не запускайте его в каталоге с секретами и не публикуйте как production-сервер.
Обработка POST-запросов
Тело POST читается по Content-Length. До чтения проверьте тип и лимит, затем декодируйте формат. Следующий обработчик принимает небольшой JSON; для chunked transfer, форм, multipart и потоковой обработки нужен полноценный HTTP-фреймворк.
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
MAX_BODY = 64 * 1024
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
if self.path != "/echo":
self.send_error(404)
return
if self.headers.get_content_type() != "application/json":
self.send_error(415, "Expected application/json")
return
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
self.send_error(400, "Invalid Content-Length")
return
if not 0 < length <= MAX_BODY:
self.send_error(413, "Body is empty or too large")
return
try:
payload = json.loads(self.rfile.read(length))
except (UnicodeDecodeError, json.JSONDecodeError):
self.send_error(400, "Invalid JSON")
return
body = json.dumps({"received": payload}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever()curl -i -X POST http://127.0.0.1:8080/echo \
-H 'Content-Type: application/json' \
-d '{"message":"hello"}'Создание локального HTTPS-сервера
TLS оборачивает TCP-соединение, шифрует трафик и подтверждает имя сервера сертификатом. Для локального эксперимента можно создать самоподписанный сертификат; браузер не доверяет ему автоматически. Команда ниже предназначена только для Linux/macOS и локальной разработки.
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout key.pem -out cert.pem -days 7 \
-subj '/CN=localhost' \
-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1'import ssl
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
server = ThreadingHTTPServer(("127.0.0.1", 8443), SimpleHTTPRequestHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")
server.socket = context.wrap_socket(server.socket, server_side=True)
try:
server.serve_forever()
finally:
server.server_close()curl --cacert cert.pem https://localhost:8443/Не отключайте проверку сертификата в настоящем клиенте. В production сертификаты выпускает доверенный центр, закрытый ключ хранится с ограниченными правами, TLS часто завершается на reverse proxy, а приложение запускается готовым WSGI/ASGI-сервером.