Quoted-printable, MIME and email encoding
Why email needs its own encoding layer, how encoded words in headers work, when a body is base64 rather than quoted-printable, and the line length limits that cause silent breakage.
Email was designed for 7-bit text
SMTP originally carried only 7-bit ASCII lines of at most 1000 characters. MIME adds headers that describe how the real content is encoded, which is why a modern message has several layers of encoding before the text you read.
Subject: =?UTF-8?Q?Caf=C3=A9_meeting?=
From: =?UTF-8?B?QW5hIFBlem5pY2s=?= <[email protected]>
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
MIME-Version: 1.0
The caf=C3=A9 is open until 18:00 =
on weekdays.Qmeans quoted-printable,Bmeans base64, and the encoded word may not exceed 75 characters.- Adjacent encoded words are joined; the underscore in a Q-word represents a space.
- A soft line break is a trailing
=with no content after it — the continuation is joined with no space inserted. - Non-ASCII in a header must be encoded; a raw accented byte is rejected or mangled in transit.
Quoted-printable or base64
| Encoding | Expansion | Readable as text | Best for |
|---|---|---|---|
| 7bit / 8bit | None | Yes | Pure ASCII content |
| quoted-printable | Modest, most bytes literal | Mostly | Mostly-ASCII text with accents |
| base64 | About 33 percent | No | Binary attachments, non-Latin text |
| binary | None | No | Only with an extension such as CHUNKING |
import quopri, base64
raw = "The café is open until 18:00 on weekdays.".encode("utf-8")
encoded = quopri.encodestring(raw)
print(encoded.decode())
# The caf=C3=A9 is open until 18:00 on weekdays.
assert quopri.decodestring(encoded) == raw
# base64 for the same payload
b64 = base64.encodebytes(raw).decode()
assert base64.decodebytes(b64.encode()) == rawQuoted-printable keeps a mostly-ASCII message readable in a raw dump, which is why it is preferred for text bodies. Base64 is more compact for binary data because it has no per-byte worst case of tripling.
Line length and the traps
- Encoded lines must stay under 76 characters; longer lines are broken, and base64 padding can be split across lines in ways some decoders reject.
- A soft break in quoted-printable adds no space, so re-wrapping text changes the meaning.
- Mixing transfer encodings inside one message requires a multipart structure with a boundary.
- Attachment filenames are themselves encoded words, and older clients only understand a subset of charsets.
- A message that declares UTF-8 but encodes the body as 8bit will be corrupted by any relay that normalises to 7-bit.
from email.message import EmailMessage
msg = EmailMessage()
msg["Subject"] = "Café meeting" # the library encodes the header for you
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg.set_content("The café is open until 18:00.")
with open("report.pdf", "rb") as f:
msg.add_attachment(f.read(), maintype="application",
subtype="pdf", filename="report.pdf")
# let the library choose the transfer encoding
assert msg["Content-Transfer-Encoding"] in ("quoted-printable", "base64")💡
Never hand-write MIME. The header encoding, line folding, boundary selection and charset declaration interact, and a small mistake produces a message that looks fine to one client and arrives as a wall of equals signs in another.
FAQ
Why does my email body show equals signs?
A client failed to decode quoted-printable, or the decoder was given the wrong charset. The
=C3=A9 sequences are the UTF-8 bytes of an accented letter.Should I use base64 for a plain text body?
Only when the text is mostly non-Latin. Base64 hides the content from inspection and adds a third more bytes, which is why quoted-printable is the default for text.
Related
Escape sequences across languages Compression vs encoding: gzip, deflate and Brotli
Last refreshed 2026-09-18.