Gerar PDF com Python: Relatórios Profissionais
Aprenda a gerar relatórios em PDF com Python usando dados, HTML, gráficos, WeasyPrint, templates e boas práticas para automações corporativas.
Gerar PDF com Python é uma daquelas habilidades que parecem simples, mas aparecem em muitos cenários reais de trabalho: relatório financeiro mensal, fechamento comercial, extrato de uso, resumo de indicadores, laudo operacional, proposta para cliente, comprovante interno, ata de reunião ou material que precisa ser enviado por e-mail sem depender de uma planilha aberta. No Brasil, muita rotina corporativa ainda termina em PDF porque o formato é fácil de encaminhar, imprimir, anexar em sistemas e arquivar.
O problema é que muita gente começa tentando montar PDF direto no braço, posicionando texto por coordenada e brigando com quebra de página. Isso até funciona para documentos muito simples, mas fica difícil quando o relatório tem tabela, logo, cabeçalho, rodapé, gráficos, estilos e conteúdo variável. Para projetos profissionais, o caminho mais produtivo costuma ser outro: transformar dados em HTML com um template e converter esse HTML em PDF.
Neste guia, vamos montar um fluxo prático para gerar relatórios em PDF com Python. A ideia é combinar Pandas para preparar dados, Jinja2 para montar HTML, CSS para cuidar da aparência e WeasyPrint para gerar o PDF final. Também vamos falar de validação, automação, envio por e-mail, versionamento e como transformar isso em um projeto forte de portfólio para vagas de dados, automação, backoffice, operações e desenvolvimento backend.
Quando PDF faz sentido
PDF não deve ser o formato de trabalho para tudo. Se a pessoa precisa filtrar, cruzar ou editar dados, uma planilha, um dashboard ou uma tabela em banco costuma ser melhor. PDF faz sentido quando você quer entregar uma versão fechada, estável e compartilhável de uma análise.
Use PDF quando o relatório precisa:
- manter o mesmo layout em qualquer máquina;
- ser enviado para pessoas que não usam Python, BI ou planilha avançada;
- virar anexo de e-mail recorrente;
- guardar evidência de uma data específica;
- combinar texto explicativo, tabelas, totais e gráficos;
- ser impresso ou anexado em um sistema interno.
Um exemplo comum é um relatório semanal de vendas por região. A base pode vir de CSV, Excel, banco de dados ou API. O Python limpa os dados, calcula métricas, gera uma tabela resumida, adiciona comentários automáticos e cria um PDF que a liderança recebe toda segunda-feira. Para chegar nesse ponto, você precisa separar bem três responsabilidades: dados, apresentação e entrega.
Escolhendo a biblioteca certa
Existem várias formas de gerar PDF em Python. As mais comuns são:
| Abordagem | Quando usar |
|---|---|
| ReportLab | PDF programático com controle fino de posições |
| FPDF/fpdf2 | documentos simples, recibos e comprovantes |
| WeasyPrint | relatórios com HTML, CSS, tabelas e layout editorial |
| Playwright/Chromium | páginas web renderizadas exatamente como no navegador |
| nbconvert | converter notebooks Jupyter para PDF |
Para relatórios corporativos, WeasyPrint costuma ser uma excelente escolha porque permite usar HTML e CSS, duas tecnologias que muita gente já entende. Você escreve o relatório como uma página, usa um template para preencher os dados e deixa a biblioteca cuidar da conversão.
ReportLab continua útil quando o documento exige posicionamento muito controlado, como etiquetas, boletos internos, formulários com campos fixos ou layouts que seguem uma grade rígida. Para a maioria dos relatórios de negócio, porém, HTML + CSS é mais fácil de manter.
Preparando o ambiente
Crie um projeto isolado:
python -m venv .venv
source .venv/bin/activate
pip install pandas jinja2 weasyprint
Com uv, o fluxo fica assim:
uv init relatorio-pdf-python
cd relatorio-pdf-python
uv add pandas jinja2 weasyprint
Se você ainda está organizando dependências, leia também ambientes virtuais em Python e uv como gerenciador de pacotes. Para relatórios que rodam todo mês, ambiente reprodutível é parte da entrega. Não adianta o script funcionar só na sua máquina.
Uma estrutura simples pode começar assim:
relatorio-pdf-python/
data/
vendas.csv
templates/
relatorio.html
output/
src/
gerar_relatorio.py
pyproject.toml
README.md
Essa organização separa dados de entrada, template visual, PDF gerado e código Python. Em um projeto real, data/ pode ser substituído por uma consulta SQL, uma API ou um arquivo exportado por outro sistema.
Criando uma base de exemplo
Imagine um CSV chamado data/vendas.csv:
data,regiao,vendedor,produto,valor
2026-05-01,Sudeste,Ana,Curso Python,1290.00
2026-05-02,Sul,Bruno,Consultoria,2400.00
2026-05-03,Nordeste,Carla,Curso Python,890.00
2026-05-04,Sudeste,Diego,Suporte Mensal,1800.00
2026-05-05,Centro-Oeste,Ana,Consultoria,3200.00
O primeiro passo é ler e resumir os dados:
from pathlib import Path
import pandas as pd
BASE_DIR = Path(__file__).resolve().parent.parent
def carregar_vendas() -> pd.DataFrame:
caminho = BASE_DIR / "data" / "vendas.csv"
df = pd.read_csv(caminho, parse_dates=["data"])
colunas_obrigatorias = {"data", "regiao", "vendedor", "produto", "valor"}
ausentes = colunas_obrigatorias - set(df.columns)
if ausentes:
raise ValueError(f"Colunas ausentes: {', '.join(sorted(ausentes))}")
if (df["valor"] < 0).any():
raise ValueError("A coluna valor não pode ter números negativos")
return df
def resumir_vendas(df: pd.DataFrame) -> dict:
por_regiao = (
df.groupby("regiao", as_index=False)["valor"]
.sum()
.sort_values("valor", ascending=False)
)
return {
"total": df["valor"].sum(),
"pedidos": len(df),
"ticket_medio": df["valor"].mean(),
"periodo_inicio": df["data"].min().strftime("%d/%m/%Y"),
"periodo_fim": df["data"].max().strftime("%d/%m/%Y"),
"por_regiao": por_regiao.to_dict(orient="records"),
}
Repare que já existe uma validação básica antes de gerar o PDF. Isso evita publicar um relatório bonito com dado errado. Em projetos mais críticos, você pode usar Pandera para validação de dados ou testes com pytest.
Montando o template HTML
Crie templates/relatorio.html:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Relatório de vendas</title>
<style>
@page {
size: A4;
margin: 22mm 18mm;
}
body {
font-family: Arial, sans-serif;
color: #1f2937;
line-height: 1.5;
}
h1 {
color: #306998;
margin-bottom: 4px;
}
.meta {
color: #6b7280;
font-size: 12px;
margin-bottom: 24px;
}
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.card {
border: 1px solid #d7dee8;
padding: 12px;
border-radius: 6px;
background: #f7f3ea;
}
.label {
font-size: 11px;
text-transform: uppercase;
color: #6b7280;
}
.value {
font-size: 20px;
font-weight: 700;
margin-top: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 12px;
}
th, td {
border-bottom: 1px solid #d7dee8;
padding: 8px;
text-align: left;
}
th {
background: #eef6fb;
}
.number {
text-align: right;
}
</style>
</head>
<body>
<h1>Relatório de vendas</h1>
<p class="meta">Período: {{ periodo_inicio }} a {{ periodo_fim }}</p>
<section class="cards">
<div class="card">
<div class="label">Receita total</div>
<div class="value">R$ {{ total_formatado }}</div>
</div>
<div class="card">
<div class="label">Pedidos</div>
<div class="value">{{ pedidos }}</div>
</div>
<div class="card">
<div class="label">Ticket médio</div>
<div class="value">R$ {{ ticket_medio_formatado }}</div>
</div>
</section>
<h2>Receita por região</h2>
<table>
<thead>
<tr>
<th>Região</th>
<th class="number">Valor</th>
</tr>
</thead>
<tbody>
{% for item in por_regiao %}
<tr>
<td>{{ item.regiao }}</td>
<td class="number">R$ {{ item.valor_formatado }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
O template é HTML normal com variáveis Jinja2. A vantagem é clara: se o time quiser mudar cor, tabela ou texto, você mexe no template sem reescrever a lógica de dados.
Gerando o PDF
Agora conecte tudo em src/gerar_relatorio.py. Em um projeto pequeno, você pode deixar a leitura, a montagem do contexto e a renderização no mesmo arquivo. Quando o fluxo crescer, separe em módulos como dados.py, metricas.py e pdf.py.
from pathlib import Path
import pandas as pd
from jinja2 import Environment, FileSystemLoader, select_autoescape
from weasyprint import HTML
BASE_DIR = Path(__file__).resolve().parent.parent
def moeda(valor: float) -> str:
return f"{valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
def carregar_vendas() -> pd.DataFrame:
df = pd.read_csv(BASE_DIR / "data" / "vendas.csv", parse_dates=["data"])
colunas_obrigatorias = {"data", "regiao", "vendedor", "produto", "valor"}
ausentes = colunas_obrigatorias - set(df.columns)
if ausentes:
raise ValueError(f"Colunas ausentes: {', '.join(sorted(ausentes))}")
if (df["valor"] < 0).any():
raise ValueError("A coluna valor não pode ter números negativos")
return df
def montar_contexto(df: pd.DataFrame) -> dict:
por_regiao = (
df.groupby("regiao", as_index=False)["valor"]
.sum()
.sort_values("valor", ascending=False)
)
por_regiao["valor_formatado"] = por_regiao["valor"].map(moeda)
total = df["valor"].sum()
ticket_medio = df["valor"].mean()
return {
"periodo_inicio": df["data"].min().strftime("%d/%m/%Y"),
"periodo_fim": df["data"].max().strftime("%d/%m/%Y"),
"total_formatado": moeda(total),
"pedidos": len(df),
"ticket_medio_formatado": moeda(ticket_medio),
"por_regiao": por_regiao.to_dict(orient="records"),
}
def renderizar_pdf(contexto: dict) -> Path:
env = Environment(
loader=FileSystemLoader(BASE_DIR / "templates"),
autoescape=select_autoescape(["html", "xml"]),
)
template = env.get_template("relatorio.html")
html = template.render(**contexto)
saida = BASE_DIR / "output" / "relatorio-vendas.pdf"
saida.parent.mkdir(exist_ok=True)
HTML(string=html, base_url=str(BASE_DIR)).write_pdf(saida)
return saida
if __name__ == "__main__":
dados = carregar_vendas()
contexto = montar_contexto(dados)
caminho_pdf = renderizar_pdf(contexto)
print(f"PDF gerado em: {caminho_pdf}")
Esse script gera um PDF com métricas e tabela resumida. O ponto importante é que o PDF não nasce de valores digitados manualmente. Ele nasce de um fluxo reproduzível: entrada de dados, validação, agregação, template e exportação.
Adicionando gráficos
Relatórios em PDF ficam melhores quando combinam números e visualização. Uma forma simples é gerar uma imagem com Matplotlib e inserir no HTML:
import matplotlib.pyplot as plt
def gerar_grafico_regioes(por_regiao: pd.DataFrame) -> str:
caminho = BASE_DIR / "output" / "grafico-regioes.png"
caminho.parent.mkdir(exist_ok=True)
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(por_regiao["regiao"], por_regiao["valor"], color="#306998")
ax.set_title("Receita por região")
ax.set_ylabel("Valor em R$")
fig.tight_layout()
fig.savefig(caminho, dpi=160)
plt.close(fig)
return str(caminho)
Depois você passa grafico_regioes no contexto e usa no template:
<img src="{{ grafico_regioes }}" alt="Gráfico de receita por região">
Para relatórios de portfólio, um gráfico simples já mostra domínio de pipeline completo. Você demonstra que sabe transformar dados em comunicação, não apenas escrever código isolado.
Automatizando o envio
Depois que o PDF é gerado, você pode enviá-lo por e-mail, salvar em uma pasta compartilhada ou anexar em uma tarefa de atendimento. O site já tem um guia específico de automação de e-mails com Python, mas o cuidado principal vale repetir: não coloque senha no código.
Use variáveis de ambiente:
import os
smtp_user = os.environ["SMTP_USER"]
smtp_password = os.environ["SMTP_PASSWORD"]
Para rodar todo dia, comece simples: cron, GitHub Actions, GitLab CI, um job no servidor ou uma função agendada na nuvem. Se o fluxo crescer e tiver várias etapas, aí faz sentido estudar Airflow com Python. O erro é colocar orquestração pesada cedo demais. Primeiro faça um relatório confiável; depois automatize a agenda.
Boas práticas para relatórios reais
Em ambiente profissional, gerar PDF é só parte do trabalho. O que dá confiança ao processo são os detalhes ao redor:
- valide colunas obrigatórias antes de calcular métricas;
- registre a data e a fonte dos dados no relatório;
- salve o PDF com nome previsível, como
relatorio-vendas-2026-06.pdf; - mantenha o template versionado no Git;
- escreva um README com instruções de execução;
- gere logs simples indicando quantidade de linhas processadas;
- trate ausência de dados como erro visível, não como relatório vazio;
- teste a função que calcula métricas separadamente do template.
Também vale pensar em privacidade. Se o PDF contém dados pessoais, salário, cobrança, inadimplência, saúde ou informação sensível, limite acesso, evite anexos desnecessários e revise a política interna da empresa. Um relatório automatizado que manda dado confidencial para o destinatário errado é pior do que um processo manual lento.
Ideia de projeto para portfólio
Um projeto de geração de PDF pode ser excelente para quem busca vaga júnior ou transição para dados. Ele é concreto, fácil de demonstrar e conversa com muitas vagas brasileiras que pedem Python, Excel, Power BI, SQL, relatórios e automação.
Um bom escopo seria:
- Ler uma base pública ou CSV de exemplo.
- Validar colunas e tipos.
- Gerar métricas com Pandas.
- Criar um gráfico simples.
- Renderizar um PDF com template HTML.
- Incluir testes para os cálculos principais.
- Escrever README com exemplo de saída.
Você pode usar dados públicos brasileiros, uma base fictícia de vendas ou um dataset de atendimento. O importante é deixar claro o problema: “gerar automaticamente um relatório mensal de indicadores a partir de uma base CSV”. Isso soa mais profissional do que “script de PDF”.
Para conectar esse projeto à busca por vaga, leia também projetos de portfólio Python, currículo Python para vaga júnior e acompanhe as vagas Python no Brasil. Se você está comparando oportunidades de entrada além de Python, o eu.dev.br ajuda a enxergar requisitos reais de estágio, júnior e primeiro emprego tech.
Erros comuns
O primeiro erro é misturar regra de negócio dentro do template. O HTML deve receber valores prontos para exibição. Cálculo de total, filtro de período, validação e ordenação devem ficar no Python.
O segundo erro é depender de caminho absoluto da sua máquina, como /home/usuario/Downloads/vendas.csv. Use pathlib, caminhos relativos ao projeto e instruções claras no README.
O terceiro erro é gerar PDF sem revisar o resultado. Sempre abra o arquivo gerado, confira quebra de página, acentos, tabelas longas e valores monetários. Se possível, gere uma amostra pequena em CI para garantir que o script não quebrou.
O quarto erro é tratar PDF como banco de dados. Depois que virou PDF, extrair informação fica mais difícil. Guarde também a base original ou um CSV agregado, principalmente se o relatório serve para auditoria.
Conclusão
Gerar PDF com Python é uma habilidade prática porque junta dados, automação e comunicação. Em vez de apenas criar uma planilha ou imprimir um notebook, você entrega um documento final que outra pessoa consegue abrir, arquivar e compartilhar. Para empresas, isso reduz trabalho manual. Para quem está construindo carreira, mostra maturidade: você sabe pegar dados, validar entrada, calcular métricas, montar uma apresentação e produzir um artefato final.
Comece com um CSV pequeno, um template HTML e WeasyPrint. Depois adicione gráficos, testes, envio por e-mail e agendamento. Esse caminho cria um projeto de portfólio realista, útil e alinhado com demandas que aparecem em vagas brasileiras de análise de dados, automação, operações, finanças, marketing e backend Python.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português