UnicodeEncodeError: 'charmap' codec can't encode: solución

Un script de Python que funciona sin problemas en otra máquina falla en Windows al imprimir texto con emojis o símbolos:

Traceback (most recent call last):
  File "informe.py", line 3, in <module>
    print("\U0001F550 \u2264")
  File "...\Lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f550' in position 0: character maps to <undefined>

El carácter puede ser otro (por ejemplo '\u2264', el símbolo ≤). Con la misma raíz, leer un archivo JSON con open() falla en sentido contrario:

UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 in position 15: character maps to <undefined>

Los datos no tienen nada malo: el JSON es válido y el texto que quieres enviar a otro sistema también. Falla solo el paso local de leer del disco o escribir en la salida estándar con una codificación que no soporta esos caracteres.

Por qué pasa

Para convertir texto en bytes (y al revés) Python necesita una codificación. Si no se le indica una, usa la del sistema. En muchas instalaciones de Windows en español, esa codificación es cp1252, que no contiene emojis ni símbolos como ≤. El nombre charmap del error viene del códec que implementa cp1252, no de tu código.

Hay dos puntos donde ocurre:

  • Archivos. En Python 3.14 y anteriores, open() sin encoding usa la codificación local. Un archivo escrito en UTF-8 se lee entonces como cp1252: algunos bytes (como 0x90) no existen en esa tabla y producen UnicodeDecodeError; el resto se decodifica sin error, pero mal. Por ejemplo, canción se lee como canción.
  • Salida estándar. Desde Python 3.6, la consola interactiva de Windows usa UTF-8, pero la documentación aclara que ese cambio solo se aplica a la consola interactiva, no cuando la salida se redirige a un archivo o a una tubería. Si el mismo script se ejecuta desde una tarea programada, un servicio, un sistema de CI, con > o |, o desde un terminal que entrega una tubería en lugar de una consola, print() vuelve a usar la codificación local.

Esto cambia en Python 3.15: según la documentación, el modo UTF-8 pasa a estar activado por defecto (PEP 686) y open() usa UTF-8 si no se indica encoding. En 3.14 y versiones anteriores el problema sigue vigente.

Solución

  1. Comprueba qué codificación está usando tu Python.

    import locale, sys
    print(sys.version.split()[0], sys.stdout.encoding, locale.getpreferredencoding(False), sys.flags.utf8_mode)

    Si ejecutas esto con la salida redirigida (por ejemplo, añadiendo | more al comando de Python) y ves cp1252 y 0, estás en el caso descrito.

  2. En archivos, indica siempre la codificación. Es la solución más robusta porque no depende del equipo donde se ejecute:

    import json
    
    with open("datos.json", encoding="utf-8") as f:
        datos = json.load(f)
    
    with open("salida.json", "w", encoding="utf-8") as f:
        json.dump(datos, f, ensure_ascii=False, indent=2)

    Pasar encoding de forma explícita hace que el código se comporte igual en todas las versiones de Python y en todos los equipos.

  3. En la salida estándar, elige una de estas opciones.

    • Dentro del script, al inicio (disponible desde Python 3.7):
      import sys
      sys.stdout.reconfigure(encoding="utf-8")
    • Con una variable de entorno, solo para la salida estándar. En PowerShell, para la sesión actual:
      $env:PYTHONIOENCODING = 'utf-8'
      python informe.py
      En cmd es set PYTHONIOENCODING=utf-8. Para dejarla definida en tu usuario puedes usar setx PYTHONIOENCODING utf-8; solo afecta a los procesos que abras después.
    • Con el modo UTF-8 de Python (3.7 o posterior), que además hace que open() use UTF-8 por defecto:
      python -X utf8 informe.py
      Equivale a definir PYTHONUTF8=1.
  4. En una tarea programada o un servicio, configura el entorno de esa tarea. No hereda el entorno de tu terminal. La forma más simple es poner la opción en el propio comando, por ejemplo python -X utf8 C:\proyectos\mi-app\informe.py, o definir la variable de entorno en la configuración de la tarea o del servicio.

Elegir entre PYTHONIOENCODING y el modo UTF-8 es una cuestión de alcance. PYTHONIOENCODING solo cambia stdin, stdout y stderr. El modo UTF-8 también cambia la codificación por defecto de open(), por lo que un script que lea archivos antiguos en cp1252 sin indicar encoding pasaría a leerlos mal. Si tienes archivos así, indícalo en cada open() con encoding="cp1252".

Cómo verificar que quedó resuelto

Prueba siempre con la salida redirigida, porque en la consola interactiva el error puede no aparecer ni siquiera antes de la corrección:

python informe.py | more
python -c "import json; print(json.load(open('datos.json', encoding='utf-8')))" | more

El primer comando debe imprimir todo el texto, con emojis y símbolos, sin Traceback. El segundo debe cargar el JSON. Para confirmar la codificación de la salida:

python -X utf8 -c "import sys; print(sys.stdout.encoding)" | more

Debe mostrar utf-8. Si el script se ejecuta como tarea programada o servicio, revisa su registro después de la primera ejecución real.

Variantes y errores relacionados

Error al escribir un archivo

Un open("salida.txt", "w") sin encoding produce el mismo UnicodeEncodeError al escribir texto que cp1252 no contiene. La solución es la del paso 2.

UnicodeDecodeError: 'utf-8' codec can't decode byte ...

Es el caso inverso: el archivo no está en UTF-8, sino en cp1252 o en otra codificación, y lo abres como UTF-8. Indica la codificación real (encoding="cp1252") o convierte el archivo a UTF-8.

PYTHONIOENCODING con manejador de errores

La sintaxis es codificación:manejador. Por ejemplo, PYTHONIOENCODING=cp1252:replace evita la excepción sustituyendo cada carácter no representable por ?. Es útil para que un script no se detenga, pero pierde información: no lo uses si necesitas conservar el texto.

La variable no cambia nada en la consola interactiva

En Windows, PYTHONIOENCODING se ignora en los búferes de la consola interactiva (salvo que también se defina PYTHONLEGACYWINDOWSSTDIO); sí afecta a archivos y tuberías redirigidos. Por eso conviene probar con la salida redirigida, como se explica arriba.

Python 3.15 o posterior

Con el modo UTF-8 activado por defecto, el código sin encoding lee y escribe UTF-8. Si necesitas el comportamiento anterior, puedes desactivarlo con PYTHONUTF8=0 o -X utf8=0, o pedir la codificación local de forma explícita con encoding="locale" (disponible desde Python 3.10).

Resumen

  • 'charmap' codec can't encode/decode en Windows suele significar que Python usó cp1252 por defecto; los datos no están dañados.
  • Al leer o escribir archivos, pasa siempre encoding="utf-8" a open().
  • Para la salida, usa sys.stdout.reconfigure(encoding="utf-8"), PYTHONIOENCODING=utf-8 o python -X utf8 (equivale a PYTHONUTF8=1).
  • El fallo aparece sobre todo con la salida redirigida (tareas, servicios, CI, tuberías); en tareas y servicios configura la opción en su propio entorno.
  • Desde Python 3.15 el modo UTF-8 es el predeterminado; en 3.14 y anteriores sigue haciendo falta lo anterior.

Referencias

Comentarios