Updated September 2026. The original version of this post used the smtpd module. Python 3.12 removed that module, so I rewrote the post for aiosmtpd.
If you run python -m smtpd on Python 3.12 or later, you get this:
/usr/bin/python3: No module named smtpdThe smtpd module was deprecated in Python 3.6 and removed in Python 3.12 (PEP 594). The official replacement is aiosmtpd, an asyncio-based SMTP server from the aio-libs project. It is a pip install away, and it has the same “print every email to the terminal” mode that made smtpd useful for local testing.
smtpdis gone in Python 3.12;aiosmtpdis the drop-in replacement for local email testing.
The short version:
# Old (Python 3.11 and earlier) python -m smtpd -n -c DebuggingServer localhost:1025 # New (Python 3.12 and later) pip install aiosmtpd python -m aiosmtpd -n -l localhost:1025 The rest of this post shows the new command, a script to send a test email, and a pytest fixture that lets your tests check the emails your code sends.
Why a local SMTP server?
When your app sends email (sign-up confirmations, password resets, alerts), you do not want to send real email while you develop. A local SMTP server accepts the message on your machine and prints it. Nothing leaves your laptop, and you see exactly what your code sent: headers, body, and recipients.
Step 1: Start the debug server
Install aiosmtpd in your virtual environment:
pip install aiosmtpd Start the server:
python -m aiosmtpd -n -l localhost:1025 What the options do:
-n: Do not try to switch to thenobodyuser. The server tries this by default, and it fails when you are not root. The oldsmtpdcommand had the same flag.-l localhost:1025: The host and port to listen on. Without this flag,aiosmtpdlistens onlocalhost:8025.
You do not need a -c flag. The default handler is aiosmtpd.handlers.Debugging, which is the replacement for the old DebuggingServer class. It prints every message to the terminal.
Step 2: Send a test email
Save this as send_email.py:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Test Email"
msg["From"] = "testing_email@xyz.com"
msg["To"] = "recipient_test@abc.com"
msg.set_content("This is a test email\nHello World")
with smtplib.SMTP("localhost", 1025) as server:
server.send_message(msg)smtplib is still in the standard library. Only the server side moved to a separate package. The script uses EmailMessage, the modern API in the email package, and a with block so that the connection closes even when sending fails.
Run it:
python send_email.py The terminal that runs the server prints the message:
---------- MESSAGE FOLLOWS ----------
Subject: Test Email
From: testing_email@xyz.com
To: recipient_test@abc.com
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
X-Peer: ('::1', 51530, 0, 0)
This is a test email
Hello World
------------ END MESSAGE ------------If you used the old DebuggingServer, you will notice one change. The old server printed every line as a bytes literal (b'Subject: Test Email'). aiosmtpd prints plain text.
Step 3: Catch emails in your tests
The terminal output is fine when you test by hand. In automated tests, you want to assert on the email itself. aiosmtpd includes a Controller that runs the server in a background thread, so you can start and stop it from a pytest fixture.
import smtplib
from email.message import EmailMessage
import pytest
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Message
class Inbox(Message):
def __init__(self):
super().__init__()
self.messages = []
def handle_message(self, message):
self.messages.append(message)
@pytest.fixture
def inbox():
handler = Inbox()
controller = Controller(handler, hostname="localhost", port=1025)
controller.start()
yield handler
controller.stop()
def send_welcome_email(to):
msg = EmailMessage()
msg["Subject"] = "Welcome"
msg["From"] = "app@example.com"
msg["To"] = to
msg.set_content("Thanks for signing up.")
with smtplib.SMTP("localhost", 1025) as server:
server.send_message(msg)
def test_welcome_email(inbox):
send_welcome_email("new_user@example.com")
assert len(inbox.messages) == 1
email = inbox.messages[0]
assert email["To"] == "new_user@example.com"
assert email["Subject"] == "Welcome"
assert "Thanks for signing up." in email.get_payload()$ pytest -q test_email.py . [100%] 1 passed in 0.05s How it works:
Messageis a handler inaiosmtpdthat parses each incoming email into anemail.message.Messageobject. TheInboxsubclass keeps each parsed message in a list.controller.start()does not return until the server accepts connections, so the test does not need a sleep.controller.stop()shuts the server down after the test, so the port is free for the next test.
In a real project, send_welcome_email is your application code. Point its SMTP host and port at localhost:1025 in your test settings.
If you are still on Python 3.11 or earlier
The old command still works on those versions:
python -m smtpd -n -c DebuggingServer localhost:1025 It prints a DeprecationWarning on 3.10 and 3.11. I still recommend aiosmtpd, because the same command then works when you upgrade.
Wrapping up
The fix for the missing smtpd module is one package and one changed command: pip install aiosmtpd, then python -m aiosmtpd -n -l localhost:1025. For tests, the Controller class gives you an in-process inbox you can assert on. If you have feedback, leave a comment below, or send me an email if you’d rather not comment publicly.
Want new posts in your inbox? Subscribe by email or follow the RSS feed.
If you want to learn how to build projects like Docker or BitTorrent, or understand the internals of tools like Git and grep by recreating them in your language of choice, I recommend Code Crafters. It’s a solid platform for learning by building.
If you liked this post, you can support my work by buying me a coffee. Your support means a lot. If you share this on X, tag me @muhammad_o7. You can also follow me on LinkedIn.