Python для начинающих
1.24K subscribers
546 photos
3 videos
232 files
74 links
Python для начинающих
Download Telegram
Создание локального веб-сервера с помощью встроенного http.server
Создание локального веб-сервера с помощью встроенного http.server

Когда слышишь «веб‑сервер», в голове всплывают сложные штуки вроде Nginx, Apache или хотя бы Flask. Но в Python уже есть крошечный сервер «из коробки» — модуль http.server. Он идеален для экспериментов, отладки и быстрых демо.

---

### Запускаем самый простой сервер

Перейдите в папку с файлами, которые хотите раздавать, и запустите:

python -m http.server 8000


Теперь в браузере откройте http://localhost:8000 — вы уже подняли сервер, который раздает текущую директорию.
Порт можно не указывать, тогда по умолчанию будет 8000.

---

### Минимальный сервер своим скриптом

Можно сделать то же самое из Python‑файла:

from http.server import HTTPServer, SimpleHTTPRequestHandler

def run_server(port: int = 8000):
server_address = ("", port) # "" означает слушать на всех интерфейсах
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
print(f"Serving on port {port}...")
httpd.serve_forever()

if __name__ == "__main__":
run_server()


SimpleHTTPRequestHandler умеет раздавать файлы из текущей папки и показывает простую HTML‑страницу со списком файлов.

---

### Свой обработчик запросов

Хотите кастомный ответ вместо списка файлов? Наследуемся от BaseHTTPRequestHandler:

from http.server import HTTPServer, BaseHTTPRequestHandler

class HelloHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/":
message = "Hello, Python beginner!"
else:
message = f"Unknown path: {self.path}"

data = message.encode("utf-8")

self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()

self.wfile.write(data)

def run_server(port: int = 8080):
server_address = ("", port)
httpd = HTTPServer(server_address, HelloHandler)
print(f"Custom server on port {port}...")
httpd.serve_forever()

if __name__ == "__main__":
run_server()


Теперь при открытии http://localhost:8080/ вы получите текстовый ответ, а при переходе по другому пути — сообщение с этим путем.

---

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

- Быстро показать HTML/CSS/JS без настройки больших серверов.
- Локально протестировать AJAX‑запросы.
- Написать минимальный API‑мок для отладки клиента.

http.server не для продакшена: нет защиты, логики масштабирования, нормального логирования. Но для обучения он идеально показывает, как HTTP‑запрос превращается в ответ, и помогает почувствовать «скелет» любого веб‑фреймворка.
👍2
Работа с буфером ввода-вывода с помощью io.StringIO
### Работа с буфером ввода-вывода с помощью io.StringIO

Иногда хочется поработать с текстом «как с файлом», но без создания настоящего файла на диске. Для этого в Python есть удобный инструмент — io.StringIO. Это такой «файлик в памяти»: его можно читать, писать в него, перематывать курсор — почти как с обычным файлом.

---

### Зачем вообще нужен StringIO?

Типичные сценарии:

1. Тестирование кода, который работает с файлами, без создания временных файлов.
2. Промежуточная обработка строк в виде потока: удобно, когда данные приходят частями.
3. Перенаправление вывода (например, print) в строку, чтобы потом её обработать.

---

### Базовый пример: пишем и читаем

from io import StringIO

buffer = StringIO()

buffer.write("Hello, world!\n")
buffer.write("This is in-memory file.\n")

# Перемещаем курсор в начало, как после открытия файла для чтения
buffer.seek(0)

content = buffer.read()
print(content)


Здесь buffer ведет себя почти как файл, но все хранится в оперативной памяти в виде строки.

---

### Построчное чтение

from io import StringIO

data = "line 1\nline 2\nline 3\n"
buffer = StringIO(data)

for line in buffer:
print(line.strip())


Интерфейс знакомый: можно итерироваться по строкам, использовать readline(), readlines() и т.д.

---

### Перенаправление print в строку

Иногда нужно собрать то, что обычно выводится в консоль:

from io import StringIO
import sys

buffer = StringIO()
stdout_backup = sys.stdout

