Python для начинающих
1.24K subscribers
546 photos
3 videos
232 files
74 links
Python для начинающих
Download Telegram
Как использовать set для удаления дубликатов из списка
Как использовать set для удаления дубликатов из списка

Одна из самых частых задач в Python — избавиться от дубликатов в списке. Можно писать циклы, проверять, есть ли элемент в новом списке… а можно использовать встроенный тип set и сделать всё в одну строку.

### Что такое set?

set — это множество: неупорядоченная коллекция уникальных элементов.
Ключевые свойства:

- в set не бывает дубликатов;
- элементы должны быть хешируемыми (числа, строки, кортежи и т.д.);
- порядок не гарантируется.

Создать множество из списка можно так:

numbers = [1, 2, 2, 3, 3, 3]
unique_numbers = set(numbers)
print(unique_numbers) # {1, 2, 3}


Мы просто «заставили» Python выкинуть всё лишнее.

### Возврат к списку без дубликатов

Часто нужен именно список (с индексами и возможностью изменять элементы). Тогда:

numbers = [1, 2, 2, 3, 3, 3]
unique_list = list(set(numbers))
print(unique_list) # порядок не гарантируется


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

### Как сохранить порядок и убрать дубликаты?

Комбинируем set и цикл:

items = ["a", "b", "a", "c", "b", "d"]
seen = set()
result = []

for item in items:
if item not in seen:
seen.add(item)
result.append(item)

print(result) # ['a', 'b', 'c', 'd']


Здесь:

- seen хранит уже встреченные элементы;
- result — итоговый список без повторов, в исходном порядке.

### Короткая версия через dict.fromkeys

Если важен порядок, но не хочется писать цикл:

items = ["a", "b", "a", "c", "b", "d"]
result = list(dict.fromkeys(items))
print(result) # ['a', 'b', 'c', 'd']


До Python 3.7 порядок ключей в dict официально не гарантировался, но в современных версиях этот трюк работает надёжно.

### Где это полезно?

- очистка списков e-mail адресов от повторов;
- фильтрация тегов;
- удаление дубликатов из данных перед анализом.

Главное: set — быстрый и простой способ получить уникальные значения. Используйте set, когда порядок не важен, и сочетайте его с дополнительной логикой, когда порядок нужно сохранить.
👍3🔥1
Создание простых Webhook-обработчиков с Flask
Создание простых Webhook-обработчиков с Flask

Иногда нужно, чтобы ваш скрипт реагировал на внешние события: оплата на сайте, новый комментарий в соцсети, пуш из GitHub и т.п. Сервисы решают это через webhooks — они отправляют HTTP-запросы (обычно POST) на ваш URL. Ваша задача — принять запрос и что-то сделать.

Самый простой способ в Python — мини-фреймворк Flask.

---

### Минимальный Webhook на Flask

Установим Flask:

pip install flask


Создадим app.py:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
def webhook_handler():
data = request.get_json(silent=True) or {}
event_type = data.get("event", "unknown")

# Примитивная обработка события
if event_type == "payment_succeeded":
# здесь могла бы быть запись в базу или отправка email
print("Payment succeeded:", data)
else:
print("Unknown event:", data)

return jsonify({"status": "ok"}), 200

if __name__ == "__main__":
app.run(port=5000, debug=True)


Что тут важно:

- methods=["POST"] — принимаем только POST (классический формат webhook’ов).
- request.get_json() — достаем JSON-тело запроса.
- Возвращаем JSON-ответ и HTTP-код 200, чтобы внешний сервис понял, что все прошло успешно.

---

### Как протестировать без реального сервиса

Можно «прикинуться» внешним сервисом с помощью curl:

curl -X POST http://127.0.0.1:5000/webhook \
-H "Content-Type: application/json" \
-d '{"event":"payment_succeeded","amount":1500}'


В консоли сервера вы увидите распечатанный JSON.

---

### Добавляем простую проверку подписи

Реальные сервисы почти всегда подписывают запросы, чтобы никто не мог отправить фальшивый webhook. Простейший вариант — общий секрет и заголовок с HMAC:

import hmac
import hashlib
from flask import abort

SECRET_TOKEN = "super-secret-key"

def verify_signature(raw_body: bytes, received_signature: str) -> bool:
mac = hmac.new(SECRET_TOKEN.encode(), msg=raw_body, digestmod=hashlib.sha256)
expected_sig = mac.hexdigest()
return hmac.compare_digest(expected_sig, received_signature or "")

