StackPractices
beginner Por Mathias Paulenko

Leer y Escribir Archivos en Python, Node.js y Bash

Cómo leer y escribir archivos de forma segura en Python, Node.js, Bash y Java, con escritura atómica y streaming.

Visión General

Pocas tareas de E/S aparecen tan seguido como leer y escribir archivos: cargar configuración, procesar logs, exportar informes, persistir estado. Hacerlo de forma segura implica manejar bien la codificación y cerrar siempre el descriptor del archivo.

Los archivos son la interfaz común entre un programa y el almacenamiento persistente. Guardar preferencias, revisar logs del servidor, exportar un CSV: las reglas no cambian — abrir el archivo, hacer el trabajo y liberar el descriptor aunque algo falle. Los runtimes actuales se encargan del búfer, la codificación y la limpieza por ti, pero saber qué pasa por debajo te sirve cuando persigues una lectura lenta o un archivo corrupto.

Abajo encontrarás la lectura/escritura idiomática para archivos de texto en Python, Node.js y Bash, más el patrón de streaming para archivos demasiado grandes para la memoria.

Cuándo Usar

Saca esta receta cuando:

  • Cargas archivos de configuración o datos al arrancar. Consulta Parse JSON para archivos de config estructurados.
  • Generas informes, exportaciones o logs para auditoría y análisis
  • Procesas texto línea a línea (CSV, logs, fixtures)
  • Persistes pequeñas cantidades de estado sin una base de datos
  • Lees y escribes archivos de configuración JSON o YAML
  • Haces streaming de archivos de logs grandes sin cargarlos completamente en memoria
  • Creas archivos temporales para procesamiento intermedio en pipelines de datos. Consulta Call REST API para descargar datos remotos.

No uses esta receta cuando el archivo es demasiado grande para el búfer o necesitas un pipeline de transformación continuo: ese caso lo cubre Stream Processing.

Solución

Python

En Python, with envuelve el archivo en un gestor de contexto: el descriptor se cierra solo aunque el bloque lance una excepción. Pasa siempre encoding="utf-8" para evitar valores por defecto dependientes de la plataforma.

# Escribir
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("Hola, archivo!\n")