try:
sys.stdout = buffer
print("First line")
print("Second line")
finally:
sys.stdout = stdout_backup

result = buffer.getvalue()
print("Captured:")
print(result)


buffer.getvalue() возвращает всю накопленную строку. Удобно для логирования и тестов.

---

### Важные нюансы

- StringIO работает только с текстом (str). Для байтов используйте io.BytesIO.
- Метод getvalue() можно вызывать сколько угодно раз — он не сдвигает курсор.
- Не забывайте про seek(0), если хотите перечитать содержимое с начала.

io.StringIO — отличный способ тренироваться с файловым API, не засоряя диск, и гибкий инструмент для чистого и удобного кода при работе с текстовыми потоками.
👍1🔥1
Генерация диаграмм с pygraphviz: основы визуализации графов
Генерация диаграмм с pygraphviz: основы визуализации графов

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

---

### Установка

pip install pygraphviz


Важно: на некоторых системах сначала нужно установить сам Graphviz (через пакетный менеджер ОС).

---

### Простейший граф

Создадим ориентированный граф и сохраним его как PNG:

from pygraphviz import AGraph

g = AGraph(directed=True)

g.add_node("User")
g.add_node("API")
g.add_node("DB")

g.add_edge("User", "API")
g.add_edge("API", "DB")

g.layout(prog="dot") # алгоритм раскладки
g.draw("simple_graph.png")


prog="dot" — классический и самый читаемый для иерархических структур (запросы сверху, база снизу).

---

### Стайлинг: делаем диаграмму понятной

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

from pygraphviz import AGraph

g = AGraph(directed=True, strict=True, rankdir="LR") # слева направо

g.add_node("Client", shape="box", style="filled", fillcolor="#AED6F1")
g.add_node("Service", shape="ellipse", style="filled", fillcolor="#A9DFBF")
g.add_node("Cache", shape="diamond", style="filled", fillcolor="#F9E79F")

g.add_edge("Client", "Service", label="HTTP")
g.add_edge("Service", "Cache", label="GET")
g.add_edge("Cache", "Service", label="HIT", color="green")
g.add_edge("Service", "Client", label="Response", color="blue")

g.graph_attr.update(label="Request Flow", fontsize="20")
g.layout(prog="dot")
g.draw("styled_graph.png")


Ключевые идеи:
- rankdir="LR" — направление слева направо (удобно для потоков).
- shape, fillcolor, style — визуальное кодирование типов узлов.
- label и color у ребер помогают понимать протоколы, типы взаимодействия и т.п.

---

### Быстрая визуализация структур из кода

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

---

pygraphviz хорош тем, что избавляет от ручного рисования схем в редакторах: диаграмма становится частью кода, обновляется автоматически и всегда соответствует реальности. Для начинающего питониста это отличный инструмент, чтобы увидеть свои структуры данных и архитектуру, а не только представлять их в голове.
👍1
Использование shutil для копирования и удаления файлов и папок
Используем shutil: копируем и удаляем файлы без боли

Работа с файлами в Python часто начинается с модуля os, но как только дело доходит до копирования и целых папок — на сцену выходит shutil. Это такой «швейцарский нож» для файловой системы.

Подключается просто:

import shutil
from pathlib import Path


## Копирование файлов

Базовый вариант — shutil.copy():

src = Path("data/source.txt")
dst = Path("backup/source_copy.txt")

shutil.copy(src, dst)


copy переносит содержимое и права доступа, но не метаданные (например, время изменения).

Если нужны максимально точные «клоны» файла, есть copy2:

shutil.copy2(src, dst)


Разница — в сохранении метаданных (atime, mtime и т.п.). Для бэкапов это часто критично.

## Копирование папок

Для директорий используется copytree:

src_dir = Path("data")
dst_dir = Path("data_backup")

shutil.copytree(src_dir, dst_dir)


Важно: если data_backup уже существует, будет ошибка. В Python 3.8+ можно указать dirs_exist_ok=True:

shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True)


Так можно «обновлять» уже существующую папку бэкапа.

## Удаление файлов и папок