@app.route("/secure-webhook", methods=["POST"])
def secure_webhook():
raw_body = request.data
signature = request.headers.get("X-Signature")

if not verify_signature(raw_body, signature):
abort(401, description="Invalid signature")

payload = request.get_json(silent=True) or {}
print("Secure event:", payload)
return jsonify({"status": "accepted"}), 200


Ключевые моменты:

- Используем request.data, а не уже разобранный JSON — подпись считается по «сырому» телу.
- hmac.compare_digest защищает от атак по времени сравнения.
- При неверной подписи сразу возвращаем 401.

---

### Зачем это все начинающему?

Webhook-обработчик — это реальная задачка уровня «боевого» кода:

- вы работаете с HTTP, JSON, заголовками и кодами ответа;
- думаете о безопасности (подписи, проверка метода);
- учитесь разделять «прием запроса» и «бизнес-логику».

А Flask позволяет уложить все это в несколько десятков строк понятного кода — отличный старт для вхождения в мир веб-разработки на Python.
👍4
Работа с emoji и вывод цветных иконок в консоль Python
Работа с emoji и цветными иконками в консоли Python

Консоль не обязана быть скучной. Немного emoji и цвета — и ваш скрипт начинает «разговаривать» с пользователем гораздо живее. Разберём три простых приёма: Unicode-emoji, библиотеку emoji и цветной вывод через colorama.

---

## 1. Emoji по‑простому: через Unicode

Многие emoji — это обычные Unicode-символы. Достаточно вставить их прямо в строку:

print("Task completed ")
print("Warning ⚠️")
print("Python is fun 🐍")


Если консоль и шрифт поддерживают Unicode, всё просто заработает.
Иногда полезно использовать кодовую точку:

smile = "\U0001F600"  # 😀
print(f"Hello {smile}")


---

## 2. Библиотека emoji

Когда хочется писать :smile: вместо поиска нужного символа.

Установка:

pip install emoji


Использование:

import emoji

text = emoji.emojize(
"Server status: :green_circle: Running, :red_circle: Stopped",
language="alias"
)
print(text)


Полезные фишки:
- emoji.demojize("Hello 😀")"Hello :grinning_face:"
- Можно хранить «коды» в файлах настроек и подставлять emoji при выводе.

---

## 3. Цветной текст с colorama

colorama кроссплатформенно добавляет цвет в консоль (особенно удобно на Windows).

Установка:

pip install colorama


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

from colorama import init, Fore, Style

init(autoreset=True)

print(Fore.GREEN + "Success ")
print(Fore.YELLOW + "Loading... ")
print(Fore.RED + "Error ")
print(Style.DIM + "This is a low-priority message")


Ключевые элементы:
- Fore.RED, Fore.GREEN, Fore.YELLOW и др. — цвет текста.
- Style.BRIGHT, Style.DIM, Style.NORMAL — «стиль» текста.
- autoreset=True избавляет от необходимости вручную сбрасывать цвет.

---

## 4. Комбинируем: мини‑панель статуса

from colorama import init, Fore
import emoji
import time

init(autoreset=True)

def show_status(message, ok=True):
icon = ":check_mark_button:" if ok else ":cross_mark:"
icon = emoji.emojize(icon, language="alias")
color = Fore.GREEN if ok else Fore.RED
print(color + icon, message)

show_status("Connecting to database...")
time.sleep(1)
show_status("Connected", ok=True)
show_status("Failed to fetch data", ok=False)


Итог: пара библиотек + Unicode — и ваша консоль превращается из серого лога в удобную и наглядную панель. Это особенно выручает в CLI‑утилитах, мини‑играх и учебных проектах, где хочется мгновенно различать успех, ошибку и предупреждения.
👍3
Как использовать itertools для нового взгляда на циклы и генераторы
Как использовать itertools: новый взгляд на циклы и генераторы

Если обычные циклы for — это отвертка, то модуль itertools — целый чемодан инструментов. Он позволяет писать короче, читать легче и иногда вообще обходиться без явных циклов.

---

## Бесконечные последовательности без боли

from itertools import count, cycle, repeat

for i in count(10, 2): # 10, 12, 14, ...
if i > 20:
break
print(i)


