Comunicação programática é um pilar de qualquer sistema moderno — confirmações de cadastro, alertas de sistema, notificações de pagamento, relatórios automáticos, mensagens de suporte. Python tem suporte nativo a SMTP para e-mails e integração direta com as principais APIs de comunicação do mercado: SendGrid, Twilio, Slack e WhatsApp. Neste artigo veremos como implementar cada uma dessas integrações de forma profissional.
E-mail com smtplib: Protocolo Nativo
import mimetypes
import smtplib
import ssl
from email.message import EmailMessage
from email.utils import formataddr
from pathlib import Path
class ClienteEmail:
"""Cliente SMTP reutilizável com suporte a TLS."""
def __init__(self, host: str, porta: int, usuario: str, senha: str, tls: bool = True):
self.host = host
self.porta = porta
self.usuario = usuario
self.senha = senha
self.tls = tls
def _criar_conexao(self) -> smtplib.SMTP:
smtp = smtplib.SMTP(self.host, self.porta, timeout=30)
if self.tls:
# Sem context, o starttls() não verifica o certificado do servidor
smtp.starttls(context=ssl.create_default_context())
smtp.login(self.usuario, self.senha)
return smtp
def enviar(self, destinatarios: list, assunto: str, corpo_texto: str,
corpo_html: str = None, anexos: list = None, cc: list = None,
bcc: list = None, remetente_nome: str = None) -> bool:
msg = EmailMessage()
msg["From"] = formataddr((remetente_nome, self.usuario)) if remetente_nome else self.usuario
msg["To"] = ", ".join(destinatarios)
msg["Subject"] = assunto
if cc:
msg["Cc"] = ", ".join(cc)
msg.set_content(corpo_texto) # texto puro (fallback)
if corpo_html:
msg.add_alternative(corpo_html, subtype="html") # preferido pelos clientes
for caminho_anexo in anexos or []:
arquivo = Path(caminho_anexo) # ausente: FileNotFoundError,
tipo, _ = mimetypes.guess_type(arquivo.name) # em vez de sair sem o anexo
principal, sub = (tipo or "application/octet-stream").split("/")
msg.add_attachment(arquivo.read_bytes(), maintype=principal,
subtype=sub, filename=arquivo.name)
try:
with self._criar_conexao() as smtp:
# send_message lê To e Cc do cabeçalho; o Bcc vai só no envelope
smtp.send_message(msg, to_addrs=destinatarios + (cc or []) + (bcc or []))
print(f"E-mail enviado para {len(destinatarios + (cc or []) + (bcc or []))} destinatário(s).")
return True
except (smtplib.SMTPException, OSError) as e: # OSError: rede, DNS, timeout
print(f"Erro ao enviar e-mail: {e}")
return False
# Configurações por provedor
def gmail(usuario, senha):
return ClienteEmail("smtp.gmail.com", 587, usuario, senha)
def outlook(usuario, senha):
return ClienteEmail("smtp.office365.com", 587, usuario, senha)
def yahoo(usuario, senha):
return ClienteEmail("smtp.mail.yahoo.com", 587, usuario, senha)
# Uso
cliente = gmail(
os.getenv("EMAIL_USER"),
os.getenv("EMAIL_PASS") # use App Password no Gmail
)
cliente.enviar(
destinatarios=["ana@email.com"],
assunto="Boletim Escolar — Março 2024",
corpo_texto="Segue em anexo seu boletim escolar.",
corpo_html="""
<h2>Olá, Ana!</h2>
<p>Segue em anexo seu <strong>boletim escolar</strong> de Março 2024.</p>
<p>Em caso de dúvidas, entre em contato com a secretaria.</p>
""",
anexos=["boletim_2024-03.pdf"],
remetente_nome="Sistema Escolar"
)
Templates de E-mail com Jinja2
from jinja2 import Environment, FileSystemLoader
from pathlib import Path
def criar_ambiente_templates():
Path("templates/email").mkdir(parents=True, exist_ok=True)
return Environment(
loader=FileSystemLoader("templates/email"),
autoescape=True
)
# templates/email/base.html
TEMPLATE_BASE = """
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; }
.container { max-width: 600px; margin: 20px auto; background: white;
border-radius: 8px; overflow: hidden; }
.header { background: #1F4E79; color: white; padding: 30px;
text-align: center; }
.header h1 { margin: 0; font-size: 22px; }
.body { padding: 30px; color: #333; line-height: 1.6; }
.footer { background: #f4f4f4; padding: 15px; text-align: center;
font-size: 12px; color: #888; }
.btn { display: inline-block; background: #2E75B6; color: white;
padding: 12px 24px; border-radius: 4px; text-decoration: none;
font-weight: bold; margin: 15px 0; }
.alerta { background: #FCE4D6; border-left: 4px solid #C00000;
padding: 12px; border-radius: 4px; }
.sucesso { background: #E2EFDA; border-left: 4px solid #375623;
padding: 12px; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; }
th { background: #2E75B6; color: white; padding: 10px; }
td { padding: 8px; border-bottom: 1px solid #eee; }
tr:nth-child(even) td { background: #f9f9f9; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{% block titulo %}Sistema Escolar{% endblock %}</h1>
</div>
<div class="body">
{% block conteudo %}{% endblock %}
</div>
<div class="footer">
<p>© 2024 Sistema Escolar. Este é um e-mail automático.</p>
<p><a href="{{ url_cancelar }}">Cancelar inscrição</a></p>
</div>
</div>
</body>
</html>
"""
# templates/email/boas_vindas.html
TEMPLATE_BOAS_VINDAS = """
{% extends "base.html" %}
{% block titulo %}Bem-vindo(a), {{ nome }}!{% endblock %}
{% block conteudo %}
<p>Olá, <strong>{{ nome }}</strong>!</p>
<p>Sua conta foi criada com sucesso no <strong>Sistema Escolar</strong>.</p>
<div class="sucesso">
<strong>Seus dados de acesso:</strong><br>
E-mail: {{ email }}<br>
Senha temporária: {{ senha_temp }}
</div>
<p>Por segurança, recomendamos alterar sua senha no primeiro acesso.</p>
<a href="{{ url_acesso }}" class="btn">Acessar o Sistema</a>
<p>Em caso de problemas, entre em contato com a secretaria.</p>
{% endblock %}
"""
class ServicoEmail:
def __init__(self, cliente: ClienteEmail):
self.cliente = cliente
self.env = criar_ambiente_templates()
# Salva templates em disco
Path("templates/email/base.html").write_text(TEMPLATE_BASE, encoding="utf-8") # o Jinja lê UTF-8
Path("templates/email/boas_vindas.html").write_text(TEMPLATE_BOAS_VINDAS, encoding="utf-8")
def _renderizar(self, template_nome: str, contexto: dict) -> str:
template = self.env.get_template(template_nome)
return template.render(**contexto, url_cancelar="#")
def boas_vindas(self, nome: str, email: str, senha_temp: str):
html = self._renderizar("boas_vindas.html", {
"nome": nome,
"email": email,
"senha_temp": senha_temp,
"url_acesso": "https://sistema.escolar.com/login"
})
texto = f"Olá, {nome}! Sua conta foi criada. Senha temporária: {senha_temp}"
return self.cliente.enviar(
destinatarios=[email],
assunto=f"Bem-vindo(a) ao Sistema Escolar, {nome}!",
corpo_texto=texto,
corpo_html=html,
remetente_nome="Sistema Escolar"
)
def boletim(self, aluno: dict, pdf_path: str):
html = f"""
<h2>Olá, {aluno['nome']}!</h2>
<p>Seu boletim do mês está disponível em anexo.</p>
<table>
<tr><th>Disciplina</th><th>Nota</th><th>Status</th></tr>
{''.join(
f"<tr><td>{d['nome']}</td><td>{d['nota']}</td>"
f"<td>{'✓' if d['nota'] >= 6 else '✗'}</td></tr>"
for d in aluno.get('disciplinas', [])
)}
</table>
"""
return self.cliente.enviar(
destinatarios=[aluno["email"]],
assunto="Seu Boletim Escolar está disponível",
corpo_texto=f"Olá {aluno['nome']}, seu boletim está em anexo.",
corpo_html=html,
anexos=[pdf_path]
)
SendGrid: API Profissional de E-mail
pip install sendgrid
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (
Mail, To, From, Subject,
PlainTextContent, HtmlContent,
Attachment, FileContent, FileName,
FileType, Disposition
)
import base64
from pathlib import Path
class ClienteSendGrid:
def __init__(self, api_key: str, remetente: str, nome_remetente: str = ""):
self.sg = SendGridAPIClient(api_key)
self.remetente = remetente
self.nome = nome_remetente
def enviar(
self,
destinatario: str,
assunto: str,
html: str,
texto: str = None,
anexos: list = None
) -> bool:
mensagem = Mail(
from_email=From(self.remetente, self.nome),
to_emails=To(destinatario),
subject=Subject(assunto),
plain_text_content=PlainTextContent(texto or ""),
html_content=HtmlContent(html)
)
if anexos:
for caminho in anexos:
arquivo = Path(caminho)
with open(arquivo, "rb") as f:
dados = base64.b64encode(f.read()).decode()
anexo = Attachment(
file_content=FileContent(dados),
file_name=FileName(arquivo.name),
file_type=FileType("application/octet-stream"),
disposition=Disposition("attachment")
)
mensagem.attachment = anexo
try:
resposta = self.sg.send(mensagem)
print(f"SendGrid: {resposta.status_code}")
return resposta.status_code in [200, 202]
except Exception as e:
print(f"Erro SendGrid: {e}")
return False
sg = ClienteSendGrid(
api_key=os.getenv("SENDGRID_API_KEY"),
remetente="noreply@escola.com",
nome_remetente="Sistema Escolar"
)
sg.enviar(
destinatario="aluno@email.com",
assunto="Confirmação de Matrícula",
html="<h1>Matrícula confirmada!</h1><p>Bem-vindo(a).</p>",
texto="Matrícula confirmada! Bem-vindo(a)."
)
Twilio: SMS e WhatsApp
pip install twilio
import os
import json
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
class ClienteTwilio:
def __init__(self, account_sid: str, auth_token: str, numero: str):
self.client = Client(account_sid, auth_token)
self.numero = numero # número Twilio: +15551234567
def enviar_sms(self, destinatario: str, mensagem: str) -> bool:
"""Envia SMS. destinatario no formato +5511999999999"""
try:
msg = self.client.messages.create(
body=mensagem,
from_=self.numero,
to=destinatario
)
print(f"SMS enviado: {msg.sid}")
return True
except (TwilioRestException, OSError) as e: # OSError: rede fora, proxy, DNS
print(f"Erro Twilio: {e}")
return False
def enviar_whatsapp(self, destinatario: str, mensagem: str) -> bool:
"""
Envia WhatsApp via Twilio Sandbox.
destinatario: whatsapp:+5511999999999
"""
try:
msg = self.client.messages.create(
body=mensagem,
from_=f"whatsapp:{self.numero}",
to=f"whatsapp:{destinatario}"
)
print(f"WhatsApp enviado: {msg.sid}")
return True
except (TwilioRestException, OSError) as e:
print(f"Erro WhatsApp: {e}")
return False
def enviar_whatsapp_template(
self,
destinatario: str,
template_sid: str,
variaveis: dict
) -> bool:
"""Envia mensagem usando template aprovado pelo WhatsApp."""
try:
msg = self.client.messages.create(
from_=f"whatsapp:{self.numero}",
to=f"whatsapp:{destinatario}",
content_sid=template_sid,
content_variables=json.dumps(variaveis) # str(dict) sai com aspas simples: não é JSON
)
return True
except (TwilioRestException, OSError) as e:
print(f"Erro template: {e}")
return False
twilio = ClienteTwilio(
account_sid=os.getenv("TWILIO_ACCOUNT_SID"),
auth_token= os.getenv("TWILIO_AUTH_TOKEN"),
numero= os.getenv("TWILIO_NUMBER")
)
# SMS
twilio.enviar_sms(
"+5511999999999",
"Sua nota foi lançada: 8.5. Acesse o portal para ver o boletim."
)
# WhatsApp
twilio.enviar_whatsapp(
"+5511999999999",
"🎓 *Sistema Escolar*\nOlá, Ana! Sua nota de Matemática foi lançada: *8.5*"
)
Slack: Notificações para Equipes
pip install slack-sdk
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
class ClienteSlack:
def __init__(self, token: str):
self.client = WebClient(token=token)
def enviar_mensagem(self, canal: str, texto: str) -> bool:
try:
self.client.chat_postMessage(channel=canal, text=texto)
return True
except (SlackApiError, OSError) as e: # OSError: o Slack fora do ar
print(f"Erro Slack: {e}")
return False
def enviar_bloco(self, canal: str, blocos: list, texto: str = "") -> bool:
"""Envia mensagem com Block Kit — layout rico."""
try:
self.client.chat_postMessage(
channel=canal, blocks=blocos,
text=texto, # o que a notificação mostra; sem ele, o SDK avisa
)
return True
except (SlackApiError, OSError) as e:
print(f"Erro Slack: {e}")
return False
def notificar_alerta(self, canal: str, titulo: str, mensagem: str, urgente: bool = False):
emoji = "🚨" if urgente else "ℹ️"
blocos = [
{
"type": "header",
"text": {"type": "plain_text", "text": f"{emoji} {titulo}"}
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": mensagem}
},
{
"type": "context",
"elements": [{
"type": "mrkdwn",
"text": f"*Sistema Escolar* | {datetime.now().strftime('%d/%m/%Y %H:%M')}"
}]
}
]
return self.enviar_bloco(canal, blocos, texto=f"{emoji} {titulo}")
def notificar_boletim(self, canal: str, aluno: str, nota: float):
status = "✅ Aprovado" if nota >= 6 else "❌ Reprovado"
blocos = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Nova nota lançada*\n*Aluno:* {aluno}\n*Nota:* {nota:.1f}\n*Status:* {status}"
}
},
{"type": "divider"},
{
"type": "actions",
"elements": [{
"type": "button",
"text": {"type": "plain_text", "text": "Ver Boletim"},
"url": "https://sistema.escolar.com/boletim"
}]
}
]
return self.enviar_bloco(canal, blocos, texto=f"Nova nota: {aluno}")
from datetime import datetime
slack = ClienteSlack(token=os.getenv("SLACK_BOT_TOKEN"))
slack.notificar_alerta(
canal="#alertas-sistema",
titulo="Erro no processamento",
mensagem="Falha ao gerar boletins da turma B. Verificar logs.",
urgente=True
)
slack.notificar_boletim(
canal="#notas",
aluno="Ana Silva",
nota=9.5
)
Exemplo Completo: Sistema Centralizado de Notificações
from enum import Enum
from dataclasses import dataclass
from typing import Optional
import os
class Canal(str, Enum):
EMAIL = "email"
SMS = "sms"
WHATSAPP = "whatsapp"
SLACK = "slack"
@dataclass
class Notificacao:
destinatario: str
assunto: str
mensagem: str
mensagem_html: Optional[str] = None
canal: Canal = Canal.EMAIL
urgente: bool = False
anexos: list = None
class SistemaNotificacoes:
def __init__(self):
self._email = ClienteEmail(
host=os.getenv("SMTP_HOST", "smtp.gmail.com"),
porta=587,
usuario=os.getenv("EMAIL_USER"),
senha=os.getenv("EMAIL_PASS")
)
self._twilio = ClienteTwilio(
os.getenv("TWILIO_ACCOUNT_SID"),
os.getenv("TWILIO_AUTH_TOKEN"),
os.getenv("TWILIO_NUMBER")
)
self._slack = ClienteSlack(os.getenv("SLACK_BOT_TOKEN"))
def enviar(self, notif: Notificacao) -> bool:
print(f"[{notif.canal.value.upper()}] → {notif.destinatario}: {notif.assunto}")
if notif.canal == Canal.EMAIL:
return self._email.enviar(
destinatarios=[notif.destinatario],
assunto=notif.assunto,
corpo_texto=notif.mensagem,
corpo_html=notif.mensagem_html,
anexos=notif.anexos or []
)
elif notif.canal == Canal.SMS:
return self._twilio.enviar_sms(notif.destinatario, notif.mensagem)
elif notif.canal == Canal.WHATSAPP:
return self._twilio.enviar_whatsapp(notif.destinatario, notif.mensagem)
elif notif.canal == Canal.SLACK:
return self._slack.notificar_alerta(
canal=notif.destinatario,
titulo=notif.assunto,
mensagem=notif.mensagem,
urgente=notif.urgente
)
return False
def enviar_multiplos(self, notificacoes: list) -> dict:
resultados = {"sucesso": 0, "falha": 0}
for notif in notificacoes:
if self.enviar(notif):
resultados["sucesso"] += 1
else:
resultados["falha"] += 1
return resultados
# Uso
sistema = SistemaNotificacoes()
# Notificar resultado de prova por múltiplos canais
aluno = {
"nome": "Ana Silva",
"email": "ana@email.com",
"telefone": "+5511999999999",
"nota": 9.5
}
notificacoes = [
Notificacao(
destinatario=aluno["email"],
assunto="Sua nota foi lançada!",
mensagem=f"Olá {aluno['nome']}, sua nota é {aluno['nota']}.",
mensagem_html=f"<h2>Olá, {aluno['nome']}!</h2><p>Nota: <strong>{aluno['nota']}</strong></p>",
canal=Canal.EMAIL
),
Notificacao(
destinatario=aluno["telefone"],
assunto="Nota lançada",
mensagem=f"Olá {aluno['nome']}! Sua nota: {aluno['nota']}. Aprovado(a)! 🎉",
canal=Canal.SMS
),
Notificacao(
destinatario="#professores",
assunto="Nota lançada",
mensagem=f"*{aluno['nome']}* recebeu nota *{aluno['nota']}*",
canal=Canal.SLACK
),
]
resultado = sistema.enviar_multiplos(notificacoes)
print(f"\nResultado: {resultado['sucesso']} enviados, {resultado['falha']} falhas")
Enviar uma mensagem por programa é sempre a mesma operação vestida de formas diferentes: montar o conteúdo, autenticar num serviço e entregar. Com smtplib e EmailMessage, o próprio Python monta um e-mail com texto, HTML e anexos e o entrega a qualquer servidor SMTP; o Jinja2 separa o layout do código; SendGrid, Twilio e Slack trocam o protocolo por uma API HTTP, com entregabilidade, SMS, WhatsApp e mensagens ricas para equipes. O sistema centralizado do fim põe todos atrás da mesma interface, e o passo seguinte natural é tirar o envio da requisição, com uma fila como a de Celery: Filas de Tarefas e Processamento Assíncrono, para que um provedor lento não atrase a resposta ao usuário.
Comunicação é a parte do sistema que fala com o mundo em nome da instituição, e por isso os erros daqui pesam mais. Um TLS sem verificação de certificado entrega a senha a quem estiver no meio do caminho; um remetente mal codificado vira spam ou some; um anexo que faltou sai como "segue em anexo" sem nada; e uma queda de rede num provedor não pode derrubar o envio pelos outros. Vale testar os envios contra um servidor SMTP local, que mostra a mensagem exatamente como sai, e tratar o retorno de cada canal como um dado que alguém vai conferir.
Fontes e leituras recomendadas
- smtplib — documentação oficial — https://docs.python.org/3/library/smtplib.html
- email.mime — documentação oficial — https://docs.python.org/3/library/email.mime.html
- SendGrid Python SDK — https://github.com/sendgrid/sendgrid-python
- Twilio Python SDK — https://www.twilio.com/docs/libraries/python
- Slack SDK para Python — https://slack.dev/python-slack-sdk/
- Slack Block Kit Builder — https://app.slack.com/block-kit-builder
- SWEIGART, Al. Automate the Boring Stuff with Python. 2. ed. No Starch Press, 2019. Cap. 16 — envio de e-mails e SMS com Python.
- PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. Cap. 8 — adaptadores para serviços externos e testes de integração.
Exercícios
Exercício 1
Uma auditoria de segurança aponta que o sistema de notificações da escola "aceita qualquer certificado" no envio de e-mails. O desenvolvedor responde que o código chama smtp.starttls(), "então a conexão é criptografada". Quem tem razão? Mostre o que o starttls() faz sem argumentos e qual é o risco concreto.
Ver resposta
✓ Resposta: Os dois, em parte, e o risco está na parte que o desenvolvedor não viu. A conexão é criptografada, mas o servidor não é autenticado. Sem o parâmetro context, o starttls() usa ssl._create_stdlib_context() — medido no código-fonte da 3.14 —, e esse contexto tem verify_mode igual a 0, CERT_NONE: qualquer certificado é aceito, inclusive um autoassinado de quem se colocou entre o servidor e o provedor, numa rede Wi-Fi de evento ou num proxy comprometido. Esse intermediário recebe o comando AUTH criptografado para ele mesmo e fica com o usuário e a senha da conta de envio. Com ssl.create_default_context(), o verify_mode é 2, CERT_REQUIRED, e o nome do servidor também é conferido. A correção é uma linha: smtp.starttls(context=ssl.create_default_context()), como no cliente corrigido do artigo. O mesmo vale para smtplib.SMTP_SSL, que recebe context no construtor. Criptografia sem autenticação só protege contra quem escuta sem interferir.
Exercício 2
Depois que o remetente passou a se chamar "Secretaria São José", as respostas dos pais pararam de chegar, e parte dos e-mails começou a cair no spam. Com "Secretaria Escolar" funcionava. O cabeçalho foi montado com msg["From"] = f"{remetente_nome} <{self.usuario}>" num MIMEMultipart. O que o acento mudou?
Ver resposta
✓ Resposta: Um cabeçalho de e-mail só pode ter ASCII; o que não é ASCII é codificado em "encoded-words", como =?utf-8?q?...?=. A API antiga do email.mime recebeu a string inteira e a codificou inteira — nome, sinal de menor, endereço e sinal de maior. Medido com um servidor SMTP local: o cabeçalho saiu como =?utf-8?q?Secretaria_S=C3=A3o_Jos=C3=A9_=3Csecretaria=40escola=2Eexample=3E?=, e o parser do destino entendeu tudo como o nome de uma caixa sem domínio: username='Secretaria São José <secretaria@escola.example>', domain=''. Sem endereço válido no From, o "Responder" não tem para onde ir, e os filtros tratam a mensagem como malformada. Com "Secretaria Escolar", tudo ASCII, nada era codificado e o defeito ficava invisível. A correção é montar o cabeçalho com email.utils.formataddr((nome, endereco)), que codifica só o nome, e usar a API moderna, EmailMessage: medido, o From passou a ter display_name='Secretaria São José' e o domínio escola.example. Todo teste de e-mail deveria incluir um nome acentuado.
Exercício 3
Na virada do mês, trezentos pais receberam "Segue em anexo o boletim de março" — sem anexo nenhum. O log registra "E-mail enviado para 1 destinatário(s)" trezentas vezes, e o enviar devolveu True em todas. O job que gera os PDFs tinha falhado antes, mas o de envio rodou no horário. Onde o cliente de e-mail do artigo contribuiu para o incidente?
Ver resposta
✓ Resposta: No if not arquivo.exists(): continue. O cliente tratava um anexo ausente como opcional: pulava o arquivo em silêncio e enviava o resto. Medido com um servidor SMTP local, pedindo dois anexos, um deles inexistente: a mensagem saiu com um só, e o enviar devolveu True. Para quem chama, não havia como saber. Um anexo pedido e não encontrado é erro, porque o texto do e-mail promete o arquivo. O cliente corrigido lê o arquivo com Path.read_bytes(), que levanta FileNotFoundError antes de qualquer conexão — medido. Havia ainda um segundo defeito, menos visível: o anexo era colocado dentro de uma parte multipart/alternative, que significa "versões diferentes do mesmo conteúdo", ao lado do texto e do HTML. A estrutura correta é multipart/mixed, com o alternative dentro e o anexo ao lado, que é o que o EmailMessage monta sozinho com add_alternative e add_attachment — medido. E, no desenho do processo, o job de envio deveria depender do de geração, e não de um horário.
Exercício 4
O enviar_multiplos recebe trinta notificações: e-mail, SMS e Slack. No dia em que a rede do escritório bloqueou o acesso ao Slack, o script terminou com um traceback de URLError na terceira notificação, e as outras vinte e sete nunca saíram — inclusive os e-mails, que funcionavam. Os métodos de envio tinham try/except. Por que não seguraram, e como corrigir?
Ver resposta
✓ Resposta: Porque cada except pegava só a exceção da própria biblioteca, e essas exceções representam respostas de erro do serviço: SlackApiError quando o Slack responde que o canal não existe, TwilioRestException quando a Twilio recusa o número, SMTPException quando o servidor recusa o login. Quando o serviço nem responde, o erro é de rede, e vem da camada de baixo. Medido: com o Slack inacessível, chat_postMessage levantou URLError; a Twilio atrás de um proxy fora do ar levantou ProxyError, do requests; e o SMTP numa porta fechada levantou ConnectionRefusedError. Nenhuma delas é subclasse da exceção da biblioteca, mas as três são OSError. A correção nos clientes é except (SlackApiError, OSError) e equivalentes — medido: os dois passaram a devolver False. Cuidado com o tratamento: o e.response['error'] do Slack não existe num OSError, e o próprio except levantaria AttributeError. Como defesa extra, o enviar_multiplos deve isolar cada envio com seu próprio try, porque um canal quebrado não pode impedir os outros.
Exercício 5
Um desenvolvedor passou a usar templates aprovados do WhatsApp com content_variables=str({"1": "Ana", "2": "9.5"}). Nos testes do Python, o valor "parece JSON" quando impresso, mas nenhuma mensagem sai com as variáveis preenchidas. Explique a diferença entre o que foi enviado e o que a API espera.
Ver resposta
✓ Resposta: str() de um dicionário é a representação do Python, não JSON. Medido: str({"1": "Ana", "2": "9.5"}) dá {'1': 'Ana', '2': '9.5'}, com aspas simples, e json.loads desse texto levanta JSONDecodeError, porque JSON só aceita aspas duplas. O parâmetro content_variables da Twilio é uma string com um objeto JSON, e o que chegou não é JSON válido. Com dados que por acaso não têm aspas nem acentos, a diferença é só essa troca de aspas, e passa despercebida numa inspeção visual; com um nome como "D'Ávila", a representação do Python passa a usar aspas duplas por fora e muda de forma de novo. A correção é json.dumps(variaveis), como no cliente corrigido. A regra geral vale para qualquer API: quando o parâmetro pede JSON em texto, gere com json.dumps, nunca com str ou f-string. Em testes, validar o parâmetro com json.loads antes de enviar pega o erro sem depender da resposta do serviço.