Los mensajes de correo y algunos protocolos orientados a texto necesitan transportar bytes por canales que no siempre conservan todos los valores. Quoted-printable resuelve este problema manteniendo legible el ASCII común y representando bytes especiales mediante secuencias que empiezan con =. quopri en Python implementa esta codificación de transporte para cadenas de bytes y streams binarios.
Quoted-printable funciona mejor cuando el contenido es principalmente texto común y contiene pocos bytes no imprimibles o fuera de ASCII. Cuando los datos son binarios o tienen muchos valores especiales, Base64 suele ser más compacto y predecible. Esta guía explica las APIs de strings y archivos, el modo de cabecera, los charsets, los saltos suaves, la validación y la integración segura.
El tema complementa nuestras guías sobre mimetypes, fileinput, tempfile, filecmp y shlex.
Qué hace quoted-printable
El formato fue diseñado para contenido mayoritariamente legible en ASCII con una pequeña cantidad de bytes especiales. Letras, números y muchos signos permanecen visibles. Otros bytes se convierten en un signo igual seguido de dos dígitos hexadecimales.
Café → Caf=C3=A9El ejemplo supone que el texto fue convertido primero a bytes UTF-8. Quoted-printable no entiende caracteres Unicode directamente; transforma bytes.
Codificar bytes con encodestring
import quopri
bruto = "Hola, café".encode("utf-8")
codificado = quopri.encodestring(bruto)
print(codificado)La función recibe bytes y devuelve bytes. Si partes de un string de Python, elige explícitamente el charset. UTF-8 es habitual, aunque un sistema heredado puede declarar otro.
Decodificar con decodestring
datos = b"Hola, caf=C3=A9"
decodificado = quopri.decodestring(datos)
texto = decodificado.decode("utf-8")
print(texto)Eliminar la codificación de transporte y convertir bytes a texto son pasos diferentes. Primero decodifica quoted-printable y después aplica el charset indicado por el protocolo.
Charset y transferencia no son lo mismo
UTF-8 describe cómo los caracteres se convierten en bytes. Quoted-printable explica cómo esos bytes viajan por un canal textual. Una parte MIME puede usar simultáneamente charset=utf-8 y Content-Transfer-Encoding: quoted-printable.
Omitir una capa produce texto corrupto o secuencias hexadecimales visibles. Documenta qué capa recibe y devuelve cada función.
El parámetro quotetabs
encodestring() acepta quotetabs. Cuando es verdadero, también codifica espacios y tabs internos.
datos = b"campo con\ttabulador"
print(quopri.encodestring(datos, quotetabs=True))Los espacios y tabs al final de una línea siempre se codifican porque muchos sistemas eliminan whitespace final. La regla evita cambios silenciosos.
Saltos de línea suaves
Las líneas quoted-printable tienen un límite de longitud. El encoder puede colocar un signo igual al final de una línea física para indicar que el salto solo pertenece al transporte.
contenido lógico muy largo=
continúa aquíEl decoder elimina el salto suave y reconstruye la secuencia original. No proceses cada línea física de forma independiente sin conservar esta regla.
Codificar archivos y streams
Para entradas grandes utiliza quopri.encode() con objetos binarios.
import quopri
with open("mensaje.txt", "rb") as origen:
with open("mensaje.qp", "wb") as destino:
quopri.encode(origen, destino, quotetabs=False)El procesamiento por stream evita cargar todo el archivo en memoria. Ambos objetos deben abrirse en modo binario.
Decodificar archivos
with open("mensaje.qp", "rb") as origen:
with open("mensaje.txt", "wb") as destino:
quopri.decode(origen, destino)Un fallo de I/O puede dejar un archivo parcialmente escrito. Cuando la atomicidad importa, escribe en un temporal, valida el resultado y reemplaza el destino solamente después del éxito.
Modo de cabecera
La opción header=True aplica las convenciones de palabras Q-encoded en cabeceras MIME. Durante la decodificación, underscore se interpreta como espacio.
valor = b"Informe_mensual=C3=A9"
print(quopri.decodestring(valor, header=True))No actives este modo para el cuerpo común de un mensaje. Un underscore literal del cuerpo debe conservarse.
Usar email para mensajes completos
quopri es una herramienta de bajo nivel. Para mensajes completos, prefiere el paquete email, que entiende cabeceras, multiparts, charsets, adjuntos, políticas y plegado de líneas.
Construir límites MIME y cabeceras manualmente aumenta el riesgo de inyección, saltos inválidos y problemas de compatibilidad. Usa quopri directamente cuando controles una capa concreta o debas interoperar con un protocolo simple.
Cuándo elegir quoted-printable
- Texto principalmente ASCII con pocos caracteres acentuados.
- Contenido que debe permanecer parcialmente legible.
- Sistemas MIME heredados.
- Protocolos restringidos a líneas textuales.
- Archivos de diagnóstico destinados a inspección humana.
Para imágenes, PDFs, archivos comprimidos y contenido denso en bytes no ASCII, Base64 normalmente genera una representación más uniforme.
Comparación con Base64
Base64 aumenta el tamaño de manera relativamente constante y oculta toda la estructura visual. Quoted-printable conserva la mayor parte del ASCII, pero cada byte especial puede convertirse en tres caracteres. Un documento en un idioma con muchos caracteres no ASCII puede quedar más grande que en Base64.
Mide muestras reales cuando importen tamaño, legibilidad, compatibilidad o almacenamiento.
Entradas malformadas
Los datos externos pueden incluir signos igual incompletos, pares hexadecimales inválidos, líneas enormes o saltos extraños. El decoder es tolerante en varias situaciones, por lo que una devolución exitosa no demuestra que la fuente cumpla el estándar.
Aplica límites, registra anomalías cuando corresponda y valida el formato de nivel superior después de decodificar.
Seguridad y límites
Decodificar no ejecuta código, pero el resultado puede ser HTML, script, comando, ruta o adjunto malicioso. Trata los bytes según su contexto final. No renderices HTML de correo sin sanitización ni pases valores a shells o rutas directamente.
Limita el tamaño de entrada y salida. Un cuerpo enorme puede consumir memoria o disco aunque la transformación sea sencilla.
Normalización de líneas
El correo de Internet usa CRLF; los archivos locales pueden usar LF o CRLF. Evita normalizar antes de la decodificación porque los saltos suaves dependen de la representación original.
Después de recuperar el cuerpo, aplica la política de saltos correspondiente al contenido.
Helpers con charset explícito
def codificar_texto(texto: str, charset: str = "utf-8") -> bytes:
bruto = texto.encode(charset, errors="strict")
return quopri.encodestring(bruto, quotetabs=False)
def decodificar_texto(datos: bytes, charset: str = "utf-8") -> str:
bruto = quopri.decodestring(datos)
return bruto.decode(charset, errors="strict")El modo estricto evita reemplazos silenciosos. Captura UnicodeError para informar que el charset declarado no coincide.
Streams en memoria
from io import BytesIO
origen = BytesIO(b"texto con acento: \xc3\xa7")
destino = BytesIO()
quopri.encode(origen, destino, quotetabs=False)
resultado = destino.getvalue()BytesIO es útil en pruebas y en APIs que entregan objetos similares a archivos.
Prueba de ida y vuelta
def test_round_trip():
original = "café y Python".encode("utf-8")
codificado = quopri.encodestring(original)
restaurado = quopri.decodestring(codificado)
assert restaurado == originalAñade casos con espacios finales, tabs, líneas largas, bytes nulos, underscores de cabecera, entrada vacía y todos los charsets soportados.
Salida temporal y reemplazo atómico
Las herramientas de producción deben escribir en un archivo temporal del mismo sistema de archivos. Después de validar tamaño y contenido, reemplaza atómicamente el destino. Así un fallo no deja datos corruptos bajo el nombre final.
Diseñar una interfaz de comandos
Para automatizaciones duraderas crea una interfaz con argparse. Puede elegir codificación o decodificación, modo de cabecera, tamaño máximo, origen, destino y charset.
Errores frecuentes
- Pasar
strdonde se esperanbytes. - Eliminar quoted-printable y olvidar el charset.
- Usar
header=Truesobre un cuerpo. - Confundir underscore literal con espacio de cabecera.
- Elegir quoted-printable para grandes archivos binarios.
- Procesar líneas por separado y perder soft breaks.
- Suponer que la salida es segura.
- Escribir directamente sobre el destino definitivo.
Buenas prácticas
- Mantén las transformaciones en bytes hasta conocer el charset.
- Usa
emailpara mensajes completos. - Prefiere streams para entradas grandes.
- Limita tamaños codificados y decodificados.
- Valida el formato final por separado.
- Activa header únicamente para cabeceras Q-encoded.
- Prueba whitespace final y líneas largas.
- Compara con Base64 usando datos representativos.
Conclusión
quopri en Python implementa quoted-printable para bytes y streams. Es especialmente útil para contenido MIME mayoritariamente textual que debe atravesar canales restringidos sin perder por completo la legibilidad.
La transformación es sencilla, pero la integración correcta exige separar capas: charset y transferencia son conceptos distintos, las cabeceras usan reglas especiales y la salida necesita validación. Consulta la documentación oficial de quopri y la RFC 2045 para los requisitos completos.