count(start, step) создает бесконечный счетчик. Безопасность — на вас: обязательно ставьте break.

colors = ["red", "green", "blue"]
for c, n in zip(cycle(colors), range(5)):
print(c)


cycle(iterable) зацикливает список. Удобно для повторяющихся паттернов.

for x in repeat("hello", 3):
print(x)


repeat(obj, times) — ленивый клон объекта.

---

## Перебор сочетаний, перестановок и не только

from itertools import permutations, combinations, product

letters = ["a", "b", "c"]

print(list(permutations(letters, 2))) # Порядок важен
print(list(combinations(letters, 2))) # Порядок не важен


Такие функции идеально подходят для перебора вариантов в брутфорс-задачах, тестировании, комбинаторике.

Декартово произведение:

sizes = ["S", "M"]
colors = ["black", "white"]

for combo in product(sizes, colors):
print(combo)


Это мощнее вложенных циклов и проще читается.

---

## Умная фильтрация и группировка

from itertools import compress

data = [10, 20, 30, 40]
selectors = [True, False, True, False]

print(list(compress(data, selectors))) # [10, 30]


compress фильтрует элементы по маске логических значений.

Группировка подряд идущих одинаковых элементов:

from itertools import groupby

data = "aaabbccccd"

for char, group in groupby(data):
print(char, len(list(group)))
# a 3, b 2, c 4, d 1


Полезно для простого сжатия или анализа последовательностей.

---

## Комбинирование итераторов как конструктор

from itertools import chain, islice

data1 = [1, 2]
data2 = [3, 4, 5]

merged = chain(data1, data2) # ленивое "склеивание"
print(list(islice(merged, 0, 4))) # берём только первые 4


chain объединяет несколько источников, а islice позволяет "отрезать" нужный кусок без создания лишних списков.

---

itertools — про ленивость и композицию. Вместо того чтобы писать сложные циклы, вы собираете поведение из маленьких строительных блоков. Результат — короче код, меньше ошибок и приятное ощущение "Python-стиля".
👍3
Работа с collections: полезные структуры данных и где их использовать
Работа с collections: полезные структуры данных и где их использовать

Стандартные списки и словари — отличная вещь, но модуль collections превращает их в инструменты «профессионального уровня». Разберём ключевые структуры, которые реально упрощают код.

---

### Counter: считаем всё подряд

Counter — словарь для подсчёта объектов.

from collections import Counter

text = "banana bandana"
counter = Counter(text)

print(counter) # сколько раз встречается каждый символ
print(counter.most_common(2)) # два самых частых


Где полезно: подсчёт частоты слов в тексте, статистика действий пользователей, анализ логов.

---

### defaultdict: словарь, который не ругается на новые ключи

Обычный словарь бросит KeyError, если ключа нет. defaultdict автоматически создаёт значение по умолчанию.

from collections import defaultdict

groups = defaultdict(list)

data = [("cat", 1), ("dog", 2), ("cat", 3)]
for animal, value in data:
groups[animal].append(value)

print(groups) # {'cat': [1, 3], 'dog': [2]}


Где полезно: группировка данных, инвертирование словарей, построение графов (списки соседей).

---

### namedtuple: читаемые «кортежи с именами»

Обычный кортеж непонятен: user[0], user[1]namedtuple даёт имена полям.

from collections import namedtuple

User = namedtuple("User", ["name", "age"])
user = User(name="Alice", age=30)

print(user.name)
print(user.age)


Где полезно: лёгкие «объекты без классов» — координаты, настройки, параметры функций. Иммутабельность помогает избежать случайных изменений.

---

### deque: очередь и стек без тормозов

Список плохо работает с pop(0) — это O(n). deque оптимизирован для добавления/удаления с обоих концов.

from collections import deque

queue = deque()

queue.append("task1")
queue.append("task2")
queue.appendleft("urgent")

print(queue.popleft()) # 'urgent'
print(queue.pop()) # 'task2'


Где полезно: очереди задач, реализовать стек/очередь, «скользящее окно» последних N элементов.

---

### OrderedDict: порядок тоже важен

С Python 3.7 обычный dict уже запоминает порядок вставки, но OrderedDict всё ещё полезен там, где нужны дополнительные операции (например, move_to_end). В современных проектах используют реже, но стоит знать о его существовании.

---