# Leer
with open("notes.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)

JavaScript

Bajo node:fs/promises, Node.js trae una API basada en promesas que nunca bloquea el event loop: innegociable para un servidor que atiende peticiones concurrentes.

import { readFile, writeFile } from "node:fs/promises";

await writeFile("notes.txt", "Hola, archivo!\n", "utf-8");

const content = await readFile("notes.txt", "utf-8");
console.log(content);

Bash

En Bash, el trabajo con archivos pasa por la redirección de shell. El operador > sobrescribe el archivo destino y >> añade al final: la opción más rápida para escrituras pequeñas dentro de scripts.

# Escribir (sobrescribir) y añadir
echo "Hola, archivo!" > notes.txt
echo "Otra línea" >> notes.txt

# Leer
cat notes.txt

Explicación

  • Python se apoya en la sentencia with (un gestor de contexto) para que el archivo se cierre solo aunque algo falle. open() recibe una cadena de modo: "r" lee, "w" escribe y trunca, "a" añade, "x" crea solo si el archivo no está. Pasa siempre encoding="utf-8".
  • JavaScript: fs/promises hace el trabajo pesado. Las síncronas readFileSync/writeFileSync congelan el event loop, así que mantenlas fuera de los manejadores de peticiones. Para archivos grandes, createReadStream() procesa los datos en bloques.
  • Bash trabaja con redirección: > sobrescribe, >> añade al final y cat imprime el contenido. Para análisis estructurado, pasa la salida de cat por jq para JSON o awk para CSV.

Las escrituras atómicas siguen una secuencia fija que protege a los lectores de archivos a medio escribir:

Secuencia de escritura atómica: escribir los datos en un archivo temporal dentro del mismo filesystem, hacer flush y fsync a disco, y renombrarlo sobre la ruta destino — una operación atómica. Los lectores siempre ven o el archivo viejo o el nuevo completo, nunca una escritura parcial.

Para convertir el contenido de un archivo en datos estructurados, consulta Parsear JSON.

Errores que te vas a encontrar

ErrorDe dónde vieneSolución
FileNotFoundError / ENOENTruta ausente o un typocomprueba pathlib.Path.exists() / fs.access() primero
PermissionError / EACCESarchivo de solo lectura o falta el bit de ejecución en un directorio padrearregla permisos (chmod); reintentar no sirve
IsADirectoryError / EISDIRabriste un directorio como si fuera archivoverifica path.is_file() antes de abrir
UnicodeDecodeErrorel archivo no es UTF-8detecta la codificación real (file -i, chardet) o pásala explícita
ENOSPCdisco lleno a mitad de escriturala escritura atómica mantiene el original intacto; haz visible el error y alerta

Variantes

LenguajeLeerEscribirAñadir
Pythonopen(p).read()open(p, "w")open(p, "a")
JavaScriptreadFile(p)writeFile(p, data)appendFile(p, data)
Bashcat p> p>> p

Lo que funciona

  • Define siempre la codificación: un utf-8 explícito evita valores por defecto dependientes de la plataforma que corrompen caracteres no ASCII en Windows o macOS.
  • Usa gestores de contexto / APIs async: with en Python, fs/promises en Node, para evitar fugas de descriptores y bloqueos del event loop. La limpieza corre igual si una excepción golpea a mitad del bloque.
  • Comprueba que la ruta existe: un archivo ausente debe degradar con elegancia, no tumbar el proceso. En Python, pathlib.Path.exists() te lo dice por adelantado; en Node, fs.access() o fs.stat() hacen el trabajo.
  • Procesa archivos grandes en streaming: no cargues gigabytes en memoria cuando una línea cada vez basta. Python te da for line in f, Node tiene readline o createReadStream, y Bash tiene while read line.
  • Escribe de forma atómica: deja los bytes en un archivo temporal y luego renombra; un fallo a mitad de escritura deja el original intacto.
  • Usa rutas absolutas en scripts: las rutas relativas se rompen en cuanto cambia el directorio de trabajo. Resuélvelas a forma absoluta — pathlib en Python, path.resolve() en Node — antes del primer open().
  • Establece permisos restrictivos en archivos sensibles: ¿secretos en un archivo de configuración? Ciérralo a solo su propietario (chmod 600).

Errores Comunes

  • Olvidar cerrar el descriptor: provoca fugas de descriptores y acaba agotando el límite del proceso; usa siempre with o try/finally.
  • Bloquear el event loop en Node: no pongas readFileSync dentro de manejadores de peticiones; una sola lectura síncrona congela todo el servidor para todos los usuarios concurrentes.
  • Codificación incorrecta: lee UTF-8 como ASCII y el texto no inglés se vuelve mojibake en tus logs y en la UI.
  • Sobrescribir con >: usar > en lugar de >> en Bash borra el archivo en silencio, sin deshacer ni confirmación.
  • Ignorar errores: un archivo ausente o un fallo de permisos merece un manejador de verdad, no un catch vacío. Regístralo y falla de forma controlada.
  • Leer archivos completos en memoria: cargar un archivo de logs de 10 GB en una cadena tumbará tu proceso. Comprueba primero el tamaño del archivo, o procesa en streaming cualquier cosa de más de unos megabytes.
  • Escribir en el mismo archivo que estás leyendo: sobrescribe tu entrada sobre la marcha y puedes truncarla a mitad de lectura: datos perdidos.

Soluciones Avanzadas

Python: Escritura atómica con pathlib y manejo de errores

import os
import tempfile
from pathlib import Path
from typing import Any

def safe_write(path: str | Path, data: str, encoding: str = 'utf-8') -> None:
    """Escribe texto atómicamente: archivo temporal + renombrado. Seguro ante crashes."""
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    fd, tmp_path = tempfile.mkstemp(
        dir=path.parent, suffix='.tmp', prefix=path.name
    )
    try:
        with os.fdopen(fd, 'w', encoding=encoding) as f:
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp_path, path)
    except Exception:
        Path(tmp_path).unlink(missing_ok=True)
        raise

