Python quopri: Quoted-Printable

Published on: August 8, 2026
Reading time: 6 minutes
Digital message representing quoted-printable encoding with Python quopri

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=A9

The 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 here

The 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 == original

Add 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 str where the API expects bytes.
  • Removing quoted-printable but forgetting the character set.
  • Using header=True on 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 email package 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Digital file icon representing MIME types with Python mimetypes
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python mimetypes: MIME Types

    Learn Python mimetypes to identify MIME types, extensions, and encodings safely in uploads, downloads, email, and web APIs.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026
    Binary search and sorted lists with Python bisect
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python bisect: Search Sorted Lists

    Learn Python bisect for binary search, ordered insertion, duplicate ranges, thresholds, and safe sorted-list design.

    Ler mais

    Tempo de leitura: 4 minutos
    08/08/2026
    Code and packaged files with Python importlib.resources
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python importlib.resources: Practical Guide

    Learn Python importlib.resources to access packaged files safely across wheels, installed applications, and custom import loaders.

    Ler mais

    Tempo de leitura: 5 minutos
    07/08/2026
    Keyboard and data flow representing multiple-file processing with Python fileinput
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fileinput: Read Multiple Files

    Learn Python fileinput to read multiple files or stdin, track line numbers, open compressed logs, and rewrite content safely with

    Ler mais

    Tempo de leitura: 7 minutos
    07/08/2026
    Code editor with numbered lines representing the Python linecache module
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python linecache: Read Lines by Number

    Learn Python linecache to read source lines by number, manage cached files, refresh changed code, and support traceback and import

    Ler mais

    Tempo de leitura: 7 minutos
    07/08/2026
    Binary data representing internal serialization with Python marshal
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python marshal: Internal Serialization

    Learn Python marshal to serialize internal types, control format versions, and reject code objects when they are unnecessary.

    Ler mais

    Tempo de leitura: 6 minutos
    06/08/2026