Одиночные файлы удобнее удалять через Path.unlink(), а вот для целых деревьев директорий есть rmtree:

target_dir = Path("old_logs")

shutil.rmtree(target_dir)


Команда безвозвратная: корзины не будет, поэтому перед вызовом стоит дважды проверить путь или добавить «защиту от дурака»:

if "backup" in str(target_dir):
shutil.rmtree(target_dir)


## Мини-утилита «скопировать и почистить»

Соберём всё вместе: скопируем папку и удалим старую:

def move_folder(src: Path, dst: Path):
shutil.copytree(src, dst, dirs_exist_ok=True)
shutil.rmtree(src)

move_folder(Path("tmp_uploads"), Path("storage/uploads"))


Фактически мы реализовали аналог «перемещения» папки силами shutil.

---

shutil хорош тем, что закрывает 80% задач по управлению файлами: копирование, рекурсивные операции, бэкапы, чистка временных директорий. А главное — код остаётся коротким и читаемым, без ручного обхода каталогов и велосипедов на os.listdir().
🔥2👍1
Создание фейковых данных для тестирования с библиотекой faker
Создание фейковых данных для тестирования с библиотекой Faker

Тестовые данные — боль любого начинающего разработчика. Ручное заполнение таблиц “Иван Иванов, test@test.ru, 123456” быстро превращает жизнь в скуку. К счастью, есть библиотека Faker, которая генерирует реалистичные фейковые данные: имена, адреса, телефоны, даты, тексты и даже фейковые компании.

---

## Установка

pip install faker


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

from faker import Faker

fake = Faker()

print(fake.name())
print(fake.email())
print(fake.address())


Каждый вызов вернет новое случайное, но правдоподобное значение.

---

## Локализация данных

Если вы тестируете российское приложение, логичнее видеть “Иван Петров”, а не “John Smith”:

from faker import Faker

fake = Faker("ru_RU")

for _ in range(3):
print(fake.name(), "-", fake.phone_number())


Faker поддерживает десятки локалей: en_US, de_DE, fr_FR и т.д.

---

## Генерация набора тестовых пользователей

Допустим, нужно быстро заполнить базу 10 фейковыми пользователями:

from faker import Faker

fake = Faker("ru_RU")

def generate_user():
return {
"name": fake.name(),
"email": fake.unique.email(),
"address": fake.address(),
"birthdate": fake.date_of_birth(minimum_age=18, maximum_age=65),
}

users = [generate_user() for _ in range(10)]
for user in users:
print(user)


Метод fake.unique гарантирует уникальность значения (пока не исчерпан генератор).

---

## Фейковые данные для API и фронтенда

Нужно протестировать список товаров или карточек в интерфейсе? Не проблема:

from faker import Faker
import random

fake = Faker()

def generate_product():
return {
"id": fake.uuid4(),
"title": fake.sentence(nb_words=3),
"price": round(random.uniform(10, 500), 2),
"description": fake.text(max_nb_chars=80),
"in_stock": fake.boolean(chance_of_getting_true=80),
}

products = [generate_product() for _ in range(5)]
for product in products:
print(product)


---

## Фиксация “сидов” для повторяемости

Важно, чтобы при тестах данные могли воспроизводиться. Для этого задаем seed:

from faker import Faker

fake = Faker()
Faker.seed(42)

print(fake.name())
print(fake.name()) # при повторном запуске будут те же значения


---

Faker экономит часы рутины, делает тестовые данные живыми и разнообразными, а ваш код — более надежным. Попробуйте заменить все “test123” в ваших проектах на фейковые, но правдоподобные данные и посмотрите, сколько багов всплывет.
👍4
Изучение таймеров и замеров времени с модулем timeit
Изучаем таймеры и замеры времени с модулем timeit

Когда код работает медленно, интуиция часто подводит. Кажется, что «вот это место точно тормозит», а на деле время утекает в другом участке. Здесь на сцену выходит модуль timeit — простой способ точно измерить, что действительно медленнее, а что быстрее.

### Почему не time.time()?

Наивный подход:

import time

start = time.time()
result = sum(range(10_000_000))
end = time.time()
print(end - start)


