Empezando CS50P aunque ya escribo Python

16 de agosto de 2026 code learningpython

Empezar CS50P este mes se sintió un poco al revés, la verdad. Ya he lanzado un CRM en Django, un script de cámara que me manda correos, un resumen de noticias, un puñado de scrapers y herramientas de PDF, dos proyectos de boot.dev (bookbot, webflyx). CS50P es el curso de "introducción a Python". Ya sé lo que es un bucle for.

Lo hice de todos modos, y me alegro de haberlo hecho, cuatro semanas después.

lo que los cursos de fundamentos comprueban en realidad

No si puedes escribir un bucle for. Si puedes escribir uno que otra persona, o check50, o mi yo futuro dentro de seis meses, pueda de verdad leer y en el que pueda confiar. La convención de archivos test_ de CS50P, escribir de verdad pruebas de pytest reales en lugar de examinar la salida a ojo en la terminal, fue lo que me dio un poco de vergüenza. Llevo un tiempo escribiendo scripts "reales" y sin probar prácticamente ninguno de ellos.

el primer archivo de pruebas que escribí, que técnicamente pasó y no demostró nada

La tarea de la semana me hacía escribir una pequeña función de validación de entrada, comprobando que un código estilo matrícula sigue un conjunto de reglas de formato, y escribiendo pruebas junto a ella. Mi primer intento del archivo de pruebas se veía así:

from project import is_valid

def test_valid():
    assert is_valid("CS50P")

def test_invalid():
    assert not is_valid("cs50p")

Dos pruebas. Ambas pasaron. Marcas verdes, se sintió genial durante unos cuatro minutos, hasta que check50 corrió sus propias pruebas ocultas contra mi función is_valid y falló con una entrada que nunca había probado de verdad: una matrícula con un número en medio, como AB12CD, que mi función aceptaba incorrectamente.