def safe_read(path: str | Path, encoding: str = 'utf-8',
              default: str | None = None) -> str | None:
    """Lee texto con un valor por defecto para archivos ausentes."""
    path = Path(path)
    if not path.exists():
        return default
    try:
        return path.read_text(encoding=encoding)
    except PermissionError:
        raise PermissionError(f"No se puede leer {path}: permiso denegado")
    except UnicodeDecodeError as e:
        raise UnicodeDecodeError(
            e.encoding, e.object, e.start, e.end,
            f"El archivo {path} no es {encoding} válido"
        )

def read_lines_lazy(path: str | Path, encoding: str = 'utf-8') -> list[str]:
    """Lee las líneas del archivo, descartando vacías y saltos finales."""
    path = Path(path)
    with path.open('r', encoding=encoding) as f:
        return [line.rstrip('\n\r') for line in f if line.strip()]

def write_json_atomic(path: str | Path, data: Any, indent: int = 2) -> None:
    """Serializa JSON y escribe atómicamente."""
    import json
    text = json.dumps(data, indent=indent, ensure_ascii=False, default=str)
    safe_write(path, text)

# Uso
# safe_write('/etc/app/config.yaml', 'key: value\n')
# content = safe_read('/etc/app/config.yaml', default='key: default\n')
# write_json_atomic('/data/state.json', {'users': 42, 'active': 10})

Node.js: lectura/escritura en streaming con recuperación de errores

const fs = require('fs');
const { pipeline } = require('stream');
const { promisify } = require('util');
const { createReadStream, createWriteStream } = fs;
const pipe = promisify(pipeline);

async function streamFile(srcPath, destPath, transformFn) {
    const tmpPath = destPath + '.tmp';
    const readStream = createReadStream(srcPath, { encoding: 'utf-8' });
    const writeStream = createWriteStream(tmpPath, { encoding: 'utf-8' });

    let lineBuffer = '';
    const lineTransform = new (require('stream').Transform)({
        transform(chunk, encoding, callback) {
            lineBuffer += chunk;
            const lines = lineBuffer.split('\n');
            lineBuffer = lines.pop();
            for (const line of lines) {
                const result = transformFn(line);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
        flush(callback) {
            if (lineBuffer) {
                const result = transformFn(lineBuffer);
                if (result !== null) this.push(result + '\n');
            }
            callback();
        },
    });

    try {
        await pipe(readStream, lineTransform, writeStream);
        await fs.promises.rename(tmpPath, destPath);
    } catch (err) {
        try { await fs.promises.unlink(tmpPath); } catch {}
        throw err;
    }
}

async function readLines(path) {
    const content = await fs.promises.readFile(path, 'utf-8');
    return content.split('\n').filter(l => l.trim());
}

async function appendLine(path, line) {
    await fs.promises.appendFile(path, line + '\n', 'utf-8');
}

// Uso
// streamFile('input.log', 'output.log', line => line.toUpperCase());
// const lines = await readLines('config.txt');
// await appendLine('app.log', `[${new Date().toISOString()}] Iniciado`);

Java: Operaciones de archivo NIO con escritura atómica

import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ArrayList;

public class FileOps {

    // Escritura atómica: archivo temporal + Files.move con ATOMIC_MOVE
    public static void atomicWrite(Path path, String content) throws IOException {
        Path parent = path.getParent();
        if (parent != null) Files.createDirectories(parent);
        Path tmp = Files.createTempFile(parent, path.getFileName().toString(), ".tmp");
        try {
            Files.writeString(tmp, content, StandardCharsets.UTF_8);
            Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE,
                       StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
            Files.deleteIfExists(tmp);
            throw e;
        }
    }

    // Lectura segura con valor por defecto
    public static String safeRead(Path path, String defaultValue) {
        if (!Files.exists(path)) return defaultValue;
        try {
            return Files.readString(path, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new UncheckedIOException("Error al leer " + path, e);
        }
    }

    // Leer todas las líneas
    public static List<String> readLines(Path path) throws IOException {
        return Files.readAllLines(path, StandardCharsets.UTF_8);
    }

    // Añadir una línea
    public static void appendLine(Path path, String line) throws IOException {
        String entry = line + System.lineSeparator();
        Files.writeString(path, entry,
            StandardCharsets.UTF_8,
            StandardOpenOption.CREATE,
            StandardOpenOption.APPEND);
    }

    // Procesar líneas con try-with-resources
    public static void processLines(Path path, LineHandler handler) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                handler.handle(line);
            }
        }
    }

    @FunctionalInterface
    public interface LineHandler {
        void handle(String line) throws IOException;
    }
}