Проблемы:
- одно измерение: могли попасть на фоновую нагрузку системы;
- точность зависит от платформы;
- неудобно сравнивать несколько вариантов.

timeit решает это: он запускает код много раз, считает среднее время и делает это максимально «чисто».

### Базовый пример использования

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

import timeit

t = timeit.timeit("sum(range(1000))", number=10000)
print(t)


Параметры важны:
- первый аргумент — строка с кодом, который измеряем;
- number — сколько раз выполнить этот код.

Но писать код в строках не всегда удобно. Лучше использовать функции и параметр stmt как объект:

import timeit

def calc():
return sum(range(1000))

t = timeit.timeit(stmt=calc, number=10000)
print(t)


Так проще рефакторить и отлаживать.

### Сравниваем два варианта кода

Предположим, мы хотим понять, что быстрее: список или генератор в sum.

import timeit

def use_list():
return sum([i for i in range(1000)])

def use_gen():
return sum(i for i in range(1000))

t_list = timeit.timeit(use_list, number=10000)
t_gen = timeit.timeit(use_gen, number=10000)

print("list:", t_list)
print("gen :", t_gen)


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

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

Если нужна подготовка данных, её не стоит включать в измеряемый код. Для этого есть setup:

import timeit

setup_code = "data = list(range(10000))"

stmt_code = """
result = 0
for x in data:
result += x
"""

t = timeit.timeit(stmt=stmt_code, setup=setup_code, number=1000)
print(t)


Здесь:
- в setup мы генерируем данные один раз для каждого прогона timeit;
- в stmt считаем время только самого алгоритма.

### Немного удобства: repeat

Чтобы понять разброс по времени, используйте repeat:

import timeit

def func():
return sum(range(10_000))

times = timeit.repeat(func, number=1000, repeat=5)
print(times) # список из 5 измерений
print("best:", min(times))


Так можно ориентироваться на минимум как на «лучшие условия» выполнения.

---

timeit — это карманный профилировщик для микробенчмарков. Он не заменяет полноценный профилировщик, но идеально подходит, когда нужно честно ответить на вопрос: «Этот способ действительно быстрее, или мне только кажется?»
🔥4
Как использовать модуль zipfile для создания и извлечения архивов
Как использовать модуль zipfile для создания и извлечения архивов

Когда файлов становится много, а отправить нужно «всё и сразу», на сцену выходит модуль zipfile. Он входит в стандартную библиотеку Python, так что ничего устанавливать не нужно.

---

### Создаем ZIP‑архив из одного файла

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

import zipfile

