Email messages and older text-oriented protocols sometimes need to transport bytes through channels that do not preserve every value safely. Quoted-printable solves this problem by keeping ordinary ASCII readable while representing special bytes with sequences that begin with =. Python quopri implements this transport encoding for byte strings and binary streams.
Quoted-printable works best when the content is mostly ordinary text with relatively few non-printable or non-ASCII bytes. When the data is binary or contains many special bytes, Base64 is usually more compact and predictable. This guide explains string and stream APIs, header mode, character encodings, soft line breaks, validation, and secure integration.
It complements our guides to Python mimetypes, fileinput, tempfile, filecmp, and shlex.
What quoted-printable does
The format was designed for content that is mostly readable ASCII but contains a small number of special bytes. Letters, digits, and many punctuation marks remain visible. Other bytes become an equals sign followed by two hexadecimal digits.
Café → Caf=C3=A9The example assumes the text was first converted to UTF-8 bytes. Quoted-printable does not understand Unicode characters directly; it transforms bytes.
Encoding bytes with encodestring
import quopri
raw = "Hello, café!".encode("utf-8")
encoded = quopri.encodestring(raw)
print(encoded)The function accepts bytes and returns bytes. When starting from a Python string, select the character encoding explicitly. UTF-8 is common, but legacy systems may declare a different charset.
Decoding with decodestring
data = b"Hello, caf=C3=A9!"
decoded = quopri.decodestring(data)
text = decoded.decode("utf-8")
print(text)Removing the transport encoding and converting bytes to text are separate operations. Decode quoted-printable first, then apply the charset declared by the surrounding protocol.
Charset and transfer encoding are different
UTF-8 describes how characters become bytes. Quoted-printable describes how those bytes travel through a text-safe channel. A MIME part can therefore use charset=utf-8 and Content-Transfer-Encoding: quoted-printable at the same time.
Skipping either layer produces corrupted text or visible hexadecimal escapes. Integration code should document exactly which layer each function accepts and returns.
The quotetabs argument
encodestring() accepts quotetabs. When true, embedded spaces and tabs are encoded as well.
data = b"field with\ttab"
print(quopri.encodestring(data, quotetabs=True))Spaces and tabs at the end of a line are always encoded because many transport systems trim trailing whitespace. This rule protects data from silent modification.
Soft line breaks
Quoted-printable lines have a length limit. An encoder can place an equals sign at the end of a physical line to indicate that the break is only for transport.
very long logical content=
continues hereThe decoder removes the soft break and reconstructs the original byte sequence. Do not decode every physical line independently without preserving this rule.
Encoding files and streams
For large inputs, use quopri.encode() with binary input and output objects.
import quopri
with open("message.txt", "rb") as source:
with open("message.qp", "wb") as target:
quopri.encode(source, target, quotetabs=False)Stream processing avoids loading the entire file into memory. Both objects must be opened in binary mode because the module works with bytes.
Decoding files
with open("message.qp", "rb") as source:
with open("message.txt", "wb") as target:
quopri.decode(source, target)I/O failures can leave a partially written destination. When atomicity matters, write to a temporary file, validate the result, flush it, and replace the final path only after success.
Header mode
The header=True option applies conventions used by MIME Q-encoded headers. During decoding, an underscore is interpreted as a space.
value = b"Monthly_report=C3=A9"
print(quopri.decodestring(value, header=True))Do not enable this mode for an ordinary message body. A literal underscore in body text should remain an underscore, while in an encoded header word it can represent a space.
Use the email package for complete messages
quopri is a low-level transformation tool. For complete email messages, prefer Python’s email package, which understands headers, MIME multipart structure, character sets, attachments, policies, and line folding.
Hand-building MIME boundaries and headers increases the risk of header injection, invalid line endings, and compatibility failures. Use quopri directly when you control one specific byte layer or must interoperate with a simple legacy protocol.
When quoted-printable is a good choice
The format is suitable for:
- mostly ASCII text with a few accented characters;
- content that should remain partly readable;
- legacy MIME systems;
- protocols restricted to textual lines;
- diagnostic files where human inspection helps.
For images, PDFs, archives, and dense non-ASCII content, Base64 normally produces a smaller and more uniform representation.
Quoted-printable versus Base64
Base64 expands data by a relatively constant ratio and hides all visual structure. Quoted-printable preserves most ASCII but can expand each special byte into three characters. A natural-language document containing many non-ASCII characters may therefore become larger than its Base64 equivalent.
Measure real samples when message size, readability, gateway compatibility, or storage costs matter.
Malformed input
External data can contain incomplete equals signs, invalid hexadecimal pairs, excessively long lines, or unusual line endings. The decoder is tolerant in several situations, so a successful return does not prove that the source was standards-compliant.
Set input limits, record anomalies when appropriate, and validate the higher-level format after decoding.
Security and resource limits
Decoding does not execute code, but the result may be malicious HTML, script, a command, a path, or a dangerous attachment. Treat the decoded bytes according to their final context. Do not render email HTML without sanitization and never pass decoded values directly into shell commands or filesystem paths.
Limit the size of both the encoded input and decoded output. Large inputs can consume memory or disk even when the transformation itself is simple.
Line-ending normalization
Internet email uses CRLF line endings, while local files may use LF or CRLF. Avoid normalizing lines before quoted-printable decoding because soft breaks depend on the transport representation.
After recovering the body bytes, apply any content-specific newline policy.
Explicit charset helpers
def encode_text(text: str, charset: str = "utf-8") -> bytes:
raw = text.encode(charset, errors="strict")
return quopri.encodestring(raw, quotetabs=False)
def decode_text(data: bytes, charset: str = "utf-8") -> str:
raw = quopri.decodestring(data)
return raw.decode(charset, errors="strict")Strict error handling prevents silent replacement. An application can catch UnicodeError and report that the declared charset does not match the bytes.
In-memory streams with BytesIO
from io import BytesIO
source = BytesIO(b"text with accent: \xc3\xa7")
target = BytesIO()
quopri.encode(source, target, quotetabs=False)
result = target.getvalue()BytesIO is convenient in tests and when another library exposes a file-like object instead of a physical file.
Round-trip testing
def test_round_trip():
original = "café and Python".encode("utf-8")
encoded = quopri.encodestring(original)
restored = quopri.decodestring(encoded)
assert restored == originalAdd cases for trailing spaces, tabs, long lines, null bytes, header underscores, empty input, and every supported charset.
Temporary output and atomic replacement
Production conversion tools should write to a temporary file in the destination filesystem. Validate size and content, then atomically replace the final file. This prevents a crash or disk error from leaving a corrupted result under the expected name.
Designing a command-line tool
Although the module can be used in small scripts, durable automation should expose an explicit argparse interface. The command can select encode or decode mode, header behavior, maximum size, input, output, and charset.
Common mistakes
- Passing
strwhere the API expectsbytes. - Removing quoted-printable but forgetting the character set.
- Using
header=Trueon a body. - Confusing a literal underscore with a header space.
- Choosing quoted-printable for large binary files.
- Processing physical lines independently and losing soft breaks.
- Assuming decoded output is safe.
- Writing directly over the final destination.
Best practices
- Keep transformations in bytes until the charset is known.
- Use the
emailpackage for complete messages. - Prefer streams for large inputs.
- Limit encoded and decoded sizes.
- Validate the final content type independently.
- Use header mode only for Q-encoded headers.
- Test trailing whitespace and long lines.
- Compare with Base64 using representative data.
Conclusion
Python quopri provides a direct implementation of quoted-printable encoding for byte strings and streams. It is particularly useful for mostly textual MIME content that must travel through restricted channels while remaining partly readable.
The transformation is simple, but correct integration requires clear layers: charset and transfer encoding are different, headers use special underscore rules, and decoded output still needs validation. Consult the official quopri documentation and RFC 2045 for full interoperability requirements.







