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
- Send a quick message with send_mail
- Use EmailMessage for cc, bcc, and attachments
- Configure EMAIL_BACKEND (console vs SMTP)
- Preview emails in the console during development
- Send HTML email with EmailMultiAlternatives
- Render templated emails with render_to_string
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: 12 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: 13 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: 2Common 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
| Tool | When to Use It |
|---|---|
| send_mail(...) | Quick plain-text message |
| EmailMessage | cc, bcc, reply-to, attachments |
| EmailMultiAlternatives | Plain text + HTML in one message |
| render_to_string | Render a template into an email body |
| EMAIL_BACKEND | Console (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
- Previous: File & Image Uploads
- Next: The Caching Framework — Speed up your site with per-view, low-level, and fragment caching
- Quick reference: Django cheat sheet
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.