// Uso
// FileOps.atomicWrite(Path.of("/etc/app/config.yaml"), "key: value\n");
// String config = FileOps.safeRead(Path.of("config.yaml"), "key: default\n");
// FileOps.processLines(Path.of("large.log"), line -> {
//     if (line.contains("ERROR")) System.err.println(line);
// });

Bash: Operaciones de archivo seguras con verificación de errores

#!/usr/bin/env bash
set -euo pipefail

# Escritura segura: escribir a archivo temporal, luego renombrado atómico
safe_write() {
    local file="$1"
    local content="$2"
    local tmp="${file}.tmp.$$"
    local dir
    dir="$(dirname "$file")"
    mkdir -p "$dir"
    printf '%s' "$content" > "$tmp"
    mv "$tmp" "$file"
}

# Lectura segura: verificar existencia primero, dar un valor por defecto
safe_read() {
    local file="$1"
    local default="${2:-}"
    if [[ -f "$file" && -r "$file" ]]; then
        cat "$file"
    else
        printf '%s' "$default"
    fi
}

# Añadir con timestamp (para registro)
log_append() {
    local file="$1"
    local message="$2"
    local timestamp
    timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    printf '[%s] %s\n' "$timestamp" "$message" >> "$file"
}

# Leer archivo línea por línea con manejo de errores
read_lines() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        echo "Error: $file no encontrado" >&2
        return 1
    fi
    while IFS= read -r line || [[ -n "$line" ]]; do
        echo "$line"
    done < "$file"
}

# Crear archivo con permisos restrictivos (para secretos)
create_secret_file() {
    local file="$1"
    local content="$2"
    # Crear con permisos 600 directamente
    (umask 077; printf '%s' "$content" > "$file")
    echo "Creado $file con permisos 600"
}

# Uso
# safe_write /etc/app/config.txt "key=value"
# content=$(safe_read /etc/app/config.txt "key=default")
# log_append /var/log/app.log "Aplicación iniciada"
# create_secret_file /etc/app/secret.key "my-secret-key-123"

Ver También

Código complementario: utilidades ejecutables de lectura/escritura segura para Python, Node.js, Java y Bash están en el repo complementario.

Preguntas frecuentes

¿Cómo añado contenido en vez de sobrescribir?

Usa el modo append: open(p, "a") en Python, appendFile en Node, >> en Bash. Lo que ya estaba queda donde estaba; los datos nuevos van al final.

¿Por qué debo evitar readFileSync en Node.js?

Congela el event loop de un solo hilo, así que todas las demás peticiones esperan a que termine la lectura. Usa fs/promises en cualquier código de servidor en producción.

¿Cómo leo un archivo grande sin quedarme sin memoria?

Procésalo línea a línea: for line in f en Python, createReadStream en Node, while read line en Bash. El uso de memoria se mantiene plano, tanto si el archivo pesa 10 KB como 10 GB.

¿Cómo escribo de forma segura en un archivo que otros procesos podrían estar leyendo?

Escribe en un archivo temporal dentro del mismo sistema de archivos y luego renómbralo de forma atómica sobre el destino. Los lectores reciben el archivo viejo completo o el nuevo completo, nunca uno a medio escribir.

¿Cuál es la diferencia entre modo texto y modo binario?

El modo texto aplica traducción de saltos de línea según la plataforma (\r\n en Windows) y decodificación. El modo binario lee los bytes en crudo, sin tocarlos. Usa modo binario con imágenes, archivos comprimidos o cualquier cosa que necesite fidelidad byte a byte.

¿Cómo bloqueo un archivo para evitar escrituras concurrentes?

No hay lock portable. POSIX te da fcntl.flock en Python y Windows tiene msvcrt.locking, pero Node.js no trae lock nativo. El patrón habitual ahí es un lockfile atómico: fs.open(p, 'wx') falla cuando el archivo ya existe, así que el propio lockfile actúa de mutex. Librerías como proper-lockfile lo encapsulan.