Sending Email

Reviewed & published by Brayan K

Django's email framework is a thin wrapper over Python's SMTP tools that lets you compose and deliver messages — plain text or HTML — through a configurable backend with one function call.

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll fire off a quick message with send_mail, take full control with EmailMessage, preview mail in the console backend, and build templated HTML emails using EmailMultiAlternatives and render_to_string.

What You'll Learn in This Lesson

1 send_mail and the Email Backend

The quickest way to send mail is send_mail(subject, message, from_email, recipient_list). Where that mail goes is decided by EMAIL_BACKEND. In development you use the console backend, which prints the full email to your terminal instead of contacting a real server.

# settings.py — print emails to the terminal during development
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "noreply@example.com"

# accounts/views.py
from django.core.mail import send_mail

def welcome(request):
    sent = send_mail(
        subject="Welcome to LearnCodingFast",
        message="Thanks for signing up, Ada!",
        from_email="noreply@example.com",
        recipient_list=["ada@example.com"],
        fail_silently=False,
    )
    # send_mail returns the number of messages sent (1 here)
    return HttpResponse(f"Sent {sent} email(s)")
# send_mail is the quick one-liner: subject, body, from address, and a list
# of recipients. Here we build the exact payload Django would hand to the
# backend, then "send" it through the console backend (which just prints).

def send_mail(subject, message, from_email, recipient_list):
    payload = {
        "subject": subject,
        "body": message,
        "from": from_email,
        "to": recipient_list,
    }
    # The console backend prints the message instead of sending it
    print("Content-Type: text/plain")
    print("Subject:", payload["subject"])
    print("From:", payload["from"])
    print("To:", ", ".join(payload["to"]))
    print()
    print(payload["body"])
    return 1   # send_mail returns the number of messages sent

sent = send_mail(
    subject="Welcome to LearnCodingFast",
    message="Thanks for signing up, Ada!",
    from_email="noreply@example.com",
    recipient_list=["ada@example.com"],
)
print("---")
print("messages sent:", sent)

# EXPECTED:
# Content-Type: text/plain
# Subject: Welcome to LearnCodingFast
# From: noreply@example.com
# To: ada@example.com
#
# Thanks for signing up, Ada!
# ---
# messages sent: 1

# ✅ Expected output:
# Content-Type: text/plain
# Subject: Welcome to LearnCodingFast
# From: noreply@example.com
# To: ada@example.com
#
# Thanks for signing up, Ada!
# ---
# messages sent: 1

2 EmailMessage — cc, bcc, and Attachments

When you need more than a basic message, build an EmailMessage object. You can add cc, bcc, reply_to, custom headers, and attachments, then call .send(). It's the class send_mail uses under the hood.

# billing/views.py
from django.core.mail import EmailMessage

def email_invoice(request, invoice):
    email = EmailMessage(
        subject="Your invoice",
        body="See the attached PDF.",
        from_email="billing@example.com",
        to=["ada@example.com"],
        cc=["records@example.com"],
        bcc=["audit@example.com"],
        reply_to=["support@example.com"],
    )
    email.attach("invoice.pdf", invoice.as_bytes(), "application/pdf")
    email.send()   # returns 1 on success
    return HttpResponse("Invoice emailed")
# EmailMessage gives you full control: multiple recipients, cc, bcc, reply-to,
# and attachments. It's an object you build up, then call .send() on. Here we
# construct it and render the headers it would produce.

class EmailMessage:
    def __init__(self, subject, body, from_email, to, cc=None, bcc=None):
        self.subject = subject
        self.body = body
        self.from_email = from_email
        self.to = to or []
        self.cc = cc or []
        self.bcc = bcc or []
        self.attachments = []

    def attach(self, filename, content_type):
        self.attachments.append((filename, content_type))

    def message(self):
        lines = [
            f"Subject: {self.subject}",
            f"From: {self.from_email}",
            f"To: {', '.join(self.to)}",
        ]
        if self.cc:
            lines.append(f"Cc: {', '.join(self.cc)}")
        lines.append(f"Attachments: {len(self.attachments)}")
        return "\n".join(lines)

email = EmailMessage(
    subject="Your invoice",
    body="See the attached PDF.",
    from_email="billing@example.com",
    to=["ada@example.com"],
    cc=["records@example.com"],
)
email.attach("invoice.pdf", "application/pdf")

