Python Basics: Decode Quoted-Printable Bytes Before Text
Mail transport encoding and text encoding are separate layers. Decode quoted-printable into bytes first, then interpret those bytes using the declared character set.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need Bash, a text editor, the named runtime, and standard mktemp, rm, and rmdir utilities. Run Python without -O so assertions remain enabled. The examples use only controlled local data; generated fixture directories are cleaned automatically.
Keep the same terminal open. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-quopri.py:
import quopri
wire = b"caf=C3=A9=20menu\n"
raw = quopri.decodestring(wire)
text = raw.decode("utf-8")
assert text == "caf\u00e9 menu\n"
print("Decoded text:", text.rstrip("\n"))
encoded = quopri.encodestring(text.encode("utf-8"), quotetabs=True)
assert quopri.decodestring(encoded) == raw
print("Encoded:", encoded.decode("ascii").rstrip("\n"))
print("Body underscore:", quopri.decodestring(b"one_two").decode("ascii"))
print("Header underscore:", quopri.decodestring(b"one_two", header=True).decode("ascii"))
assert quopri.decodestring(b"long=\nline") == b"longline"
print("Soft line break: joined")Run it:
python3 -B demonstrate-quopri.pyObserved output:
Decoded text: café menu
Encoded: caf=C3=A9=20menu
Body underscore: one_two
Header underscore: one two
Soft line break: joinedRead the result
The UTF-8 bytes C3 A9 become the accented character only after text decoding. quotetabs also encodes spaces and tabs. In header mode an underscore represents a space; in body mode it stays literal. An equals sign followed by a newline marks a soft break.
Apply it carefully
Use the email package to parse complete messages and encoded header words; quopri handles only this transfer-encoding layer. Do not guess UTF-8 for every message. The decoder is permissive, so successful decoding is not strict validation of all MIME syntax.
The official reference describes the behavior and options. These checks cover the fixture above rather than every input or platform.
Clean up
Remove the saved source, return to the starting directory, then remove the empty practice directory:
rm -- demonstrate-quopri.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect its remaining contents rather than deleting unknown files.
Acknowledgements
This post was created with assistance from AI.