0. Introduction
Sending emails is one of the most practical features in a Django project. At first, it may look like a small utility, but in real applications email is used everywhere: account registration, password reset, order confirmation, contact forms, newsletter messages, notifications, alerts, and admin communication. Because of that, learning how Django handles email is an important step in becoming comfortable with real-world web development.
In this tutorial, we will study email sending in Django with a deep explanation. We will begin with the basic configuration, then send a simple email, then move to better practices such as HTML emails, EmailMessage, reply_to, reusable helper functions, error handling, and development versus production setup. The goal is not just to copy a code snippet, but to understand the logic behind Django’s email system and how to use it correctly in real projects.
1. Why Email Matters in a Django Application
Many beginner projects focus on pages, models, and forms, but real websites do much more than display content. They interact with users. Email is one of the main communication channels between a website and its users.
For example, a Django site may need to:
- send a welcome message after signup
- send a contact form message to the site owner
- send a password reset email
- send an order receipt
- notify an administrator of an important event
- send a confirmation link
- alert users when something changes in their account
This means email is not an optional skill. It is part of real application development.
When Django sends an email, it acts as a bridge between your application and an email delivery system. Your Django code prepares the message, and then an email backend handles the actual delivery.
2. Django’s Email System: The Big Idea
Django provides a built-in email framework in django.core.mail. This framework makes it easier to send emails without manually dealing with low-level SMTP code everywhere in your project.
There are three important ideas to understand:
First idea: Django does not magically send emails alone
Django needs an email backend. This backend defines how the email is handled. For example:
- print the email in the terminal for development
- send it through an SMTP server in production
- use a file backend for testing
- use a third-party service in real deployment
Second idea: development and production are different
When you are learning or testing locally, you usually do not want real emails to be sent. Instead, it is safer to display them in the console. That lets you inspect the subject, body, sender, and recipients without contacting a real mail server.
In production, however, you need a real SMTP server or email provider.
Third idea: email sending is application logic
Sending email is not just a technical step. It is part of the business logic of your application. For example, after a user fills a contact form, your application decides to notify the admin. After an order is placed, your application decides to send a receipt. So email sending is often triggered inside views, forms, signals, background tasks, or custom services.
3. Basic Email Configuration in Django
Before sending any email, Django must know which backend to use.
Open settings.py and start with the simplest development configuration.
Development configuration: console backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'noreply@example.com'This backend does not send real emails. Instead, every email is printed in the terminal where the Django server is running.
This is perfect for learning because it allows you to test email logic safely and immediately.
Why DEFAULT_FROM_EMAIL matters
This setting defines the default sender address used by your application. Even if you send a message from different parts of the project, it is good to centralize the default sender in settings.
This makes your code cleaner and easier to maintain.
4. Your First Email with send_mail()
The simplest way to send an email in Django is with send_mail().
Example view
from django.http import HttpResponse
from django.core.mail import send_mail
from django.conf import settings
def send_test_email(request):
send_mail(
subject='Test Email from Django',
message='Hello! This is a test email sent from a Django project.',
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=['admin@example.com'],
fail_silently=False,
)
return HttpResponse("Test email sent.")5. Deep Explanation of send_mail()
Let us study each argument carefully.
subject
This is the title of the email. It is what the recipient sees first in their inbox.
message
This is the plain text content of the email.
from_email
This is the sender address. We usually use a trusted site email from settings rather than hardcoding it in every view.
recipient_list
This must be a list, even if there is only one recipient.
fail_silently=False
This is very important. When set to False, Django raises exceptions if the email fails. This is useful during development because you want to know when something is broken.
If it were True, Django would hide delivery errors, which might make debugging harder.
6. Add a URL for Testing
To test the example above, add a URL.
urls.py
from django.urls import path
from .views import send_test_email
urlpatterns = [
path('send-test-email/', send_test_email, name='send_test_email'),
]Now run the server:
python manage.py runserverThen open:
http://127.0.0.1:8000/send-test-email/If you are using the console backend, you will see the email content printed in the terminal.
This is a very useful first step because it proves that:
- Django email code works
- your view is connected
- the backend is configured correctly
7. Why the Console Backend Is So Useful
Many beginners rush to connect a real SMTP provider too early. But the console backend is often better at the learning stage because it separates two things:
- your application logic
- the external delivery system
If your code is wrong, the console backend helps you catch it without worrying about SMTP credentials or external mail errors. It is a very clean debugging environment.
You should think of it as a safe simulation of email sending.
8. Sending Email from a Contact Form
A common real-world case is sending email after a user submits a contact form. The form collects data, Django validates it, and then the site owner receives a notification.
Here is a simple example.
forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=150)
message = forms.CharField(widget=forms.Textarea)views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from .forms import ContactForm
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
email = form.cleaned_data['email']
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
full_message = f"""
New contact form message
Name: {name}
Email: {email}
Subject: {subject}
Message:
{message}
"""
send_mail(
subject=f"Contact Form: {subject}",
message=full_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=['admin@example.com'],
fail_silently=False,
)
messages.success(request, "Your message has been sent successfully.")
return redirect('contact')
else:
form = ContactForm()
return render(request, 'contact.html', {'form': form})9. Why We Use cleaned_data
Notice that we do not take values directly from request.POST after the form is validated. Instead, we use:
form.cleaned_data['name']This is important because cleaned_data contains values that have already passed Django’s validation system.
For example:
EmailFieldchecks email format- required fields are enforced
- values are normalized when needed
So the cleaned data is more trustworthy than raw request data.
10. Why DEFAULT_FROM_EMAIL Is Better Than the User’s Email
A very common beginner mistake is this:
from_email = emailmeaning the submitted user email is directly used as the sender address.
That may appear logical, but in real hosting environments it is often a bad idea. Many SMTP servers reject emails if the sender is not one of your verified domains. It can also increase spam risk.
A better pattern is:
- use your own verified website email as the sender
- put the user’s email inside the message body
- optionally use
reply_to
This is more professional and more reliable.
11. Using EmailMessage for More Control
send_mail() is easy and useful, but sometimes you need more control. Django provides EmailMessage for that purpose.
Example
from django.core.mail import EmailMessage
from django.conf import settings
def send_custom_email():
email = EmailMessage(
subject='Custom Email',
body='This email was sent using EmailMessage.',
from_email=settings.DEFAULT_FROM_EMAIL,
to=['admin@example.com'],
)
email.send(fail_silently=False)
12. Why Use EmailMessage?
EmailMessage is more flexible than send_mail().
It allows you to do things such as:
- add
cc - add
bcc - define
reply_to - attach files
- control headers
- prepare more advanced messages
You can think of send_mail() as a shortcut for simple situations, while EmailMessage is the better tool when your email logic becomes more serious.
13. Adding reply_to
Suppose a visitor submits a contact form using their email address. You want the site owner to be able to click “Reply” and respond directly to that visitor.
That is where reply_to becomes useful.
Example
from django.core.mail import EmailMessage
from django.conf import settings
def send_contact_email(name, email, subject, message):
body = f"""
New contact form message
Name: {name}
Email: {email}
Subject: {subject}
Message:
{message}
"""
email_message = EmailMessage(
subject=f"Contact Form: {subject}",
body=body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=['admin@example.com'],
reply_to=[email],
)
email_message.send(fail_silently=False)This approach is much better than pretending the message was sent directly from the user.
14. HTML Emails in Django
Plain text email is simple and reliable, but sometimes you want a more professional look with headings, bold text, links, and styled content. For that, Django supports HTML emails.
The easiest way is using send_mail() with html_message.
Example
from django.core.mail import send_mail
from django.conf import settings
send_mail(
subject='Welcome to Our Website',
message='This is the plain text version of the email.',
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=['user@example.com'],
html_message="""
<h1>Welcome!</h1>
<p>Thank you for joining our website.</p>
<p><a href="https://example.com">Visit our site</a></p>
""",
fail_silently=False,
)15. Why Send Both Plain Text and HTML?
This is an important professional practice.
Some email clients display HTML well, but others may prefer or require plain text. Also, plain text provides a fallback version and improves compatibility.
So a good email often includes:
- a plain text body
- an HTML body
The user sees the best version their email client can handle.
16. Using EmailMultiAlternatives
For more structured HTML emails, Django provides EmailMultiAlternatives.
Example
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
def send_welcome_email(user_email):
subject = 'Welcome to Our Website'
text_content = 'Thank you for joining our website.'
html_content = """
<h1>Welcome to Our Website</h1>
<p>Thank you for joining our website.</p>
<p>We are glad to have you with us.</p>
"""
email = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user_email],
)
email.attach_alternative(html_content, "text/html")
email.send()17. Deep Explanation of EmailMultiAlternatives
This class is useful when you want to send multiple versions of the same message.
The plain text body is the default body. Then you add the HTML version as an alternative.
This design is professional because it preserves compatibility while giving modern email clients a richer display.
You should use it for:
- welcome emails
- order confirmations
- password reset branding
- newsletters
- account notifications
18. Creating Reusable Email Helper Functions
As your project grows, sending email directly inside every view becomes repetitive. A cleaner approach is to centralize email logic in helper functions.
For example, create a file called emails.py.
emails.py
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
def send_welcome_email(user_email, user_name):
subject = 'Welcome to Our Website'
text_content = f'Hello {user_name}, welcome to our website.'
html_content = f"""
<h2>Hello {user_name},</h2>
<p>Welcome to our website.</p>
<p>We are happy to have you here.</p>
"""
email = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user_email],
)
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)Now in a view, you simply call:
from .emails import send_welcome_email
send_welcome_email(user.email, user.username)19. Why Reusable Email Functions Are Better
This design has several advantages.
First, it keeps views cleaner. A view should ideally focus on the main request/response logic, not on building long email bodies.
Second, it makes maintenance easier. If you want to change an email subject or design later, you update one place instead of many.
Third, it improves testing. Reusable email functions are easier to isolate and verify.
So this is a good professional habit from the beginning.
20. Sending Emails with Templates
Hardcoding HTML directly in Python becomes messy when emails get longer. A better solution is to use Django templates for email bodies.
Example directory
templates/emails/welcome_email.html
templates/emails/welcome_email.txtwelcome_email.txt
Hello {{ user_name }},
Welcome to our website.
We are happy to have you here.welcome_email.html
<h2>Hello {{ user_name }},</h2>
<p>Welcome to our website.</p>
<p>We are happy to have you here.</p>Render and send them
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
def send_welcome_email(user_email, user_name):
subject = 'Welcome to Our Website'
text_content = render_to_string('emails/welcome_email.txt', {
'user_name': user_name
})
html_content = render_to_string('emails/welcome_email.html', {
'user_name': user_name
})
email = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user_email],
)
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)21. Why Email Templates Are a Very Good Practice
Email templates bring the same benefits as normal Django templates:
- cleaner separation between logic and presentation
- easier editing of the message design
- more reusable structure
- better readability
They are especially useful when:
- emails are long
- branding matters
- the same structure is reused in different messages
- designers or non-backend developers may edit the content
22. SMTP Configuration for Real Email Sending
When you move from development to production, you need a real SMTP configuration.
A common SMTP setup in settings.py looks like this:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'your_email@example.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = 'your_email@example.com'23. Deep Explanation of SMTP Settings
EMAIL_BACKEND
This switches Django from console mode to real SMTP delivery mode.
EMAIL_HOST
This is the SMTP server address.
EMAIL_PORT
Common values are:
587for TLS465for SSL in some setups
EMAIL_HOST_USER
The username used to authenticate with the mail server.
EMAIL_HOST_PASSWORD
The password or application-specific key.
EMAIL_USE_TLS
This enables encryption over TLS, which is common and recommended for SMTP on port 587.
24. Keep Credentials Out of Source Code
In real projects, you should not hardcode email passwords directly inside settings.py, especially if the code is stored in Git.
A better pattern is to use environment variables.
Example:
import os
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587))
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True') == 'True'
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')This is much safer and more professional.
25. Error Handling When Sending Emails
Sometimes email sending can fail because of:
- wrong SMTP credentials
- network problems
- blocked provider access
- invalid configuration
- mail server downtime
A common pattern is to wrap sending logic in try/except.
Example
from django.core.mail import send_mail
from django.conf import settings
def send_notification():
try:
send_mail(
subject='Notification',
message='This is a notification email.',
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=['admin@example.com'],
fail_silently=False,
)
return True
except Exception as e:
print("Email sending failed:", e)
return False26. Why Error Handling Matters
In a small test project, an email failure may be only a minor inconvenience. But in a real application, email may be critical.
Imagine:
- users cannot reset passwords
- order confirmations are not sent
- contact messages are lost
- administrators miss alerts
So email errors should be handled carefully. Sometimes you may show the user a friendly message. Sometimes you may log the problem. Sometimes you may retry later through a background task.
The right approach depends on the importance of the email in your application.
27. Django Messages Framework with Email Actions
If a user triggers an email-related action from a page, the messages framework can improve the experience.
Example
from django.contrib import messages
from django.shortcuts import redirect
def send_test_email_view(request):
try:
send_mail(
subject='Test Email',
message='This is a test email.',
from_email='noreply@example.com',
recipient_list=['admin@example.com'],
fail_silently=False,
)
messages.success(request, "Email sent successfully.")
except Exception:
messages.error(request, "Email could not be sent.")
return redirect('home')This makes the application feel much more user-friendly than failing silently.
28. Attaching Files to Emails
Sometimes you need to attach files such as PDFs, invoices, reports, or exported data. EmailMessage supports attachments.
Example
from django.core.mail import EmailMessage
from django.conf import settings
def send_report():
email = EmailMessage(
subject='Monthly Report',
body='Please find the report attached.',
from_email=settings.DEFAULT_FROM_EMAIL,
to=['manager@example.com'],
)
email.attach_file('/path/to/report.pdf')
email.send(fail_silently=False)29. Why File Attachments Need Care
Attachments are powerful, but they must be used carefully.
You should think about:
- file size
- security
- whether the file path exists
- whether the user is authorized to receive it
- whether sensitive files are exposed accidentally
So attachments are not just a technical feature. They also involve security and application design.
30. Sending Emails Asynchronously
One important real-world point is that sending email can take time. If you send an email directly inside a view, the user may have to wait for the SMTP request to complete before receiving the response page.
For a simple project, this may be acceptable. But in larger projects, a better approach is to send emails in the background using tools such as Celery and Redis.
That way:
- the view responds faster
- email sending is delegated to a background worker
- retries and monitoring are easier
For now, it is enough to understand that Django can send email synchronously by default, but serious projects often move this task to asynchronous workers.
31. Common Beginner Mistakes
Mistake 1: Using real SMTP too early
Start with the console backend first. It makes debugging easier.
Mistake 2: Hardcoding email credentials in source code
Use environment variables instead.
Mistake 3: Using the user’s address directly as from_email
Prefer your own verified sender and use reply_to.
Mistake 4: Ignoring exceptions
If email sending matters, do not hide failures without logging or handling them.
Mistake 5: Writing huge email HTML directly inside views
Use templates or reusable helper functions.
Mistake 6: Forgetting plain text fallback for HTML emails
Use both text and HTML when possible.
32. A Clean Real-World Example
Here is a cleaner structure for a welcome email.
emails.py
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
def send_welcome_email(user):
subject = 'Welcome to Our Website'
context = {
'user': user,
}
text_content = render_to_string('emails/welcome_email.txt', context)
html_content = render_to_string('emails/welcome_email.html', context)
email = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user.email],
)
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)
In a view
from django.shortcuts import render, redirect
from .emails import send_welcome_email
def register_view(request):
# Assume user creation logic is already done
user = ...
send_welcome_email(user)
return redirect('home')This is much cleaner than writing the entire email logic directly in the view.
33. What You Learned in This Tutorial
In this tutorial, you learned that sending email in Django is not just about calling a function. It begins with understanding the backend system and the difference between development and production. You learned how to use the console backend for safe testing, how send_mail() works for simple messages, and why EmailMessage and EmailMultiAlternatives give more control. You saw how to send plain text and HTML emails, why reply_to is often better than using the user’s email as the sender, and how reusable helper functions and templates make your project cleaner. You also learned the importance of SMTP settings, secure credential handling, and error management.
These are not isolated tricks. Together, they form a practical email architecture for real Django applications.
34. Conclusion
Email sending is one of the most useful and realistic skills in Django development. It connects your website to your users and turns your project from a simple web interface into an active communication system. Once you understand how Django handles email, you can build features like contact forms, notifications, verification emails, receipts, support messages, and account workflows with much more confidence.
The most important thing is to build this feature in a clean and professional way: start with the console backend, keep sender configuration centralized, use reusable helper functions, separate templates from logic, and think carefully about errors and delivery reliability. That is what makes your email system maintainable and production-ready.
Next, a very natural continuation would be Tutorial : Django Signals, because signals are often used to automatically trigger emails after certain actions such as user creation or model updates.
💬 Comments
No comments yet. Be the first to comment!
Login to comment.