with zipfile.ZipFile("data.zip", mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.write("report.txt", arcname="report_in_zip.txt")


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

- mode="w" — создаем новый архив (старый будет перезаписан).
- compression=ZIP_DEFLATED — обычное сжатие ZIP.
- write("report.txt", arcname=...) — первое имя — исходный файл, второе — как он будет называться внутри архива.

---

### Добавляем несколько файлов и папку

Чтобы пройтись по директории и заархивировать всё, что внутри:

import zipfile
import os

def zip_folder(folder_path, zip_name):
with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(folder_path):
for file_name in files:
full_path = os.path.join(root, file_name)
rel_path = os.path.relpath(full_path, folder_path)
zf.write(full_path, arcname=rel_path)

zip_folder("project", "project_backup.zip")


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

---

### Извлекаем архив целиком или выборочно

Извлечь всё содержимое очень просто:

import zipfile

with zipfile.ZipFile("project_backup.zip", "r") as zf:
zf.extractall("restored_project")


А вот выборочное извлечение:

import zipfile

with zipfile.ZipFile("project_backup.zip", "r") as zf:
for name in zf.namelist():
if name.endswith(".py"):
zf.extract(name, "only_scripts")


namelist() возвращает список файлов внутри архива. Можно фильтровать по расширению, имени и т.д.

---

### Проверяем содержимое без распаковки

Иногда нужно просто «подглядеть» внутрь ZIP:

import zipfile

with zipfile.ZipFile("project_backup.zip", "r") as zf:
for info in zf.infolist():
print(info.filename, info.file_size, "bytes")


Так можно быстро оценить структуру и размер архива.

---

zipfile полезен для бэкапов, упаковки логов, раздачи учебных проектов и автоматизации рутины. Освоив эти несколько приёмов, вы легко встроите архивирование прямо в свои скрипты.
👍2
Работа с командной строкой с помощью argparse
Python для начинающих: приручаем командную строку с argparse

Рано или поздно любой скрипт вырастает из стадии «запустить и забыть» и ему хочется передавать параметры: путь к файлу, режим работы, уровень детализации логов. Жестко прописывать это в коде неудобно – каждый раз править и перезапускать. Тут и появляется герой дня — модуль argparse.

### Базовый пример: скрипт-калькулятор

Сделаем простой калькулятор, который принимает числа и операцию из командной строки:

import argparse

def main():
parser = argparse.ArgumentParser(
description="Simple CLI calculator"
)

parser.add_argument("x", type=float, help="First number")
parser.add_argument("y", type=float, help="Second number")
parser.add_argument(
"--op", "-o",
choices=["add", "sub", "mul", "div"],
default="add",
help="Operation to perform"
)

args = parser.parse_args()

if args.op == "add":
result = args.x + args.y
elif args.op == "sub":
result = args.x - args.y
elif args.op == "mul":
result = args.x * args.y
elif args.op == "div":
result = args.x / args.y

print(result)

if __name__ == "__main__":
main()


Теперь можно запускать так:

python calc.py 10 5 --op mul
# 50.0


Позиционные аргументы (x, y) обязательны, опция --op имеет значение по умолчанию и подсказку.

### Флаги и логические переключатели

Частая задача — включить «болтливый» режим:

import argparse

parser = argparse.ArgumentParser(description="Demo for flags")
parser.add_argument("--verbose", "-v", action="store_true",
help="Enable verbose mode")
args = parser.parse_args()

if args.verbose:
print("Verbose mode is ON")


Флаг без значения, просто включается при наличии в командной строке.

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

argparse сам преобразует типы и выдаст внятную ошибку, если пользователь введет ерунду:

parser.add_argument(
"--limit",
type=int,
default=10,
help="Max number of items (default: 10)"
)


Попробуйте передать строку вместо числа — будет аккуратное сообщение с подсказкой по использованию.

### Подкоманды: один скрипт — много режимов

Когда скрипт начинает «разрастаться», удобно завести подкоманды, как у git (git add, git commit):

import argparse

parser = argparse.ArgumentParser(description="User manager")
subparsers = parser.add_subparsers(dest="command", required=True)

add_parser = subparsers.add_parser("add", help="Add new user")
add_parser.add_argument("name")
add_parser.add_argument("--admin", action="store_true")

list_parser = subparsers.add_parser("list", help="List users")

args = parser.parse_args()

if args.command == "add":
print(f"Adding user {args.name}, admin={args.admin}")
elif args.command == "list":
print("Listing users...")


Запуск:

python usertool.py add Alice --admin
python usertool.py list


argparse берет на себя всю грязную работу по разбору аргументов, проверке типов и красивой справке. А вы концентрируетесь на логике программы, а не на парсинге строк.
👍2
Как использовать collections.Counter для простого анализа данных
Как использовать collections.Counter для простого анализа данных

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

---

### Что такое Counter

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

from collections import Counter

data = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
counter = Counter(data)

print(counter) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
print(counter['apple']) # 3


Counter сразу выдаёт вам частоты элементов. Можно думать о нём как об «умном» словаре для статистики.

---

### Анализ текста: частота слов и букв

Классический пример — быстрый анализ текста.

from collections import Counter

text = "data analysis with python and data tools"
words = text.split()

word_freq = Counter(words)
char_freq = Counter(text.replace(" ", ""))

print(word_freq.most_common(3)) # топ-3 самых частых слов
print(char_freq.most_common(5)) # топ-5 самых частых символов


Метод .most_common(n) возвращает n самых популярных элементов — готовая мини-аналитика.

---

### Подсчёт категорий: простой пример «аналитики»

Допустим, у вас есть список заказов с типами товаров:

from collections import Counter

orders = [
"book", "book", "laptop", "phone",
"book", "phone", "tablet", "laptop"
]

category_stats = Counter(orders)

print(category_stats) # сколько каких товаров заказали
print(category_stats.most_common(1)) # самый популярный товар


Без циклов и лишнего кода вы уже понимаете распределение по категориям.

---

### Операции над Counter: как с многомерными счётчиками

Counter умеет складывать, вычитать и объединять данные — удобно для сравнения наборов.

from collections import Counter

week1 = Counter(['book', 'book', 'phone'])
week2 = Counter(['book', 'tablet', 'phone', 'phone'])

print(week1 + week2) # суммарные продажи
print(week2 - week1) # что «добавилось» во второй неделе


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

---

### Фильтрация и преобразование результата

Counter легко превратить в обычный словарь и отфильтровать по условию:

from collections import Counter

data = ['a', 'b', 'a', 'c', 'b', 'a', 'd']
c = Counter(data)

freq = {k: v for k, v in c.items() if v > 1}
print(freq) # {'a': 3, 'b': 2}


---

collections.Counter — отличный инструмент, чтобы «понюхать данные»: быстро понять, что в них чаще всего встречается, найти лидеров и выбросы. И всё это в пару строк кода, без тяжёлых библиотек и сложной математики.
👍3
Создание PDF документов с помощью библиотеки FPDF
Создание PDF-документов с помощью FPDF: просто, как print()

PDF — идеальный формат для чеков, отчетов, сертификатов и даже мини-отчетов по учебным проектам. В Python один из самых понятных инструментов для их создания — библиотека fpdf2 (современный форк FPDF).

---

### Установка

pip install fpdf2


Базовый объект — FPDF, с ним и будем работать.

---

### Первый PDF за 10 строк

Создадим простой одностраничный документ с заголовком и текстом.

from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", size=16)
pdf.cell(0, 10, "Hello, PDF world!", ln=1, align="C")

pdf.set_font("Arial", size=12)
text = "This is a simple PDF generated with fpdf2 in Python."
pdf.multi_cell(0, 8, text)

pdf.output("example_basic.pdf")


Ключевые моменты:
- add_page() — добавляем страницу.
- set_font() — выбираем шрифт и размер.
- cell() — строка текста; ln=1 переносит курсор на новую строку.
- multi_cell() — блок текста с переносами.
- output() — сохраняем файл.

---

### Простой “отчет”: таблица в PDF

Сделаем мини-таблицу, например, список задач.

from fpdf import FPDF

data = [
("ID", "Task", "Status"),
("1", "Learn FPDF basics", "Done"),
("2", "Generate simple report", "In progress"),
("3", "Share PDF with team", "Pending"),
]

pdf = FPDF()
pdf.add_page()
pdf.set_font("Arial", "B", 14)
pdf.cell(0, 10, "Tasks Report", ln=1, align="C")
pdf.ln(4)

pdf.set_font("Arial", size=11)
col_widths = [15, 90, 30]

for row_index, row in enumerate(data):
for col_index, text in enumerate(row):
style = "B" if row_index == 0 else ""
pdf.set_font("Arial", style, 11)
pdf.cell(col_widths[col_index], 8, text, border=1)
pdf.ln(8)

pdf.output("tasks_report.pdf")


Здесь:
- Используем список кортежей как источник данных.
- Меняем стиль шрифта для заголовка таблицы.
- border=1 рисует рамки ячеек — уже похоже на отчет.

---

### Добавление изображения

Например, логотип в шапке:

from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.image("logo.png", x=10, y=8, w=30)

pdf.set_font("Arial", "B", 16)
pdf.cell(0, 10, "Company Report", ln=1, align="C")

pdf.output("report_with_logo.pdf")


---

FPDF удобна тем, что работает “как конструктор”: просто добавляете текст, таблицы, картинки, постепенно превращая Python-скрипт в генератор аккуратных PDF-документов. Отличный навык для автоматизации отчетов и учебных проектов.
👍31