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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
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.

Mesa de prueba

Del archivo a un resultado comprobado

Avanza por archivo, módulo, aplicación y prueba; observa qué produce cada etapa.

  1. datos.txt aporta la entrada.
  2. app.py lee y coordina.
  3. resumen.py transforma y devuelve datos.
  4. La aplicación presenta el resultado; el módulo ejecuta pruebas locales por separado.
Acción actualLeer 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.

Python 3 · ejemplo reproducible
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.

Python 3 · ejemplo reproducible
# 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:

Python 3 · ejemplo reproducible
# 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.

Python 3 · ejemplo reproducible
# 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.

app.py importa resumen.py. ¿Qué evita que _probar() se ejecute durante ese import?

Puedes responder o seguir leyendo.

Selecciona una respuesta

Práctica

Traza

Sin ejecutar, usa estas líneas como entrada:

Python 3 · ejemplo reproducible
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".

Python 3 · ejemplo reproducible
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.py pasa sus aserciones en un proceso nuevo
  • app.py lee una ruta válida y maneja una ruta inexistente
  • importar resumen no 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
Cargando progreso…