Модуль collections — это набор готовых решений для типичных задач работы с данными. Зная эти структуры, вы часто пишете меньше кода, делаете его быстрее и понятнее — и меньше изобретаете велосипеды.
👍4
Создание генераторов: экономим память при обработке больших данных
Создание генераторов: экономим память при обработке больших данных

Представьте, что вам нужно обработать файл на 10 ГБ, посчитать суммы, отфильтровать строки, что‑то преобразовать. Если пытаться «затащить» всё в память списками — привет, MemoryError. Здесь на сцену выходят генераторы.

Генератор — это «ленивый» источник данных: он отдаёт элементы по одному, по запросу, не храня весь результат сразу. Именно поэтому генераторы так хорошо подходят для больших данных и потоковой обработки.

### Генераторные выражения

Список:

nums = [i * 2 for i in range(10_000_000)]


держит в памяти все 10 млн элементов.

Генератор:

nums_gen = (i * 2 for i in range(10_000_000))


хранит только текущее состояние итерации. Память почти не растёт, пока вы не начинаете обходить nums_gen:

total = 0
for value in nums_gen:
total += value


### Собственные генераторные функции

Ключевое слово yield превращает функцию в генератор:

def read_large_file(path):
with open(path, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()


Здесь не создаётся список строк файла — каждая строка обрабатывается по мере чтения:

def iter_error_lines(path):
for line in read_large_file(path):
if 'ERROR' in line:
yield line

for err_line in iter_error_lines('app.log'):
print(err_line)


Файл может быть гигантским — программа использует почти столько же памяти, как и при чтении маленького файла.

### Конвейер из генераторов

Самое интересное начинается, когда вы соединяете генераторы в цепочку — получается «конвейер»:

def iter_numbers(path):
for line in read_large_file(path):
if line and line[0].isdigit():
yield int(line)

def filter_even(seq):
for n in seq:
if n % 2 == 0:
yield n

def sum_limited(seq, limit):
total = 0
for n in seq:
total += n
if total > limit:
break
return total

numbers = iter_numbers('numbers.txt')
even_numbers = filter_even(numbers)
result = sum_limited(even_numbers, limit=1_000_000)
print(result)


Каждый шаг обрабатывает элементы по одному: прочитали строку → превратили в число → отфильтровали → учли в сумме. Никаких огромных временных списков.

### Когда использовать генераторы

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

Генераторы — это простой способ сделать код и аккуратнее, и экономичнее по памяти, не усложняя архитектуру. Если вы уже пишете списковые включения, вы почти готовы использовать генераторы — достаточно заменить [] на ().
👍1🔥1
Как применять enum для улучшения читаемости кода
Python для начинающих: как enum делает код понятнее

Когда в коде появляются «магические числа» и странные строки, читаемость падает. Встречали что‑то вроде:

if status == 3:
send_email()


Через неделю уже непонятно, что такое 3. Ошибка не в логике — ошибка в обозначениях. Здесь на сцену выходит модуль enum.

---

## Что такое Enum?

Enum (перечисление) — это способ задать набор именованных констант. Вместо чисел и строк, разбросанных по коду, вы используете говорящие имена.

from enum import Enum

class OrderStatus(Enum):
NEW = 1
PAID = 2
SHIPPED = 3
CANCELED = 4


Теперь вместо 3 у вас есть OrderStatus.SHIPPED. Код сразу объясняет сам себя.

---

## Пример: читаемый if

До:

def handle_order(status):
if status == 1:
print("Create invoice")
elif status == 2:
print("Prepare shipment")
elif status == 3:
print("Send tracking code")


После:

from enum import Enum

class OrderStatus(Enum):
NEW = 1
PAID = 2
SHIPPED = 3

def handle_order(status: OrderStatus):
if status is OrderStatus.NEW:
print("Create invoice")
elif status is OrderStatus.PAID:
print("Prepare shipment")
elif status is OrderStatus.SHIPPED:
print("Send tracking code")


Что улучшилось:

- код сам документирует возможные статусы;
- меньше шансов перепутать значения;
- IDE подсказывает варианты (OrderStatus. → список).

---

## Enum вместо строк

Частая ловушка — сравнение со строками:

if role == "admin":
...


Опечатка — и проверка ломается. С Enum:

from enum import Enum, auto

class UserRole(Enum):
ADMIN = auto()
EDITOR = auto()
VIEWER = auto()

def can_delete(user_role: UserRole) -> bool:
return user_role is UserRole.ADMIN


auto() сам проставит уникальные значения — нас интересуют не числа, а имена.

---

## Небольшой бонус: словари с Enum

Enum удобно использовать как ключи:

from enum import Enum, auto

class LogLevel(Enum):
DEBUG = auto()
INFO = auto()
ERROR = auto()

LOG_PREFIXES = {
LogLevel.DEBUG: "[DEBUG]",
LogLevel.INFO: "[INFO]",
LogLevel.ERROR: "[ERROR]",
}

def log(level: LogLevel, message: str) -> None:
prefix = LOG_PREFIXES[level]
print(prefix, message)


Здесь невозможно случайно обратиться к "DBG" вместо "DEBUG" — только валидные значения перечисления.

---

Enum — это маленькое улучшение, которое сильно поднимает читаемость и надежность кода. Как только в вашем проекте появляется набор фиксированных состояний, ролей, типов — это сигнал: пора завести перечисление.
👍2
Подключение и использование конфигурационных файлов с configparser
Python для начинающих: configparser — приручаем конфиги как профи

Жёстко прописывать настройки прямо в коде — плохая идея. Захочется сменить пароль к базе или лог‑уровень — и вы уже лазите по файлам Python, рискуя всё сломать. Гораздо удобнее хранить настройки в конфигурационных файлах. Для этого в стандартной библиотеке есть модуль configparser.

---

### Как выглядит конфиг

Создадим файл settings.ini:

[database]
host = localhost
port = 5432
user = admin
password = secret

[logging]
level = DEBUG
file = app.log


Структура проста: секции в квадратных скобках и пары ключ = значение.

---

### Чтение конфигурации

from configparser import ConfigParser

config = ConfigParser()
config.read("settings.ini")

db_host = config["database"]["host"]
db_port = config.getint("database", "port")
log_level = config.get("logging", "level", fallback="INFO")

print(db_host, db_port, log_level)


Ключевые моменты:

- config["section"]["option"] — базовый доступ.
- getint, getfloat, getboolean — сразу приводят к нужному типу.
- fallback спасает, если ключа нет.

---

### Изменение и сохранение конфига

from configparser import ConfigParser

config = ConfigParser()
config.read("settings.ini")

config["database"]["host"] = "db.mycompany.com"

if "feature_flags" not in config:
config["feature_flags"] = {}
config["feature_flags"]["new_ui"] = "true"

with open("settings.ini", "w") as configfile:
config.write(configfile)


Так можно не только читать, но и аккуратно обновлять настройки.

---

### Конфиг по умолчанию

Иногда нужно задать значения «на всякий случай»:

from configparser import ConfigParser

config = ConfigParser(
defaults={
"level": "INFO",
"file": "app.log"
}
)

config.read("settings.ini")

log_level = config["logging"]["level"] # если нет в секции, возьмёт из defaults
log_file = config["logging"]["file"]


---

### Когда это полезно

- разные настройки для dev / prod без изменения кода;
- хранение секретов вне репозитория (через отдельный local_settings.ini);
- быстрое переключение фич через флаги.

configparser — простой, но мощный способ отделить код от настроек. Один раз настроили структуру .ini — и дальше ваш код становится гораздо чище и гибче.
👍1
Использование pathlib для работы с путями и файлами
Python для начинающих: как pathlib спасет вас от хаоса с путями

Если вы хоть раз писали что-то вроде:

file_path = "C:\\Users\\user\\projects\\data\\file.txt"


и ловили ошибки из‑за слэшей и кодировок, то модуль pathlib — ваш новый лучший друг. Он превращает работу с путями и файлами в аккуратные объектные операции, вместо бесконечной возни со строками.

---

### Базовая идея: Path вместо строк

Главный герой — класс Path:

from pathlib import Path

base_dir = Path.home() / "projects" / "demo"
print(base_dir)


Оператор / здесь не делит, а красиво склеивает части пути, независимо от ОС (Windows, Linux, macOS). Никаких os.path.join, никаких ручных слэшей.

---

### Проверка существования и типа

pathlib сразу умеет отвечать на важные вопросы:

from pathlib import Path

path = Path("data/example.txt")

print(path.exists()) # файл или папка есть?
print(path.is_file()) # это файл?
print(path.is_dir()) # это папка?


Можно безопасно проверять перед чтением или удалением.

---

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

Чтение и запись — это методы объекта пути:

from pathlib import Path

file_path = Path("data/notes.txt")

# запись текста
file_path.write_text("Hello, pathlib!", encoding="utf-8")

# чтение текста
content = file_path.read_text(encoding="utf-8")
print(content)


Не нужно вручную открывать и закрывать файлы — Path делает это за вас.

---

### Создание папок и обход директорий

Создать дерево каталогов и пройтись по файлам — одна строка:

from pathlib import Path

base = Path("logs/app")

# создаем все недостающие каталоги
base.mkdir(parents=True, exist_ok=True)

# ищем все .log файлы в подпапках
for log_file in base.rglob("*.log"):
print(log_file.name, log_file.stat().st_size, "bytes")


Метод rglob рекурсивно ищет файлы по шаблону, stat() дает информацию о файле.

---

### Работа с именами, расширениями и родителями

Разбирать путь по частям стало легко:

from pathlib import Path

path = Path("data/archive/report_2024.csv")

print(path.name) # report_2024.csv
print(path.stem) # report_2024
print(path.suffix) # .csv
print(path.parent) # data/archive

new_path = path.with_suffix(".xlsx")
print(new_path) # data/archive/report_2024.xlsx


---

### Почему стоит перейти на pathlib уже сейчас

- Кроссплатформенно: один и тот же код под Windows и Linux.
- Читаемо: Path‑объекты ведут себя как реальные объекты, а не как странные строки.
- Богатый функционал: от обхода директорий до смены расширений.

Если вы еще работаете с путями как со строками — попробуйте переписать небольшой скрипт на pathlib. Скорее всего, назад уже не захочется.
🔥2
Создание простых HTTP-запросов вручную с модулем http.client
Создание простых HTTP-запросов вручную с модулем http.client

Большинство новичков начинают работу с сетью через библиотеку requests. Она удобная и «магическая». Но если хочется понять, что реально происходит под капотом, полезно познакомиться с модулем стандартной библиотеки http.client.

http.client работает на более низком уровне: вы сами открываете соединение, отправляете строку запроса, заголовки, читаете ответ. Немного «олдскул», зато отлично прокачивает понимание HTTP.

---

### Простой GET-запрос

Сделаем обычный GET-запрос к публичному API:

import http.client
import json

conn = http.client.HTTPSConnection("api.github.com")

headers = {
"User-Agent": "python-http.client-demo",
"Accept": "application/vnd.github.v3+json"
}

conn.request("GET", "/repos/python/cpython", headers=headers)
response = conn.getresponse()

print("Status:", response.status, response.reason)

data = response.read()
payload = json.loads(data)

print("Full name:", payload["full_name"])
print("Stars:", payload["stargazers_count"])

conn.close()


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

- HTTPSConnection — шифрованное соединение (TLS).
- request(method, url, body=None, headers={}) — отправка запроса.
- getresponse() — возвращает объект ответа.
- status, reason, read() — статус-код, текстовое описание и тело ответа.

Без обязательного заголовка User-Agent GitHub может ответить ошибкой — это хороший пример, почему заголовки критичны.

---

### Отправка POST-запроса с данными

Теперь отправим POST с JSON-данными на тестовый сервис httpbin.org:

import http.client
import json

conn = http.client.HTTPSConnection("httpbin.org")

payload = {"name": "Alice", "lang": "Python"}
body = json.dumps(payload)

headers = {
"Content-Type": "application/json",
"Content-Length": str(len(body))
}

conn.request("POST", "/post", body=body, headers=headers)
response = conn.getresponse()

print("Status:", response.status)
response_data = response.read().decode("utf-8")

print("Response body:", response_data[:200], "...")
conn.close()


Здесь уже видно «ручную работу»:

- Сериализуем данные сами (json.dumps).
- Указываем Content-Type и Content-Length вручную.
- Сами кодируем/декодируем текст (decode("utf-8")).

---

### Зачем это нужно?

- Понимание того, как устроен HTTP «на проводе».
- Возможность тонко контролировать заголовки и поведение соединения.
- Умение работать только со стандартной библиотекой, без внешних зависимостей.

После таких упражнений любые высокоуровневые библиотеки кажутся гораздо понятнее: вы уже знаете, какую именно «рутину» они берут на себя.
👍2