print(email.message())

# EXPECTED:
# Subject: Your invoice
# From: billing@example.com
# To: ada@example.com
# Cc: records@example.com
# Attachments: 1

# ✅ Expected output:
# Subject: Your invoice
# From: billing@example.com
# To: ada@example.com
# Cc: records@example.com
# Attachments: 1

3 HTML Email with Templates

For rich email you send both a plain-text and an HTML version using EmailMultiAlternatives. You render each body from a template with render_to_string, set the plain text as the body, and attach the HTML as an alternative. Clients pick whichever they support.

# accounts/emails.py
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

def send_welcome(user):
    context = {"name": user.first_name}

    # Render both versions from templates
    text_body = render_to_string("email/welcome.txt", context)
    html_body = render_to_string("email/welcome.html", context)

    msg = EmailMultiAlternatives(
        subject="Welcome!",
        body=text_body,                 # plain-text fallback
        from_email="noreply@example.com",
        to=[user.email],
    )
    msg.attach_alternative(html_body, "text/html")   # rich version
    msg.send()
# For HTML email you render a template to a string, then attach it as an
# alternative to the plain-text body with EmailMultiAlternatives. We simulate
# render_to_string with a tiny template formatter and build both parts.

def render_to_string(template_name, context):
    # Stand-in for Django's render_to_string(template, context)
    templates = {
        "email/welcome.txt": "Hi {name}, welcome aboard!",
        "email/welcome.html": "<h1>Hi {name}</h1><p>Welcome aboard!</p>",
    }
    return templates[template_name].format(**context)

context = {"name": "Ada"}
text_body = render_to_string("email/welcome.txt", context)
html_body = render_to_string("email/welcome.html", context)

# EmailMultiAlternatives: plain text is the body, HTML is attached alternative
print("PLAIN PART:")
print(text_body)
print()
print("HTML PART:")
print(html_body)

# EXPECTED:
# PLAIN PART:
# Hi Ada, welcome aboard!
#
# HTML PART:
# <h1>Hi Ada</h1><p>Welcome aboard!</p>

# ✅ Expected output:
# PLAIN PART:
# Hi Ada, welcome aboard!
#
# HTML PART:
# <h1>Hi Ada</h1><p>Welcome aboard!</p>

🎯 Your Turn: Count the Recipients

send_mail returns how many messages were sent. Fill in the blank so the return value equals the number of recipients in the list.

# 🎯 YOUR TURN: send_mail returns the NUMBER of messages successfully sent.
# We send to two recipients in one call. Replace ___ so the return value
# equals the count of recipients.

def send_mail(subject, message, from_email, recipient_list):
    # ... Django delivers to every address in recipient_list ...
    return len(___)

count = send_mail(
    "Newsletter",
    "Hello!",
    "news@example.com",
    ["ada@example.com", "ben@example.com"],
)
print("messages sent:", count)

# EXPECTED OUTPUT:
# messages sent: 2

Common Errors (And How to Fix Them)

❌ Connection refused / SMTP error in development

You're using the SMTP backend with no mail server running locally.

✅ Fix: set EMAIL_BACKEND to the console backend while developing so mail just prints.

❌ recipient_list must be a list

You passed a bare string for the recipients, e.g. recipient_list="ada@example.com".

✅ Fix: always wrap addresses in a list — ["ada@example.com"].

❌ My HTML email shows raw tags as text

You put HTML into the plain message argument instead of attaching it as an alternative.

✅ Fix: use EmailMultiAlternatives and attach_alternative(html, "text/html").

📋 Quick Reference

ToolWhen to Use It
send_mail(...)Quick plain-text message
EmailMessagecc, bcc, reply-to, attachments
EmailMultiAlternativesPlain text + HTML in one message
render_to_stringRender a template into an email body
EMAIL_BACKENDConsole (dev) vs SMTP (production)

🏆 Mini Challenge: An Order-Confirmation Builder

Write a function that renders an order-confirmation subject and body from a context dictionary, then returns the full payload send_mail would deliver.

❓ Frequently Asked Questions

Lesson complete — your app can talk to the world!

