sendgrid review
sendgrid 6.12.5 is Twilio SendGrid's synchronous Python client for Web API v3. Mail classes assemble personalizations, content, templates, attachments, and settings for /mail/send. The fluent client builds other account paths, while EventWebhook verifies signed event payloads. This package calls HTTPS APIs; it is not an SMTP transport and cannot authenticate a sending domain. Release 6.12.5 replaces ecdsa with cryptography for signature verification, without adding a new sending workflow.
Our sendgrid 6.12.5 install took 0.5 seconds, used 18 MB across 7 packages, imported in 0.55 seconds, and had no audit findings, but it provides neither py.typed nor async I/O. Use it when SendGrid is already the provider; choose another boundary when portability or async transport matters more.
We installed it
| Install | ✓ · 0.5s | 7 packages on disk · 18 MB |
| Import | ✓ | import sendgrid in 0.55s · pure Python · requires Python >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does sendgrid install cleanly?
Yes. In a fresh container with an empty cache, pip install sendgrid finished in 0.5s, leaving 7 packages and 18 MB on disk. pip-audit reported no known vulnerabilities.
What does sendgrid need to run?
Python >=2.7, !=3.0., !=3.1., !=3.2., !=3.3., !=3.4.*, and nothing compiled: it is pure Python. In our run import sendgrid succeeded in 0.55s.
sendgrid or boto3: which should you use?
boto3: Choose Amazon SES through boto3 when IAM and AWS operations already own outbound mail. Our sendgrid 6.12.5 install took 0.5 seconds, used 18 MB across 7 packages, imported in 0.55 seconds, and had no audit findings, but it provides neither py.typed nor async I/O.
When should you not use sendgrid?
Provider independence is required; Mail and the fluent path API expose SendGrid-specific payloads throughout application code
Use it if
- An existing SendGrid account needs Python Mail helpers for templates, attachments, personalizations, and settings
- One authenticated client must also reach suppression, statistics, template, and other Web API v3 paths
- A webhook receiver needs the package's verification helper for SendGrid event signatures
- Synchronous HTTP fits a background worker or an application that already queues email
- Provider independence is required; Mail and the fluent path API expose SendGrid-specific payloads throughout application code
- The service is async throughout; this client uses synchronous python-http-client and supplies no native async transport
- Call success must mean recipient delivery; status 202 records API acceptance while delivery and bounce outcomes arrive later
- Packaging must target current Python only; 6.12.5 still declares Python 2.7 and conditional Werkzeug ranges for old interpreters
- No verified sender or authenticated domain exists in SendGrid; library code cannot replace account and DNS setup
Setup reality
We installed sendgrid 6.12.5 in 0.5 seconds on Python 3.12. Seven packages occupied 18 MB, metadata declared 9 direct dependencies, and import sendgrid succeeded in 0.55 seconds. The distribution is pure Python, uses MIT licensing, and has no py.typed marker. pip-audit found 0 known vulnerabilities. Version 6.12.5's switch from ecdsa accounts for cryptography even though ordinary sends are HTTP calls.
Create an API key with only the permissions this process uses and provide it as SENDGRID_API_KEY. The account also needs a verified sender or authenticated domain; domain setup requires DNS work outside Python. Keep production and test keys separate. The README mentions local .env support, but credential loading should follow the application's secret mechanism rather than checking a key into the project.
Recipient construction changes privacy. Addresses in one personalization can see the same To and CC set, while separate personalizations isolate them. Dynamic templates need template_id and dynamic_template_data; subject and body may come from the remote template. Responses expose status_code, headers, and bytes. python-http-client exceptions carry useful API errors that should be decoded without logging sensitive recipient content.
HTTP 202 records acceptance into SendGrid's queue, not inbox delivery. Store the message or request identifier and process Event Webhooks for delivered, deferred, bounce, and complaint states. Verify a webhook against its exact raw bytes before JSON parsing changes the payload. The SDK blocks during HTTP calls, so asyncio services should use a thread or durable job worker. Sandbox mode validates without sending and belongs in CI.
Patterns
Post one text-only email send-text-email
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
message = Mail(
from_email='alerts@example.com',
to_emails='user@example.net',
subject='Deploy complete',
plain_text_content='Build 412 is live.',
)
client = SendGridAPIClient(os.environ['SENDGRID_API_KEY'])
response = client.send(message)
print(response.status_code)SendGrid rejects a From address that belongs to neither a verified sender nor an authenticated domain.
Send multipart text and HTML content send-multipart-email
message = Mail(
from_email=('alerts@example.com', 'Status Bot'),
to_emails='user@example.net',
subject='Weekly summary',
plain_text_content='Plain text summary.',
html_content='<p>HTML <strong>summary</strong>.</p>',
)The text part remains necessary for recipients and filters that ignore HTML.
Isolate recipients into personalizations separate-recipients
from sendgrid.helpers.mail import Mail, To
recipients = [To('a@example.net'), To('b@example.net')]
message = Mail(
from_email='alerts@example.com',
to_emails=recipients,
subject='Maintenance',
plain_text_content='Window starts at 02:00 UTC.',
is_multiple=True,
)is_multiple=True prevents one recipient from receiving the other addresses in the same visible To collection.
Pass data into a remote template use-dynamic-template
message = Mail(
from_email='alerts@example.com',
to_emails='user@example.net',
)
message.template_id = 'd-1234567890abcdef1234567890abcdef'
message.dynamic_template_data = {
'first_name': 'Asha',
'invoice_total': '1240.00',
}
response = client.send(message)dynamic_template_data keys must match the template, and ordinary Mail content does not repair a template with no body.
Encode a PDF attachment for Mail attach-file
import base64
from sendgrid.helpers.mail import (
Attachment, Disposition, FileContent, FileName, FileType,
)
with open('invoice.pdf', 'rb') as handle:
encoded = base64.b64encode(handle.read()).decode('ascii')
message.attachment = Attachment(
FileContent(encoded),
FileName('invoice.pdf'),
FileType('application/pdf'),
Disposition('attachment'),
)FileContent receives base64 text rather than raw bytes; enforce the current attachment limit before encoding large input.
Exercise the API in sandbox mode sandbox-send
from sendgrid.helpers.mail import MailSettings, SandBoxMode
message.mail_settings = MailSettings()
message.mail_settings.sandbox_mode = SandBoxMode(True)
response = client.send(message)
assert response.status_code == 200Sandbox validation sends no message and returns a different success status from the usual 202 queue acceptance.
Decode structured API failures decode-api-error
import json
from python_http_client.exceptions import HTTPError
try:
response = client.send(message)
except HTTPError as exc:
payload = json.loads(exc.body.decode('utf-8'))
record_send_failure(exc.status_code, payload.get('errors', []))The exception body is bytes, so decode it before reading SendGrid's errors array.
Schedule delivery from a Unix timestamp schedule-delivery
from datetime import datetime, timedelta, timezone
message.send_at = int(
(datetime.now(timezone.utc) + timedelta(hours=6)).timestamp()
)send_at uses Unix time; cancellation also needs a batch ID and must follow current scheduling rules.
Carry an application ID into events add-event-metadata
from sendgrid.helpers.mail import Category, CustomArg
message.category = [Category('billing')]
message.custom_arg = CustomArg('invoice_id', 'inv_9182')SendGrid echoes custom arguments in event payloads, making secrets and sensitive personal data inappropriate values.
Verify events before JSON parsing verify-event-webhook
from sendgrid.helpers.eventwebhook import EventWebhook, EventWebhookHeader
verifier = EventWebhook(os.environ['SENDGRID_WEBHOOK_PUBLIC_KEY'])
valid = verifier.verify_signature(
raw_body,
request.headers[EventWebhookHeader.SIGNATURE],
request.headers[EventWebhookHeader.TIMESTAMP],
)
if not valid:
return '', 403Signature verification needs untouched request bytes; parsed and serialized JSON is a different byte sequence.
Build a suppression endpoint fluently list-bounces
response = client.client.suppression.bounces.get()
print(response.status_code)
print(response.body)Each attribute becomes a path segment, so spelling mistakes survive locally and fail at the remote API.
Route a client through SendGrid's EU region use-eu-endpoint
client = SendGridAPIClient(os.environ['SENDGRID_API_KEY'])
client.set_sendgrid_data_residency('eu')
response = client.send(message)Choose EU before any call and pair it with credentials provisioned for that regional account.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| boto3 | PyPI | Choose Amazon SES through boto3 when IAM and AWS operations already own outbound mail |
| mailersend | PyPI | Choose it when MailerSend owns the domains and its API objects match the product |
| resend | PyPI | Choose it for Resend's smaller transactional API in a new integration |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

