En esta páginaArchivos y pruebas
Archivos y pruebas
- Nivel: principiante
- Úsala cuando: el código de una actividad lea archivos, se divida en módulos o necesite comprobaciones locales
- Conocimientos previos útiles: funciones, imports y colecciones
En esta página leerás datos desde un archivo de texto, delegarás los cálculos a otro archivo de Python y comprobarás la lógica con pruebas locales. El resultado será un proyecto que puedes cerrar, volver a abrir y ejecutar en un proceso nuevo.
Qué puedes consultar aquí
- leer texto UTF-8 desde una ruta y distinguir la ruta del contenido;
- separar la entrada y la salida de la lógica que transforma datos;
- importar funciones propias sin ejecutar accidentalmente la aplicación;
- distinguir módulo, biblioteca estándar, paquete y dependencia externa;
- comprobar casos concretos con
assert; - ejecutar un programa de dos archivos en un proceso nuevo y explicar su flujo.
Archivos del proyecto
Usaremos esta estructura:
proyecto_resumen/
├── app.py
├── resumen.py
└── datos.txt
datos.txt contiene la entrada. resumen.py transforma líneas de texto en números y calcula un resumen. app.py coordina el trabajo: elige la ruta, lee el archivo, llama las funciones y muestra el resultado.
Separar responsabilidades permite comprobar la lógica sin depender del archivo real. También evita que una función de cálculo tenga que conocer cómo se obtuvo la entrada o dónde se mostrará la salida.
Rutas y contenido
Una ruta identifica una ubicación. Leer esa ruta produce texto. pathlib.Path pertenece a la biblioteca estándar de Python, por lo que no necesitas instalar nada.
from pathlib import Path
ruta = Path("datos.txt")
texto = ruta.read_text(encoding="utf-8")
lineas = texto.splitlines()
print(ruta.name)
print(lineas)
encoding="utf-8" hace explícita la codificación esperada. El bloque with open(...) también es correcto, pero Path.read_text mantiene corto este ejemplo. En ambos casos conviene decidir la codificación en lugar de depender de la configuración del sistema.
Si datos.txt no existe en la ubicación calculada, Python produce FileNotFoundError. El mensaje no significa que el contenido esté mal: significa que el programa no encontró la ruta que intentó abrir.
Archivos JSON
JSON es un formato de texto para guardar datos con estructura, como el diccionario que devuelve una función de resumen. El módulo json pertenece a la biblioteca estándar. json.dumps convierte un objeto de Python en una cadena JSON y json.loads convierte esa cadena de nuevo en un objeto de Python.
import json
resultado = {"cantidad": 3, "total": 21.5, "promedio": 7.17}
texto = json.dumps(resultado)
copia = json.loads(texto)
print(texto)
print(type(texto).__name__, type(copia).__name__)
print(copia == resultado)
La primera línea impresa es el texto {"cantidad": 3, "total": 21.5, "promedio": 7.17}. La segunda muestra str dict: texto es una cadena y copia es un diccionario nuevo. La tercera muestra True porque copia tiene las mismas claves y valores que resultado.
json.dump y json.load, sin la s final, escriben y leen un archivo abierto. Path.open abre la ruta y, como en read_text, encoding="utf-8" fija la codificación. El bloque with cierra el archivo al terminar, por eso el segundo bloque ya encuentra los datos guardados. ensure_ascii=False escribe los acentos tal cual.
import json
from pathlib import Path
ruta = Path("resumen.json")
datos = {"archivo": "datos.txt", "nota": "Línea omitida", "valores": [10.0, 7.5, 4.0]}
with ruta.open("w", encoding="utf-8") as archivo:
json.dump(datos, archivo, ensure_ascii=False)
with ruta.open(encoding="utf-8") as archivo:
leidos = json.load(archivo)
print(ruta.read_text(encoding="utf-8"))
print(leidos == datos)
print(json.dumps(datos["nota"]))
resumen.json contiene {"archivo": "datos.txt", "nota": "Línea omitida", "valores": [10.0, 7.5, 4.0]} y leidos == datos muestra True. La última línea usa el valor predeterminado ensure_ascii=True e imprime "L\u00ednea omitida": la í queda como una secuencia de escape. Es JSON válido y json.loads recupera "Línea omitida", pero si se guardara así, una persona que abra el archivo vería \u00ed en lugar de í.
json.dumps convierte diccionarios, listas, tuplas, cadenas, enteros, float, booleanos y None. Algunos valores cambian de forma al pasar por JSON.
import json
valores = {
"texto": "día",
"entero": 3,
"decimal": 7.5,
"activo": True,
"nada": None,
"lista": [1, 2],
"par": (1, 2),
}
texto = json.dumps(valores, ensure_ascii=False)
copia = json.loads(texto)
print(texto)
print(copia["par"], type(copia["par"]).__name__)
print(copia == valores)
El texto impreso es {"texto": "día", "entero": 3, "decimal": 7.5, "activo": true, "nada": null, "lista": [1, 2], "par": [1, 2]}. En JSON, True se escribe true y None se escribe null; al leer, vuelven a ser True y None. La tupla (1, 2) se escribe como un arreglo, igual que la lista, y json.loads la devuelve como [1, 2], de tipo list. Por eso copia == valores muestra False: [1, 2] no es igual a (1, 2). Un set u otro tipo distinto de los anteriores produce TypeError al llamar json.dumps.
Módulo no significa paquete externo
Un módulo es un archivo .py que puede contener definiciones y sentencias. En este proyecto, resumen.py es un módulo propio. pathlib es un módulo de la biblioteca estándar.
Un paquete agrupa módulos bajo un nombre común. Un paquete de terceros se publica fuera de la biblioteca estándar y suele instalarse por separado; cuando el programa lo necesita, constituye una dependencia externa. En un proyecto posterior, si una dependencia externa fuera necesaria, primero habría que confirmar que el entorno la permite, aislarla y registrar su versión.
Del archivo a un resultado comprobado
Avanza por archivo, módulo, aplicación y prueba; observa qué produce cada etapa.
datos.txtaporta la entrada.app.pylee y coordina.resumen.pytransforma y devuelve datos.- La aplicación presenta el resultado; el módulo ejecuta pruebas locales por separado.
Leer datos.txt como texto UTF-8- Entrada
- 10, 7.5, error, 4
- Módulo
- pendiente
- Resultado
- pendiente
- Verificación
- pendiente
Paso 1, Archivo. Entrada: 10, 7.5, error, 4. Módulo: pendiente. Resultado: pendiente. Verificación: pendiente. Entrada 10, 7.5, error y 4; módulo, resultado y verificación pendientes.
Manejo de errores
float(texto) produce ValueError cuando el texto no representa un número. Path.read_text produce FileNotFoundError cuando la ruta no existe. Un bloque try contiene la operación que puede fallar; el except nombra el error esperado y define una respuesta concreta.
def convertir(texto):
try:
return float(texto)
except ValueError:
return None
print(convertir("7.5"))
print(convertir("error"))
Este ejemplo no captura cualquier excepción: otros fallos siguen visibles y pueden investigarse. En el proyecto usaremos ValueError para omitir una línea inválida y FileNotFoundError para informar que falta el archivo.
El módulo resumen.py
La primera función convierte las líneas válidas a float. Una línea vacía no cuenta como error; una línea no vacía que no representa un número se registra como omitida. La segunda función recibe una lista no vacía y devuelve datos que otras partes del programa pueden usar.
# resumen.py
def parsear_numeros(lineas):
numeros = []
omitidas = 0
for texto in lineas:
limpio = texto.strip()
if not limpio:
continue
try:
numeros.append(float(limpio))
except ValueError:
omitidas += 1
return numeros, omitidas
def resumir(numeros):
total = sum(numeros)
return {
"cantidad": len(numeros),
"total": total,
"promedio": total / len(numeros),
}
parsear_numeros no abre archivos ni imprime. Por eso puedes pasarle líneas preparadas directamente. resumir supone que la lista contiene al menos un número; app.py comprobará esa condición antes de llamarla.
Añade pruebas locales que no se ejecuten al importar
Una aserción expresa una condición que debe ser verdadera para el caso probado. Si es falsa, Python produce AssertionError. Las aserciones correctas no demuestran que la función sea perfecta: respaldan afirmaciones concretas sobre los ejemplos elegidos.
Añade este bloque al final de resumen.py:
# Este bloque continúa dentro de resumen.py.
def _probar():
numeros, omitidas = parsear_numeros(["10", " 7.5 ", "error", ""])
assert numeros == [10.0, 7.5]
assert omitidas == 1
resultado = resumir(numeros)
assert resultado["cantidad"] == 2
assert resultado["total"] == 17.5
assert resultado["promedio"] == 8.75
print("Pruebas locales: OK")
if __name__ == "__main__":
_probar()
Cuando ejecutas python resumen.py, resumen.py se ejecuta como módulo principal y __name__ vale "__main__"; entonces se llama _probar(). Cuando app.py importa resumen, __name__ contiene el nombre del módulo y las pruebas no se ejecutan. Las funciones quedan disponibles para el importador.
El programa app.py
app.py acepta una ruta como primer argumento. Si no recibe una, usa datos.txt. Lee el archivo, evita resumir una lista vacía y presenta un resultado verificable.
# app.py
from pathlib import Path
from sys import argv
from resumen import parsear_numeros, resumir
def main():
if len(argv) > 1:
ruta = Path(argv[1])
else:
ruta = Path("datos.txt")
try:
lineas = ruta.read_text(encoding="utf-8").splitlines()
except FileNotFoundError:
print(f"No se encontró el archivo: {ruta}")
return
numeros, omitidas = parsear_numeros(lineas)
if not numeros:
print("El archivo no contiene números válidos.")
return
resultado = resumir(numeros)
print(f"Archivo: {ruta.name}")
print(f"Valores: {resultado['cantidad']}")
print(f"Promedio: {resultado['promedio']:.2f}")
print(f"Líneas omitidas: {omitidas}")
if __name__ == "__main__":
main()
Crea datos.txt con este contenido:
10
7.5
error
4
Desde la carpeta del proyecto, ejecuta cada comando en un proceso nuevo:
python resumen.py
python app.py datos.txt
La primera orden debe mostrar Pruebas locales: OK. La segunda debe informar tres valores, un promedio de 7.17 y una línea omitida. Si tu entorno usa el comando python3, sustituye python por python3 en ambas órdenes.
Errores comunes
Ejecutar desde otra carpeta
Path("datos.txt") se interpreta desde la carpeta de trabajo actual. Comprueba desde qué carpeta ejecutas el comando o pasa una ruta explícita. Mover el archivo sin cambiar la ruta produce FileNotFoundError.
Poner toda la lógica al nivel superior
Si resumen.py calcula, imprime y prueba en cuanto se importa, app.py provoca efectos inesperados. Mantén las definiciones separadas y protege la ejecución directa con if __name__ == "__main__".
Probar solo el ejemplo feliz
Una prueba con números válidos no comprueba líneas vacías, texto inválido ni una entrada sin números. Cada caso adicional debe corresponder a un riesgo concreto, no a una cantidad arbitraria de pruebas.
Instalar algo que ya viene con Python
pathlib, sys y csv pertenecen a la biblioteca estándar. No ejecutes pip install pathlib, pip install sys ni pip install csv.
Práctica
Traza
Sin ejecutar, usa estas líneas como entrada:
lineas = ["3", "", "cinco", "9"]
Escribe qué valores tendrán numeros, omitidas, cantidad, total y promedio. Después comprueba tu respuesta llamando las funciones desde una consola o un archivo temporal.
Modifica
Añade "minimo" y "maximo" al diccionario devuelto por resumir. Actualiza _probar() con aserciones para ambos campos. Cambia datos.txt y confirma que las pruebas y la aplicación siguen siendo coherentes.
Crea
Construye otro programa de dos archivos que lea una lista de tiempos o puntuaciones. La lógica reutilizable debe estar en un módulo propio y la lectura/salida en la aplicación. Incluye al menos:
- un caso normal;
- una línea inválida;
- un archivo sin valores válidos;
- una modificación explicada por ti.
Reto
Cambia la entrada a un CSV con columnas nombre y valor. Usa solo el módulo csv de la biblioteca estándar. Al abrir un CSV, especifica newline="" y encoding="utf-8".
import csv
with open("datos.csv", newline="", encoding="utf-8") as archivo:
filas = list(csv.DictReader(archivo))
valores = [float(fila["valor"]) for fila in filas]
print(valores)
Comprueba que la lectura CSV esté separada de la función de resumen, que exista una prueba con filas preparadas en memoria y que el programa informe un error comprensible si falta una columna esperada.
Lista de comprobación
resumen.pypasa sus aserciones en un proceso nuevoapp.pylee una ruta válida y maneja una ruta inexistente- importar
resumenno ejecuta la aplicación ni las pruebas - puedes señalar qué parte realiza entrada/salida y cuál transforma datos
- puedes explicar por qué no se instala ningún paquete para este proyecto
- una variante de datos produce un resultado correcto y una prueba actualizada
- puedes guardar un diccionario en JSON con acentos legibles, leerlo de nuevo y explicar por qué una tupla regresa como lista