You can send a quick message with send_mail, take full control with EmailMessage, preview mail through the console backend, and build templated HTML email with EmailMultiAlternatives and render_to_string.

Practice quiz

Which function is the simplest way to send a plain-text email?

  • mail()
  • send_mail()
  • deliver()
  • EmailMessage.send_now()

Answer: send_mail(). send_mail() is a convenience function taking subject, message, from, and recipient list.

What arguments does send_mail() take in order?

  • recipient_list, subject, message, from_email
  • from_email, subject, recipient_list, message
  • subject, message, from_email, recipient_list
  • message, subject, recipient_list, from_email

Answer: subject, message, from_email, recipient_list. send_mail(subject, message, from_email, recipient_list) is the standard signature.

Which class gives full control over cc, bcc, and attachments?

  • EmailMessage
  • send_mail
  • SimpleMail
  • MailSender

Answer: EmailMessage. EmailMessage is the underlying class exposing cc, bcc, reply-to, headers, and attachments.

Which EMAIL_BACKEND prints emails to the terminal in development?

  • smtp.EmailBackend
  • filebased.EmailBackend
  • dummy.EmailBackend
  • console.EmailBackend

Answer: console.EmailBackend. django.core.mail.backends.console.EmailBackend prints the full email to the console.

Which class lets you send both plain-text and HTML versions?

  • EmailMultiAlternatives
  • HtmlEmail
  • send_mail with html=True
  • EmailMessage.as_html()

Answer: EmailMultiAlternatives. EmailMultiAlternatives lets you attach_alternative(html, 'text/html') alongside the plain body.

What does render_to_string return?

  • An HttpResponse
  • The rendered template as a Python string
  • A template object
  • A file path

Answer: The rendered template as a Python string. render_to_string renders a template with a context and returns the output as a string, ideal for email bodies.

How do you add the HTML version on an EmailMultiAlternatives message?

  • msg.set_html(html)
  • msg.html = html
  • msg.attach_alternative(html, 'text/html')
  • msg.add_html(html)

Answer: msg.attach_alternative(html, 'text/html'). attach_alternative(html_content, 'text/html') adds the HTML alternative.

Which backend do you switch to for sending real mail in production?

  • console.EmailBackend
  • dummy.EmailBackend
  • locmem.EmailBackend
  • smtp.EmailBackend

Answer: smtp.EmailBackend. The SMTP backend, configured with EMAIL_HOST, EMAIL_PORT, and credentials, sends real mail.

When should you use EmailMessage instead of send_mail?

  • When you need attachments, cc, or bcc
  • For every email
  • Only in development
  • Never — send_mail does everything

Answer: When you need attachments, cc, or bcc. Use EmailMessage when you need extra features like attachments, cc, bcc, or custom headers.

What does send_mail() return?

  • The EmailMessage object
  • None
  • The number of messages successfully sent
  • The recipient list

Answer: The number of messages successfully sent. send_mail() returns the number of successfully delivered messages.

Continue this course

Frequently asked questions

What is the difference between send_mail and EmailMessage?

send_mail is a convenience function for the common case: you pass a subject, a plain-text body, a from address, and a list of recipients in one call. EmailMessage is the underlying class that gives you full control — cc, bcc, reply-to, custom headers, and attachments. Use send_mail for simple notifications and EmailMessage when you need any of those extra features.

How do I see emails during development without sending real mail?

Set EMAIL_BACKEND to django.core.mail.backends.console.EmailBackend. With the console backend Django prints the full email — headers and body — to your terminal instead of contacting a mail server. This is perfect for development. When you deploy you switch EMAIL_BACKEND to the SMTP backend and supply EMAIL_HOST, EMAIL_PORT, and credentials.

How do I send an HTML email?

Use EmailMultiAlternatives. You create it with a plain-text body, then call attach_alternative(html_content, 'text/html') to add the HTML version. Email clients that support HTML show the rich version, and clients that do not fall back to the plain text. You usually build both bodies with render_to_string so the content comes from templates.

What does render_to_string do for emails?

render_to_string takes a template name and a context dictionary and returns the rendered output as a Python string instead of an HttpResponse. That makes it ideal for emails: you keep your email layout in a normal Django template, render it with the recipient's data, and hand the resulting string to send_mail or attach it as the HTML alternative on an EmailMultiAlternatives message.