Last modified: Oct 10, 2026
Send HTML Emails with SendGrid and Python
Plain text emails get the job done, but they look dull. HTML emails let you add branding, buttons, images, and clean layouts.
SendGrid makes sending HTML emails from Python simple. The official library gives you clean helpers for content, attachments, and templates.
This guide walks you through the full process. You will set up your API key, build a rich HTML message, add attachments, and handle errors correctly.
Why HTML Emails Matter
HTML emails improve engagement. Buttons are clickable. Headings guide the eye. Branded colors build recognition.
They also improve accessibility when written well. Proper semantic markup helps screen readers parse your content.
Best practice: Always send both HTML and plain text versions. Some clients block HTML, and plain text is a reliable fallback.
If you are new to SendGrid itself, our guide on how to install SendGrid in Python covers the setup before you start building emails.
Install and Set Up the Library
Install the sendgrid package with pip. Then store your API key in an environment variable.
pip install sendgrid Collecting sendgrid Downloading sendgrid-6.12.5-py3-none-any.whl (85 kB) Collecting python-http-client Downloading python_http_client-3.3.7-py3-none-any.whl (12 kB) Successfully installed python-http-client-3.3.7 sendgrid-6.12.5 Set your API key as an environment variable. Never hardcode secrets in your source files.
export SENDGRID_API_KEY='your_api_key_here' On Windows, use set instead of export. For production, use a secrets manager or a .env file loaded by python-dotenv.
Send Your First HTML Email
The Mail() helper builds the message. The html_content field holds your markup. The SendGridAPIClient() class sends it.
import os from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail # Build the HTML email message = Mail( from_email='[email protected]', to_emails='[email protected]', subject='Welcome to Our Service', html_content=''' Welcome Aboard!
Thanks for signing up. We are glad to have you.
''' ) # Create the client and send sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) response = sg.send(message) print(response.status_code) 202 A status code of 202 means SendGrid accepted the message for delivery. That is the success response.
Important: The from_email address must be a verified sender in your SendGrid account. Unverified senders return a 403 error.
Add a Plain Text Fallback
Always include a plain text version alongside your HTML. It improves deliverability and accessibility.
Pass both fields to the Mail() helper. SendGrid picks the right version for each client.
message = Mail( from_email='[email protected]', to_emails='[email protected]', subject='Order Confirmation', plain_text_content='Your order #12345 has shipped. Track it at https://example.com/track', html_content=''' Order Confirmed
Your order #12345 has shipped.
''' ) This is best practice for every transactional email you send.
Use Inline CSS for Styling
Email clients strip out external stylesheets and many style tags. Inline CSS is the safest way to style your HTML emails.
Write styles directly on elements with the style attribute. Keep your CSS simple and avoid modern layout features that many clients do not support.
html = ''' ''' message = Mail( from_email='[email protected]', to_emails='[email protected]', subject='Weekly Report', html_content=html ) This pattern works reliably across Gmail, Outlook, and Apple Mail. Test in multiple clients before sending to your full list.
Attach Files to HTML Emails
You can attach files to HTML emails just like plain text emails. Encode the file in base64 and add it to the message.
import base64 from sendgrid.helpers.mail import ( Attachment, FileContent, FileName, FileType, Disposition ) # Read and encode the file with open('invoice.pdf', 'rb') as f: encoded = base64.b64encode(f.read()).decode() # Build the attachment attachment = Attachment() attachment.file_content = FileContent(encoded) attachment.file_name = FileName('invoice.pdf') attachment.file_type = FileType('application/pdf') attachment.disposition = Disposition('attachment') # Add to the HTML message message.attachment = attachment This is perfect for invoices, reports, and receipts. If you need to generate those files first, you can generate PDFs with ReportLab and attach the result directly.
Keep total message size under 25 MB. Compress large files or host them for download instead.
Embed Images in HTML Emails
You can reference hosted images with the img tag. Use a full HTTPS URL. Do not use local file paths.
html = ''' 
Welcome!
Thanks for joining our platform.
''' Host your images on a reliable CDN. Broken image links look unprofessional and can trigger spam filters.
Always set the alt attribute. Many clients block images by default, and alt text keeps your email readable.
Use Dynamic Templates for Reusability
Hardcoding HTML in Python gets messy for recurring emails. Dynamic templates separate design from code.
Build the template in the SendGrid dashboard. Then pass a template ID and a dictionary of values from Python.
message = Mail( from_email='[email protected]', to_emails='[email protected]' ) message.template_id = 'd-abc123def456' message.dynamic_template_data = { 'customer_name': 'Jane', 'order_id': 'ORD-2024-001', 'items': [ {'name': 'Widget', 'price': '$9.99'}, {'name': 'Gadget', 'price': '$24.99'}, ] } response = sg.send(message) Templates use Handlebars syntax. You can loop over arrays, use conditionals, and set fallback values. This makes personalization at scale easy.
Note: The subject in the Mail object is ignored when a template is used. Set the subject inside the template itself.
For a deeper look at the core send flow, see this guide on sending emails with the SendGrid API in Python.
Handle Errors Correctly
Network issues and API limits happen. Wrap your send call in a try block and handle exceptions gracefully.
from python_http_client.exceptions import HTTPError try: response = sg.send(message) print(f"Status: {response.status_code}") except HTTPError as e: print(f"Error code: {e.status_code}") print(f"Reason: {e.reason}") print(f"Body: {e.body}") except Exception as e: print(f"Unexpected error: {e}") The HTTPError class exposes the status code, reason phrase, and raw response body. Log all three for faster debugging.
Common Errors and How to Fix Them
401 Unauthorized: The API key is invalid, missing, or lacks Mail Send permission. Verify the environment variable is set in your process.
403 Forbidden: The sender email is not verified. Verify the from address in the SendGrid dashboard.
400 Bad Request: A required field is missing. Check from_email, to_emails, subject, and html_content.
Emails land in spam: Authenticate your domain with SPF and DKIM. Avoid spam trigger words in subjects and body text.
Images do not load: You used a local path or an unreliable host. Use a public HTTPS URL.
Styles stripped: You used external CSS or style tags. Move all styling inline.
Best Practices for HTML Emails
Keep your HTML simple. Use tables for layout when targeting legacy clients like Outlook.
Test in Gmail, Outlook, and Apple Mail before sending to your full list. Each client renders CSS differently.
Use a single column layout for mobile. Most recipients open emails on their phones.
Keep subject lines short and specific. Around 40 characters works well.
Include an unsubscribe link in every marketing email. It is a legal requirement in most regions.
Track open and click rates with SendGrid's analytics. Use the data to improve your content over time.
Conclusion
Sending HTML emails with SendGrid and Python is straightforward. Build your markup, pass it to the Mail() helper, and send it with SendGridAPIClient().
Always include a plain text fallback. Use inline CSS for reliable rendering. Attach files with base64 encoding when needed.
For recurring emails, use dynamic templates to keep design separate from code. Handle exceptions carefully and log every failure.
Follow the examples in this guide, and your HTML emails will look professional and reach inboxes reliably. Happy coding!