El módulo io define las interfaces centrales de entrada y salida de Python. Archivos abiertos con open(), buffers en memoria, wrappers de texto y muchos objetos entregados por sockets, compresión y bibliotecas externas siguen contratos basados en esta jerarquía.
Existen tres categorías principales: I/O de texto, I/O binario con buffering e I/O bruto. Comprender la diferencia evita errores de tipo, pérdida de datos por encoding, escrituras parciales, uso excesivo de memoria y sorpresas entre plataformas.
Streams y objetos file-like
Un stream es una secuencia de datos accesible mediante read(), write(), seek() y close(). Puede representar un archivo, memoria, pipe, socket, respuesta HTTP, contenido comprimido o implementación personalizada.
No todos los streams admiten todas las operaciones. Usa readable(), writable() y seekable() para consultar capacidades. Una operación no soportada puede lanzar io.UnsupportedOperation.
Texto y bytes son contratos distintos
Los streams de texto reciben y producen str. Los binarios reciben objetos bytes-like y producen bytes. Mezclar tipos genera TypeError.
with open("datos.txt", "w", encoding="utf-8") as archivo:
archivo.write("Hola")
with open("imagen.png", "wb") as archivo:
archivo.write(b"\x89PNG")
No uses modo texto para imágenes, ZIP, PDF o protocolos binarios. No uses modo binario para texto sin una política explícita de encoding y decoding.
Especifica el encoding
El encoding predeterminado de open() depende de la locale, salvo que UTF-8 Mode esté activo. Código que funciona en Linux puede fallar en Windows si omite encoding="utf-8".
with open("README.md", "r", encoding="utf-8") as archivo:
contenido = archivo.read()
Usa encoding="locale" cuando la locale forma parte deliberada del formato. Para APIs nuevas, UTF-8 explícito suele ser más predecible. Consulta codecs en Python.
EncodingWarning
Python puede avisar cuando una API depende del encoding predeterminado. Ejecuta con -X warn_default_encoding o define PYTHONWARNDEFAULTENCODING. Las funciones que reciben encoding=None pueden usar io.text_encoding().
import io
def leer_texto(ruta, encoding=None):
encoding = io.text_encoding(encoding)
with open(ruta, encoding=encoding) as archivo:
return archivo.read()
Traducción de newlines
El argumento newline controla los finales de línea. Con None, el modo universal reconoce \n, \r y \r\n y devuelve \n. Con string vacío, los reconoce pero los conserva. Un valor concreto restringe el terminador.
Para formatos que exigen bytes exactos, como firmas, hashes y protocolos, usa modo binario o configura newline de manera explícita.
Context managers y cierre
Los objetos de IOBase soportan with, garantizando el cierre incluso cuando ocurre una excepción.
with open("informe.txt", "w", encoding="utf-8") as archivo:
archivo.write("resultado\n")
Las operaciones sobre un stream cerrado suelen lanzar ValueError. Llamar close() varias veces está permitido, pero el objeto no debe reutilizarse.
flush() no es fsync()
flush() envía el buffer de Python a la capa subyacente, pero no garantiza persistencia física. Para durabilidad, haz flush y llama a os.fsync(), comprendiendo el coste y las garantías de la plataforma.
import os
with open("estado.txt", "w", encoding="utf-8") as archivo:
archivo.write("confirmado")
archivo.flush()
os.fsync(archivo.fileno())
Para actualizaciones atómicas, escribe en un archivo temporal del mismo filesystem y sustituye el destino después del éxito. Consulta tempfile en Python.
I/O bruto
RawIOBase representa acceso de bajo nivel a bytes. FileIO es la implementación de archivos. Las operaciones brutas pueden leer menos bytes que los solicitados o escribir solo parte del buffer.
import io
bruto = io.FileIO("datos.bin", "w")
try:
restante = memoryview(b"contenido")
while restante:
cantidad = bruto.write(restante)
if cantidad is None:
continue
restante = restante[cantidad:]
finally:
bruto.close()
La mayoría de aplicaciones debería preferir streams buffered, que reintentan operaciones apropiadas y ofrecen un contrato más predecible.
BufferedReader y BufferedWriter
BufferedReader lee bloques mayores y guarda bytes para llamadas posteriores. BufferedWriter acumula salida y la envía cuando el buffer se llena, durante flush(), seek o cierre.
El tamaño predeterminado está en io.DEFAULT_BUFFER_SIZE, aunque open() puede considerar el block size. Mide throughput, memoria y latencia antes de modificarlo.
read(), read1() y readinto()
read(size) puede realizar varias lecturas brutas. read1(size) usa como máximo una llamada. readinto(buffer) llena memoria preasignada y reduce allocations.
buffer = bytearray(64 * 1024)
with open("grande.bin", "rb") as archivo:
while cantidad := archivo.readinto(buffer):
procesar(memoryview(buffer)[:cantidad])
No conserves la view después de reutilizar el buffer sin copiar los datos.
BytesIO
BytesIO es un stream binario en memoria.
import io
stream = io.BytesIO()
stream.write(b"cabecera")
stream.seek(0)
print(stream.read())
getvalue() devuelve todos los bytes. getbuffer() expone una view modificable sin copia. Mientras exista, el objeto no puede redimensionarse ni cerrarse.
StringIO
StringIO es un stream de texto Unicode en memoria, útil para tests, informes y captura de salida.
import io
salida = io.StringIO()
print("primera línea", file=salida)
print("segunda línea", file=salida)
texto = salida.getvalue()
Para simular append, usa seek(0, io.SEEK_END). Al cerrar, el buffer se descarta.
TextIOWrapper
TextIOWrapper envuelve un stream binario buffered y gestiona encoding, decoding y newlines.
import io
bruto = open("datos.txt", "rb", buffering=0)
buffer = io.BufferedReader(bruto)
texto = io.TextIOWrapper(buffer, encoding="utf-8", errors="strict")
try:
print(texto.readline())
finally:
texto.close()
open(..., encoding=...) construye estas capas automáticamente.
Políticas de errores de encoding
errors="strict" lanza excepción y debería ser el default cuando importa la integridad. ignore elimina datos silenciosamente y rara vez es apropiado. replace inserta un marcador. Otros handlers sirven para contextos concretos.
No uses ignore solo para hacer que un archivo abra. Corrige el encoding o adopta una política documentada.
reconfigure()
TextIOWrapper.reconfigure() cambia encoding, errors, newline, line buffering y write-through. Encoding y newline no pueden cambiar después de leer porque el decoder ya tiene estado.
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace")
Cambiar un stream global afecta a todo el proceso. Hazlo solo durante la inicialización y respeta redirecciones.
seek() y tell() en texto
En binario, las posiciones son offsets de bytes. En texto, tell() devuelve un cookie opaco que incluye estado del decoder. Solo pasa valores obtenidos por tell() a seek(cookie).
No sumes números arbitrariamente a posiciones de texto. Para acceso por bytes, trabaja en la capa binaria.
Streams no bloqueantes
Un raw stream no bloqueante puede devolver None cuando no hay datos y escribir parcialmente. Las capas buffered y text pueden lanzar BlockingIOError.
El artículo anterior, select en Python, explica cómo esperar disponibilidad antes de repetir.
detach()
detach() separa y devuelve la capa subyacente. El wrapper exterior queda inutilizable.
buffer_binario = texto.detach()
Úsalo solo con transferencia de ownership clara. StringIO y BytesIO no tienen una capa inferior separable.
Descriptores existentes y closefd
Cuando open() o FileIO envuelve un descriptor entero, closefd=False evita que cerrar el stream cierre ese descriptor. Se necesitan reglas explícitas para evitar leaks o double close.
opener personalizado
El argumento opener controla cómo se crea el descriptor y puede permitir apertura relativa a un directorio.
import os
raiz_fd = os.open("datos", os.O_RDONLY)
try:
def opener(path, flags):
return os.open(path, flags, dir_fd=raiz_fd)
with open("archivo.txt", "r", encoding="utf-8", opener=opener) as archivo:
print(archivo.read())
finally:
os.close(raiz_fd)
Valida nombres y path traversal. Un opener no crea automáticamente una política segura.
Compresión y file-like objects
Muchos módulos aceptan objetos file-like, permitiendo componer capas.
import gzip
import io
origen = io.BytesIO(datos_comprimidos)
with gzip.GzipFile(fileobj=origen, mode="rb") as archivo:
contenido = archivo.read(1_000_000)
La descompresión en memoria sigue necesitando límites. Consulta gzip en Python.
Protocolos Reader y Writer en Python 3.14
Python 3.14 añade io.Reader[T] y io.Writer[T] para tipar funciones que solo requieren read() o write().
from io import Reader, Writer
def copiar_texto(origen: Reader[str], destino: Writer[str]) -> None:
while bloque := origen.read(8192):
destino.write(bloque)
El tipado estructural admite archivos, StringIO e implementaciones compatibles.
Thread safety y reentrancia
FileIO sigue las garantías de las syscalls. Los objetos binarios buffered protegen estructuras con locks y pueden ser llamados por varios threads. TextIOWrapper no es thread-safe.
Los objetos buffered no son reentrantes. Hacer I/O sobre el mismo stream desde un signal handler puede lanzar RuntimeError. El artículo de signal en Python recomienda handlers mínimos.
Rendimiento
Buffered I/O ofrece rendimiento predecible y suele ser preferible al raw I/O. Text I/O añade coste de codec. Para archivos grandes, procesa bloques y evita read() sin límite.
StringIO y BytesIO son eficientes para tamaños moderados, pero almacenan todo en RAM. Usa archivos temporales o streaming para volúmenes grandes.
Pruebas recomendadas
Prueba texto y bytes, Unicode, encoding inválido, newlines, archivo vacío, lecturas y escrituras parciales, non-blocking, seek/tell, cierre repetido, detach, views activas de BytesIO, StringIO, ownership de descriptor y límites de memoria.
Errores comunes
Los fallos frecuentes son omitir encoding, mezclar str y bytes, usar errors="ignore", asumir escritura completa en raw I/O, leer archivos enormes de una vez, confundir flush con persistencia, calcular offsets de texto como bytes y cerrar un descriptor propiedad de otra capa.
Conclusión
io proporciona los contratos centrales de streams de Python. Texto, bytes, acceso bruto, buffering y memoria son capas distintas que pueden combinarse explícitamente.
Especifica encoding, prefiere buffering, valida retornos de raw streams, usa context managers y limita recursos. Consulta la documentación oficial de io y la documentación oficial de open().







