Crear un archivo temporal como open('/tmp/mi_proceso.txt', 'w') tiene un problema silencioso: si dos instancias de tu programa corren al mismo tiempo, ambas escriben en el mismo archivo. Eso es una race condition clásica, y en contextos donde el archivo temporal contiene datos sensibles, también es un vector de seguridad. El módulo tempfile de la biblioteca estándar resuelve esto generando nombres únicos e impredecibles, y limpiando después de sí mismo.
El mecanismo es simple pero sólido: el SO reserva el archivo de forma atómica, combinando un directorio base (normalmente /tmp en Unix, %TEMP% en Windows), un prefijo/sufijo configurable, y bytes aleatorios del sistema. Nadie puede predecir ni colisionar con ese nombre. Además, los objetos que devuelve tempfile implementan el protocolo de contexto (__enter__/__exit__), así que el archivo se elimina automáticamente al salir del bloque with, aunque tu código lance una excepción.
La elección entre las tres variantes principales depende de si necesitas el nombre del archivo:
TemporaryFile()abre un archivo que en Unix ni siquiera tiene entrada en el directorio (se desvincula inmediatamente). Solo puedes accederlo por el descriptor de archivo que tienes en mano. Ideal para datos en tránsito que ningún otro proceso necesita ver.NamedTemporaryFile()crea un archivo con nombre real y accesible en disco mientras está abierto. Úsalo cuando necesitas pasarle la ruta a un subproceso o a una librería que solo acepta rutas, no file objects.TemporaryDirectory()crea un directorio completo y limpia todo su contenido recursivamente al cerrar.
El riesgo de hacerlo mal no es solo la race condition: si usas delete=False sin limpiar manualmente, acumulas archivos huérfanos. Si pasas la ruta de un NamedTemporaryFile a otro proceso en Windows, ese proceso no puede abrir el archivo porque el SO lo bloquea mientras el creador lo tiene abierto (detalle que en Unix no duele porque allí el descriptor y el nombre son independientes).
import tempfile
import subprocess
import os
from pathlib import Path
def compress_to_temp(data: bytes) -> bytes:
"""Comprime datos usando gzip a través de un archivo temporal nombrado.
Necesitamos NamedTemporaryFile porque subprocess necesita una ruta real,
no puede recibir un file object de Python.
"""
# delete=False porque gzip subprocess abrirá el archivo por nombre;
# en Windows un segundo open() sobre un NamedTemporaryFile abierto falla.
# Limpiamos manualmente en el bloque finally.
tmp_path = None
try:
with tempfile.NamedTemporaryFile(
suffix=".bin",
prefix="compress_input_",
delete=False,
) as tmp:
tmp_path = tmp.name # guardamos la ruta antes de cerrar
tmp.write(data)
# El archivo ya está cerrado aquí; gzip puede abrirlo sin conflicto
result = subprocess.run(
["gzip", "--stdout", tmp_path],
capture_output=True,
check=True,
)
return result.stdout
finally:
if tmp_path and Path(tmp_path).exists():
os.unlink(tmp_path) # limpieza manual obligatoria con delete=False
def pipeline_in_memory(records: list[str]) -> dict[str, int]:
"""Procesa registros a través de un archivo sin nombre.
TemporaryFile es suficiente: nadie externo necesita esta ruta.
El SO libera el inodo cuando cerramos, sin dejar rastro en el filesystem.
"""
with tempfile.TemporaryFile(mode="w+", encoding="utf-8") as tmp:
for record in records:
tmp.write(record + "\n")
tmp.seek(0) # rebobinar antes de leer; write() no lo hace solo
counts: dict[str, int] = {}
for line in tmp:
word = line.strip()
counts[word] = counts.get(word, 0) + 1
return counts # el archivo ya no existe en disco
def extract_and_process(archive_path: str) -> list[Path]:
"""Descomprime en un directorio temporal y retorna las rutas encontradas.
TemporaryDirectory limpia el árbol entero al salir, sin importar
cuántos archivos haya creado la descompresión.
"""
found: list[Path] = []
with tempfile.TemporaryDirectory(prefix="extract_") as tmpdir:
subprocess.run(
["tar", "-xf", archive_path, "-C", tmpdir],
check=True,
)
for path in Path(tmpdir).rglob("*.csv"):
# procesamos dentro del bloque; fuera el directorio ya no existe
found.append(path)
print(f" encontrado: {path.name}, {path.stat().st_size} bytes")
# tmpdir y todo su contenido fue eliminado al salir del with
return found # las rutas en found ya no son válidas fuera del bloque
if __name__ == "__main__":
compressed = compress_to_temp(b"hello world " * 1000)
print(f"Comprimido: {len(compressed)} bytes")
counts = pipeline_in_memory(["alpha", "beta", "alpha", "gamma", "beta", "beta"])
print(f"Conteos: {counts}")
Desglose del código
compress_to_temp ilustra el patrón más delicado. Cuando una librería o herramienta externa necesita una ruta real en disco, NamedTemporaryFile con delete=False es el camino correcto, pero el contrato cambia: el módulo tempfile ya no limpia por ti. Por eso cerramos el archivo antes de llamar al subproceso (el with termina, el descriptor se cierra) y envolvemos todo en un try/finally con os.unlink explícito. Fíjate en que guardamos tmp.name dentro del with; fuera del bloque, en Windows, el archivo ya podría no estar accesible por nombre si no usas delete=False.
pipeline_in_memory muestra que TemporaryFile opera a un nivel más bajo: en Linux el archivo se desvincula del directorio en el momento de la creación, así que ls /tmp nunca lo mostrará. El kernel mantiene el inodo vivo mientras el descriptor esté abierto. El seek(0) después de escribir es el error más común con cualquier archivo: write() avanza el cursor, y si no lo regresas al inicio antes de leer, obtienes un archivo vacío. No hay nada específico de tempfile aquí, pero es donde la gente tropieza.
extract_and_process demuestra el poder de TemporaryDirectory: aunque tar cree cientos de archivos anidados, el __exit__ hace un shutil.rmtree del directorio raíz y desaparece todo. El punto importante está en el comentario al final: las rutas almacenadas en found siguen existiendo como objetos Path de Python, pero el filesystem ya no tiene esos archivos. Si necesitas el contenido fuera del bloque, léelo dentro del bloque.
Errores que debes conocer
Error: abrir un NamedTemporaryFile en Windows desde un segundo proceso mientras el creador lo tiene abierto, porque el SO aplica bloqueo exclusivo por defecto.
# ❌ Wrong — en Windows, subprocess no puede abrir el archivo
with tempfile.NamedTemporaryFile(suffix=".bin") as tmp:
tmp.write(data)
subprocess.run(["tool", tmp.name], check=True) # PermissionError en Windows
# ✅ Right — cerrar antes de que otro proceso lo necesite
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp:
tmp.write(data)
tmp_path = tmp.name
# descriptor cerrado; ahora subprocess puede abrir el archivo
try:
subprocess.run(["tool", tmp_path], check=True)
finally:
os.unlink(tmp_path)
Cerrar el descriptor transfiere la propiedad del archivo al proceso siguiente, y delete=False evita que tempfile lo elimine justo al cerrar.
Error: leer un TemporaryFile (o cualquier NamedTemporaryFile) sin llamar a seek(0) después de escribir, obteniendo un archivo aparentemente vacío.
# ❌ Wrong — el cursor está al final tras escribir
with tempfile.TemporaryFile(mode="w+") as tmp:
tmp.write("datos importantes")
content = tmp.read() # devuelve "" porque el cursor está al final
# ✅ Right — rebobinar antes de leer
with tempfile.TemporaryFile(mode="w+") as tmp:
tmp.write("datos importantes")
tmp.seek(0)
content = tmp.read() # "datos importantes"
seek(0) regresa el cursor al byte 0; sin él, read() encuentra EOF inmediatamente y devuelve una cadena vacía sin lanzar ninguna excepción.
N° 85