:( is_valid rejects plates with a letter after a digit
    expected "False", not "True"

el problema real, en cuanto miré

# project.py, the broken version
def is_valid(s):
    return s[:2].isalpha() and s[2:].isalnum()

Comprueba que empieza con letras y que el resto es alfanumérico. No comprueba el orden real que exigen las reglas, que en cuanto aparece un dígito, nada excepto dígitos puede seguirlo. AB12CD pasa esta comprobación por completo: letras al principio, alfanumérico el resto, técnicamente cierto, respuesta equivocada.

Mis propias dos pruebas no habían atrapado esto porque solo había probado un caso obviamente válido y un caso obviamente inválido. Ninguno estaba ni cerca del borde real, que es exactamente donde se escondía el bug de verdad.

lo que en realidad busqué en Google

"pytest test edge cases how many test cases is enough" me llevó, con el tiempo, a la idea de las pruebas dirigidas por tabla o parametrizadas, escribir una lista de casos de antemano (entrada, resultado esperado) en lugar de un assert cada vez, específicamente para que añadir un nuevo caso límite sea solo añadir una fila en lugar de escribir toda una función nueva.

import pytest
from project import is_valid

@pytest.mark.parametrize("plate, expected", [
    ("CS50P", True),
    ("cs50p", False),
    ("AB12CD", False),
    ("A1", False),
    ("", False),
])
def test_is_valid(plate, expected):
    assert is_valid(plate) == expected

Cinco casos en lugar de dos, y escribirlos como una tabla hizo obvio el hueco en mi propio razonamiento antes de que check50 tuviera que señalármelo. AB12CD estaba ahí mismo en cuanto me vi obligado a listar de verdad casos límite en lugar de elegir dos que se sintieran representativos.

la función arreglada

# project.py, the fixed version
def is_valid(s):
    seen_digit = False
    for ch in s:
        if ch.isdigit():
            seen_digit = True
        elif seen_digit:
            return False
    return len(s) >= 2 and s[:2].isalpha()

Recorre la cadena una vez, y en el momento en que aparece una letra después de que ya se haya visto un dígito, es inválida. Pasó mis pruebas locales ampliadas y las ocultas de check50 en el siguiente envío.

el script de renombrado, revisitado

¿Te acuerdas del script de renombrado de fotos que escribí hace un tiempo, el que se rompía con una colisión de nombre de archivo la primera vez que lo ejecuté? Volví y lo miré después de una clase de CS50P sobre excepciones, y mi yo del pasado no había envuelto nada en un bloque try, simplemente dejaba que se rompiera y arreglaba el bug leyendo el traceback después. Funcionaba, técnicamente. No exactamente el enfoque recomendado.

El viejo script de renombrado, de antes de que este curso me hiciera pensar en los modos de fallo

Volviendo con ojos frescos, el arreglo en realidad ni siquiera tenía que ver con excepciones, era que nunca había considerado ni una sola vez qué debía pasar cuando un destino de renombrado ya existe, solo lo había descubierto viendo cómo la cosa se rompía. Un try/except FileExistsError alrededor de la propia llamada a os.rename, recurriendo a un sufijo numerado, habría hecho que la primera versión fuera correcta el día uno en lugar de correcta en el cuarto intento de depuración.

un pequeño inciso sobre cuánto tiempo peleé contra el vídeo de la clase en vez de contra el código

Genuinamente perdí unos cuarenta minutos convencido de que había un bug en pytest mismo, porque mi prueba parametrizada seguía reportando el valor esperado equivocado en el mensaje de fallo, mostrando False cuando estaba seguro de haber escrito True para esa fila. No había ningún bug. Había contado mal las filas al añadir un nuevo caso en medio de la lista y había desplazado cada valor esperado siguiente en uno, así que la matrícula de la fila cuatro se estaba comprobando contra la respuesta esperada de la fila cinco. El mensaje de error de pytest fue completamente correcto todo el tiempo. Simplemente no le creí hasta que imprimí toda la lista de parametrize y conté a mano como si fuera un dictado. Las pruebas dirigidas por tabla solo son tan fiables como la tabla, lo cual es obvio en retrospectiva y no era nada obvio al minuto treinta de mirar fijamente una X roja.

lo que en realidad es distinto esta vez

No estoy aprendiendo sintaxis. Ya la tengo, sobre todo por accidente, de simplemente construir cosas que necesitaban funcionar. Lo que CS50P en realidad me está dando es lo que nadie señala directamente hasta que un curso lo hace: uso adecuado de librerías en lugar de copiar y pegar de Stack Overflow hasta que compile, disciplina de pruebas real, código que se lee como si estuviera escrito para una persona y no solo para que el intérprete lo acepte.

Es un tipo raro de humildad. bookbot y webflyx ambos funcionaron, se lanzaron, hicieron lo que se suponía que debían hacer. Ninguno de los dos habría sobrevivido a una revisión de código de alguien que de verdad supiera lo que hacía, y solo lo creo de verdad ahora que he visto cómo se ve la alternativa, pruebas dirigidas por tabla incluidas.

qué haría distinto de aquí en adelante

Escribir la tabla de casos límite antes de escribir la función, no después. Cada bug que me he encontrado en este curso hasta ahora estaba sentado en el hueco entre "los dos casos que se me ocurrió probar" y "el caso que check50 en realidad probó". Listar los casos límite primero convertiría ese hueco en algo que atrapo yo mismo, en lugar de algo que un script de calificación atrapa por mí.

También dejaría de confiar en una ejecución de pruebas en verde como prueba de nada por sí sola. Dos pruebas que pasaban no me decían nada útil sobre is_valid, porque los dos casos que elegí estaban ambos cómodamente lejos del borde real de la regla. Un conjunto de pruebas solo es tan bueno como los casos que alguien de verdad pensó en escribir, los míos incluidos, y "pasó" es una frase mucho más débil de lo que suena la primera vez que la escribes.

Si de verdad voy a seguir escribiendo pruebas para cada script desechable después de que termine el curso es una pregunta aparte. Pregúntame en unos meses. Mi suposición sincera ahora mismo es: para el script de la cámara y el resumen de noticias, probablemente sí, ya que corren sin supervisión y no voy a estar ahí para notar un fallo silencioso. Para un scraper de un solo uso que ejecuto una vez y descarto, probablemente sigue siendo no, y no estoy seguro de que esa sea en realidad la decisión equivocada.

0 comentarios

Inicia sesión para comentar.

Iniciar sesión

¿No tienes cuenta?