The whole game

Emailing reports is a critical but challenging task for data science. Mainly because you have to figure out generating the email content, configuring pieces like attachments, and orchestrating it (e.g. testing, or sending on a schedule). Moreover, content can range from simple layouts to more complex ones.

In this tutorial, we’ll walk through the whole game of sending email. We’ll start with this simple example:

Code
import os
from dotenv import load_dotenv
from data_polars import sp500
import redmail

load_dotenv()
gmail_address = os.environ["GMAIL_ADDRESS"]
gmail_app_password = os.environ["GMAIL_APP_PASSWORD"]


email_subject = "Report on Cars"
email_body = sp500.head(10).style.as_raw_html(inline_css=True)

# This is here to emphasize the sender does not have to be the same as the receiver
email_receiver = gmail_address

redmail.gmail.username = gmail_address
redmail.gmail.password = gmail_app_password

redmail.gmail.send(
    subject=email_subject,
    receivers=[email_receiver],
    html=email_body,
)

We’ll also quickly review writing more advanced content layouts, and authoring email reports that involve running code with Quarto.

A simple email

  • Generate and preview
  • Authenticate (may need to refer to its own authentication page in guide)
  • Send
Code
import os
from dotenv import load_dotenv
from data_polars import sp500
import redmail

load_dotenv()
gmail_address = os.environ["GMAIL_ADDRESS"]
gmail_app_password = os.environ["GMAIL_APP_PASSWORD"]


email_subject = "Report on Cars"
email_body = sp500.head(10).style.as_raw_html(inline_css=True)

# This is here to emphasize the sender does not have to be the same as the receiver
email_receiver = gmail_address

redmail.gmail.username = gmail_address
redmail.gmail.password = gmail_app_password

redmail.gmail.send(
    subject=email_subject,
    receivers=[email_receiver],
    html=email_body,
)

Configure: subject, recipients, attachments

  • you could attach the data as a CSV attachment

Orchestrate: save and preview

  • previewing email
  • intermediate json, easy for sending email later
  • embedding images makes previewing hard
  • can always email to yourself (or use a test service like Litmus)

Content: Quarto authoring

Here’s our same simple email generated using quarto.

  • Focused on basic configuring, and content
  • Sending happens via our tool
  • Generate using quarto render
  • Can preview email

Content: advanced layouts

We’ll highlight the key pieces (discussed later in this guide) to go from that simple email, to a more advanced on like below:

Fridge

In this tutorial, we are going to send an email from a Gmail account. To do so, you will need to create an App Password. Note this is only possible if you’ve enabled 2-step verification.

Tip

This is just one of many options: it is also possible to send emails in Python from other email providers (Outlook, ProtonMail, etc.), or even from a custom domain. To skip ahead to a discussion of alternative sending methods, see Authentication

Once you’ve created your App Password, that is used as your Gmail password for sending with Python.

There are many ways to store the password seperate from your email-sending code, so as to not expose any sensitive information. One such approach uses a .env file, and the `dotenv and os packages.

.env
GMAIL_APP_PASSWORD=abcd abcd abcd abcd
main.py
import os
from dotenv import load_dotenv

load_dotenv()

your_gmail_address = "YourGmail@gmail.com"
gmail_app_password = os.environ["GMAIL_APP_PASSWORD"]

Check out the email content we will send.

from data_polars import sp500

sp500.head(10).style
date open high low close volume adj_close
2015-12-31 2060.5901 2062.54 2043.62 2043.9399 2655330000.0 2043.9399
2015-12-30 2077.3401 2077.3401 2061.97 2063.3601 2367430000.0 2063.3601
2015-12-29 2060.54 2081.5601 2060.54 2078.3601 2542000000.0 2078.3601
2015-12-28 2057.77 2057.77 2044.2 2056.5 2492510000.0 2056.5
2015-12-24 2063.52 2067.3601 2058.73 2060.99 1411860000.0 2060.99
2015-12-23 2042.2 2064.73 2042.2 2064.29 3484090000.0 2064.29
2015-12-22 2023.15 2042.74 2020.49 2038.97 3520860000.0 2038.97
2015-12-21 2010.27 2022.9 2005.9301 2021.15 3760280000.0 2021.15
2015-12-18 2040.8101 2040.8101 2005.33 2005.55 6683070000.0 2005.55
2015-12-17 2073.76 2076.3701 2041.66 2041.89 4327390000.0 2041.89

And now we send the email!

import redmail

redmail.gmail.username = your_gmail_address
redmail.gmail.password = gmail_app_password

redmail.gmail.send(
    subject="An Example Email",
    receivers=[username],
    html=email_html,
    text=email_plaintext,
)
Subject: An Example Email
From: YourGmail@gmail.com
To: Recipient@gmail.com
Date: Tue, 14 Oct 2025 20:25:38 -0000
For a number of decades in the middle of the 20th century, the nature of the soccer ball changed quite drastically in each iteration of the World Cup. The accepted number of panels constantly changed (and is still changing to this day). Presented below are the years during the transition from local manufacturers to multi-national corporations.
Year Name Country Manufacturer Panels
1950 Duplo T Brazil Superball 12
1954 Swiss World Champ Switzerland Kost Sport 18
1958 Top Star Sweden Sydlader AB 24
1962 Crack Chile Curtiembres Salvador Caussade 18
1966 Challenge 4-Star England Slazenger 25
1970 Telstar Mexico Adidas 32
I hope you enjoy!

Regards,
Jules
Source: summary-example.qmd