Bucles en Python — práctica con el PIN de la caja, las formas de ordenar la cola y el horno de croissants (y el intento que se contaba demasiado tarde)
La primera vez que hice el programa del PIN de la caja, lo di por bueno a la primera: tres intentos y se bloquea. Se lo enseñé a un compañero, se puso a teclear PIN falsos para romperlo… y consiguió cuatro intentos. El fallo era de una sola línea: yo sumaba el intento después de comprobar el PIN, y en la vuelta en la que fallaba por última vez el contador todavía decía «te queda uno». Desde entonces tengo una regla con los contadores de los while: decidir en qué momento exacto de la vuelta se suman, y hacer la traza a mano antes de fiarme.
Esa regla es el hilo de esta práctica. Ya has visto la teoría de bucles en Python con la caja de la cafetería; ahora toca usarla en tres programas completos de nivel medio, con los datos que salen de verdad en una caja: un PIN que se teclea mal, una cola que se puede ordenar de muchas formas y un horno con su cuenta atrás. Los tres están escritos sin input(): lo que vendría del teclado va en una lista fija, para que la salida que ves sea la de la ejecución real y puedas comprobarla línea a línea.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- La diferencia entre
for(sabes cuántas vueltas) ywhile(no lo sabes). - Que
range(inicio, fin)no incluye elfin, y que con un paso negativo cuenta hacia atrás. - El patrón del acumulador: se prepara antes del bucle, se actualiza dentro y se usa después.
- Qué hace
breaky por qué unwhilenecesita una línea que acerque su condición al final.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Crea una carpeta practica_bucles con un archivo por programa (pin.py, factorial.py, horno.py). Así puedes ejecutarlos por separado en VS Code y comparar tu salida con la del artículo.
Programa 1: el PIN de la caja (validador con intentos limitados)
La caja de la cafetería se abre con un PIN de cuatro cifras. Tienes tres intentos: si aciertas, la caja se abre; si fallas los tres, se bloquea y hay que avisar a la encargada. Lo que lo complica es que el bucle tiene dos maneras de terminar (acertar o quedarse sin intentos) y que los mensajes tienen que decir cuántos intentos quedan sin equivocarse en uno.
La idea
No sabemos cuántas vueltas dará: puede acertar a la primera, a la segunda o no acertar nunca. Eso pide un while. La condición tiene que juntar las dos salidas: «sigue mientras queden intentos y la caja siga cerrada«. Para la segunda parte usamos una variable bandera (desbloqueada), un booleano que empieza en False y se pone a True cuando se acierta. Así, el propio while deja de repetirse sin necesidad de break.
En cada vuelta, el orden es: leer el PIN, contar el intento, comprobar, informar.
Antes de programar, piensa en qué valor tiene que tener intentos cuando se imprime «te quedan X». Si cuentas el intento después de imprimir, el mensaje irá una vuelta por detrás (justo mi fallo de la anécdota).
El código
# Programa 1 · El PIN de la caja: 3 intentos y se bloquea
PIN_CORRECTO = "2024"
MAX_INTENTOS = 3
tecleados = ["1234", "2024"] # lo que teclea la persona, en orden (sin input())
intentos = 0
desbloqueada = False
while intentos < MAX_INTENTOS and not desbloqueada: # para con CUALQUIERA de las dos
pin = tecleados[intentos] # el intento número intentos + 1
intentos += 1 # se cuenta ANTES de comprobar: fallar también gasta intento
if pin == PIN_CORRECTO:
desbloqueada = True # esto hace falsa la condición del while
print(f"Intento {intentos}: {pin} -> correcto")
else:
restantes = MAX_INTENTOS - intentos
if restantes > 0:
print(f"Intento {intentos}: {pin} -> incorrecto, te quedan {restantes}")
else:
print(f"Intento {intentos}: {pin} -> incorrecto, sin intentos")
if desbloqueada:
print(f"Caja abierta en {intentos} intento(s)")
else:
print("Caja bloqueada: avisa a la encargada")
Salida
Con tecleados = ["1234", "2024"] (falla y luego acierta):
Intento 1: 1234 -> incorrecto, te quedan 2 Intento 2: 2024 -> correcto Caja abierta en 2 intento(s)
Y con tres PIN incorrectos, tecleados = ["1234", "0000", "2042"] (es el mismo programa con otra lista; cada versión se ejecutó una vez):
Intento 1: 1234 -> incorrecto, te quedan 2 Intento 2: 0000 -> incorrecto, te quedan 1 Intento 3: 2042 -> incorrecto, sin intentos Caja bloqueada: avisa a la encargada
Comprobación a mano del segundo caso: en la vuelta 3, intentos pasa a 3 y restantes = 3 − 3 = 0, por eso sale «sin intentos» y no «te quedan 0». Después la condición vale 3 < 3 and not False → False and True → False, y el bucle termina. La caja no se abre porque desbloqueada nunca llegó a True.
El bucle es el corazón del programa. Esta es la parte que decide todo:
while intentos < MAX_INTENTOS and not desbloqueada: # para con CUALQUIERA de las dos
pin = tecleados[intentos] # el intento número intentos + 1
intentos += 1 # se cuenta ANTES de comprobar: fallar también gasta intento
if pin == PIN_CORRECTO:
desbloqueada = True # esto hace falsa la condición del while
Su traza en el caso del bloqueo (valores al final de cada vuelta):
| Vuelta | pin | intentos | desbloqueada | Condición para la siguiente vuelta |
|---|---|---|---|---|
| 1 | «1234» | 1 | False | 1 < 3 and not False → True |
| 2 | «0000» | 2 | False | 2 < 3 and not False → True |
| 3 | «2042» | 3 | False | 3 < 3 and not False → False |
El dibujo pone los dos casos uno encima de otro, con la llave que cierra el bucle en cada uno:
Preguntas que te puedes hacer
¿Por qué pin = tecleados[intentos] y no otro contador? Porque intentos vale 0 en la primera vuelta, 1 en la segunda… justo las posiciones de la lista. Se lee el PIN antes de sumar el intento, así que la vuelta 1 lee la posición 0.
¿Por qué se suma el intento antes del if? Porque fallar también gasta un intento. Si se sumara solo en el else, acertar no contaría como intento y «Caja abierta en 1 intento(s)» saldría como «en 0». Y si se sumara después de imprimir el mensaje, el «te quedan» iría una vuelta por detrás.
¿No sería más fácil con break? Se puede: while intentos < MAX_INTENTOS: y un break al acertar. Funciona igual. La bandera tiene una ventaja: después del bucle, if desbloqueada: te dice por qué terminó. Con break tendrías que averiguarlo de otra forma (por ejemplo, mirando si intentos llegó a 3, que también es el caso de acertar en el tercer intento… y ahí se lía).
¿Qué hace not desbloqueada? Da la vuelta al booleano: mientras desbloqueada es False, not desbloqueada es True y el and puede seguir siendo verdadero. Lo vimos en los operadores lógicos.
¿Por qué MAX_INTENTOS va en mayúsculas? Es una convención de Python para las constantes: valores que no deberían cambiar mientras se ejecuta el programa. Si mañana la caja permite 5 intentos, se cambia en un solo sitio.
Escribir or en lugar de and en la condición: while intentos < MAX_INTENTOS or not desbloqueada:. Al acertar, intentos < 3 sigue siendo True, así que el or sigue siendo True y el bucle continúa pidiendo PIN con la caja ya abierta. Y si nunca aciertas, cuando se acaba la lista Python lanza IndexError.
Pruébalo tú
- Cambia
MAX_INTENTOSa 5 y pon una lista con cuatro fallos y un acierto. ¿Qué dice el mensaje final? - Reescribe el bucle con
while intentos < MAX_INTENTOS:ybreakal acertar, sin bandera, y haz que el mensaje final siga distinguiendo «abierta» de «bloqueada».
Programa 2: ¿de cuántas formas se puede ordenar la cola? (factorial)
En la cola de la cafetería hay 5 personas. ¿De cuántas formas distintas podrían ponerse en fila? El primero puede ser cualquiera de los 5; el segundo, cualquiera de los 4 que quedan; el tercero, de 3… Son 5 × 4 × 3 × 2 × 1 formas, que en matemáticas se escribe 5! (cinco factorial). El programa lo calcula con for y con while, enseña el proceso como texto y saca una tabla de factoriales para ver lo rápido que crecen. Lo que lo complica: el caso del 0 y construir el texto «1 x 2 x 3″ sin dejar un » x » colgando al final.
La idea
Es un acumulador, pero de productos: empieza en 1 y en cada vuelta se multiplica por el siguiente número. Como sabemos exactamente de dónde a dónde vamos (de 1 a personas), lo natural es un for con range(1, personas + 1). Lo hacemos también con while para ver qué tienes que escribir tú cuando el bucle no lleva el contador por ti.
Antes de programar, escribe en papel las vueltas para 3 personas: 1 → 1 × 1 = 1 → 1 × 2 = 2 → 2 × 3 = 6. Si tu programa no da 6 con personas = 3, el fallo está en el range.
El código
# Programa 2 · ¿De cuántas formas puede ordenarse la cola? n! (factorial)
personas = 5
# Con for: sabemos que hay que multiplicar de 1 a n
formas_for = 1 # 1 y no 0: es un acumulador de PRODUCTOS
for i in range(1, personas + 1): # + 1 porque el fin de range no entra
formas_for *= i
# Con while: lo mismo, pero el contador lo llevamos nosotros
formas_while = 1
i = 1
while i <= personas:
formas_while *= i
i += 1 # sin esta línea, bucle infinito
print(f"{personas}! con for: {formas_for}")
print(f"{personas}! con while: {formas_while}")
print("¿Coinciden?", formas_for == formas_while)
# El proceso a la vista: se construye un texto vuelta a vuelta
producto = 1
pasos = "" # acumulador de TEXTO: empieza vacío
for i in range(1, personas + 1):
producto *= i
pasos += str(i)
if i < personas: # sin " x " detrás del último número
pasos += " x "
print(f"Proceso: {pasos} = {producto}")
# ¿Y si la cola es más larga? Crece MUY rápido
producto = 1
for n in range(1, 11):
producto *= n # aprovecha el resultado anterior: n! = (n-1)! x n
print(f"{n:2}! = {producto}")
Salida
5! con for: 120 5! con while: 120 ¿Coinciden? True Proceso: 1 x 2 x 3 x 4 x 5 = 120 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800
Comprobación a mano: 1 × 2 = 2, × 3 = 6, × 4 = 24, × 5 = 120. Las dos versiones dan lo mismo y la tabla lo confirma en su quinta línea. Fíjate en lo que crece: con 10 personas hay más de tres millones y medio de formas de ordenar la cola.
Así crece el acumulador vuelta a vuelta:
La parte más fina es la que construye el texto del proceso:
producto = 1
pasos = "" # acumulador de TEXTO: empieza vacío
for i in range(1, personas + 1):
producto *= i
pasos += str(i)
if i < personas: # sin " x " detrás del último número
pasos += " x "
print(f"Proceso: {pasos} = {producto}")
| Vuelta (i) | producto | pasos después de la vuelta |
|---|---|---|
| 1 | 1 | «1 x « |
| 2 | 2 | «1 x 2 x « |
| 3 | 6 | «1 x 2 x 3 x « |
| 4 | 24 | «1 x 2 x 3 x 4 x « |
| 5 | 120 | «1 x 2 x 3 x 4 x 5» |
En la última vuelta i < personas es 5 < 5, que es False, así que no se añade el » x » final. Es el mismo patrón del acumulador, pero con texto: empieza en "" y se le va pegando cosas con +=.
¿Y si en la cola no hay nadie? Por definición, 0! = 1 (hay exactamente una forma de ordenar cero personas: no hacer nada). Y el programa lo da sin ningún if, porque range(1, 1) está vacío:
# ¿Y 0!? range(1, 1) está vacío: el for no da ni una vuelta
personas = 0
formas = 1
for i in range(1, personas + 1):
formas *= i
print("Esta línea no se llega a imprimir")
print(f"{personas}! = {formas}")
print("Vueltas de range(1, 1):", len(range(1, personas + 1)))
Salida:
0! = 1 Vueltas de range(1, 1): 0
El for no da ni una vuelta (el print de dentro no aparece), así que formas se queda en su valor inicial, 1. El acumulador de productos empezando en 1 resuelve el caso límite solo.
Preguntas que te puedes hacer
¿Por qué formas_for = 1 y no 0? Porque 0 × cualquier cosa = 0. Con 0, el resultado sería siempre 0, para cualquier número de personas. El 1 no cambia lo que multiplicas.
¿Por qué range(1, personas + 1)? Porque el fin de range no entra. Con range(1, personas) llegaría al 4 y el resultado sería 24, no 120. Y empezamos en 1 porque, si empezáramos en 0 (range(personas + 1)), la primera vuelta multiplicaría por 0.
Si las dos versiones dan lo mismo, ¿cuál uso? La del for. Hace lo mismo con menos líneas y es imposible olvidar el i += 1. El while solo tiene sentido cuando no sabes cuántas vueltas habrá.
¿Por qué la tabla no vuelve a calcular cada factorial desde cero? Porque no hace falta: 6! = 5! × 6. El acumulador producto ya tiene el factorial anterior, así que cada vuelta hace una sola multiplicación. Calcularlos desde cero necesitaría un bucle anidado y muchas más operaciones.
¿Qué es {n:2}? Reserva dos huecos para el número, para que » 1!» y «10!» queden alineados en la columna.
Reutilizar producto sin volver a ponerlo a 1. Si borras el producto = 1 de antes de la tabla, la tabla empieza con el 120 del bloque anterior y todo sale multiplicado por 120.
Pruébalo tú
- Cambia
personasa 7 y comprueba con la tabla que el resultado es 5040. - Haz que la tabla se pare (con
break) en cuanto el factorial pase de un millón, e imprime en quénocurrió.
Programa 3: la cuenta atrás del horno de croissants
El horno de la cafetería tiene un temporizador: cuenta atrás desde 5 segundos (en la vida real serían minutos, pero así no esperas tanto), avisa a mitad para dar la vuelta a la bandeja y anuncia que los croissants están listos. Después hacemos la misma cuenta con while para compararlas. Lo que lo complica es contar hacia atrás con range y meter un aviso solo en una vuelta concreta.
La idea
Sabemos de dónde a dónde vamos (de 5 a 1), así que for. Para bajar, range necesita un paso negativo: range(segundos, 0, -1). El fin (0) no entra, así que la última vuelta es la del 1. El aviso de la bandeja es un if dentro del cuerpo que solo se cumple en una vuelta. Y time.sleep(1) hace que el programa espere un segundo de verdad entre número y número.
Antes de programar, escribe qué genera range(5, 0, -1) y qué generaría range(5, 0). Si la segunda te sale vacía, ya lo tienes (y si no, mira el dibujo de range en la teoría).
El código
# Programa 3 · Cuenta atrás del horno de croissants
import time
segundos = 5
print("=== Horno en marcha ===")
for s in range(segundos, 0, -1): # 5, 4, 3, 2, 1: el 0 no entra
print(f"{s}...")
if s == 3:
print(" (dale la vuelta a la bandeja)")
time.sleep(1) # espera 1 segundo de verdad
print("¡Croissants listos!")
print("--- La misma cuenta atrás con while ---")
s = segundos
while s > 0:
print(f"{s}...", end=" ") # end=" " deja los números en la misma línea
s -= 1 # sin esta línea, el horno no acaba nunca
print("¡Listos!")
Salida
=== Horno en marcha === 5... 4... 3... (dale la vuelta a la bandeja) 2... 1... ¡Croissants listos! --- La misma cuenta atrás con while --- 5... 4... 3... 2... 1... ¡Listos!
Comprobación: cinco números del 5 al 1 (el 0 no aparece), el aviso justo después del 3 y, en la versión while, los mismos cinco números en una línea. Al ejecutarlo, la primera parte tarda 5 segundos: uno por cada time.sleep(1).
Zoom al bucle for:
for s in range(segundos, 0, -1): # 5, 4, 3, 2, 1: el 0 no entra
print(f"{s}...")
if s == 3:
print(" (dale la vuelta a la bandeja)")
time.sleep(1) # espera 1 segundo de verdad
| Vuelta | s | ¿s == 3? | Qué imprime |
|---|---|---|---|
| 1 | 5 | No | 5… |
| 2 | 4 | No | 4… |
| 3 | 3 | Sí | 3… y el aviso de la bandeja |
| 4 | 2 | No | 2… |
| 5 | 1 | No | 1… |
Preguntas que te puedes hacer
¿Por qué range(segundos, 0, -1) y no range(segundos, 1, -1)? Porque el fin no entra: con 1 como fin, la última vuelta sería la del 2 y el 1 no aparecería. Para que el 1 entre, el fin tiene que ser 0.
¿Para qué sirve import time? Para usar time.sleep(), que está en el módulo time de la biblioteca estándar. Sin el import, Python no sabe qué es time y lanza NameError.
¿Por qué la versión while no tiene sleep? Para que veas la diferencia de forma sin esperar otros 5 segundos. Lo importante de esa versión es el s -= 1: sin él, s se queda en 5 y el horno no acaba nunca.
¿Qué hace end=" "? Cambia lo que print pone al final: en lugar de un salto de línea, un espacio. Por eso los números del while salen seguidos, y el «¡Listos!» del final aparece en la misma línea.
¿Este programa es mejor con for o con while? Con for: sabes cuántas vueltas habrá y el contador lo lleva el propio bucle. Aquí los dos son igual de cortos, pero el while tiene una línea que puedes olvidar.
Escribir range(5, 0) sin el -1. No da error: el rango está vacío, el bucle no da ninguna vuelta y el programa pasa directamente a «¡Croissants listos!». Es de los fallos más difíciles de ver porque no hay ningún mensaje.
Pruébalo tú
- Haz que el aviso de la bandeja salga a mitad de la cuenta para cualquier valor de
segundos(pista:segundos // 2). - Cambia la versión
whilepara que cuente de 2 en 2 hacia atrás desde 10 (10, 8, 6, 4, 2).
Errores típicos en esta práctica
- Contar el intento en el momento equivocado: si se suma después de imprimir, los mensajes van una vuelta por detrás y se cuela un intento de más.
oren vez deanden una condición doble: el bucle no para cuando debería; con dos condiciones de «seguir», casi siempre esand.- Acumulador de productos que empieza en 0: todo da 0.
rangeque se queda corto:range(1, n)en lugar derange(1, n + 1).- Paso negativo olvidado:
range(5, 0)está vacío y el bucle no hace nada, sin avisar. whilesin su-= 1o+= 1: bucle infinito; Ctrl + C para salir.
Míralo paso a paso en Python Tutor
En pythontutor.com (elige Python 3) puedes ver cómo cambia la condición del PIN vuelta a vuelta. Esta es una versión corta del programa 1, con dos fallos y un acierto:
# Para Python Tutor: el PIN de la caja, versión corta
PIN = "2024"
tecleados = ["1234", "0000", "2024"]
intentos = 0
abierta = False
while intentos < 3 and not abierta:
pin = tecleados[intentos]
intentos += 1
if pin == PIN:
abierta = True
print(abierta, intentos)
Salida:
True 3
Qué observar paso a paso:
- Antes del bucle,
intentosvale 0 yabiertavaleFalse: la condición0 < 3 and not FalseesTrue. - En cada vuelta,
pintoma la posición que marcaintentosantes de queintentossume 1. Mira las dos variables cambiar en ese orden. - En la tercera vuelta,
pines «2024» yabiertapasa aTrue. - La flecha vuelve al
while, evalúa3 < 3 and not Truey sale: las dos partes son falsas a la vez. Comprueba qué pasaría sitecleadostuviera el acierto en la segunda posición. - El
printfinal muestraTrue 3: la caja se abrió y se gastaron tres intentos.
Resumen
- Un
whilecon dos condiciones unidas porandtermina en cuanto cualquiera de las dos se vuelve falsa; una bandera booleana te dice después por qué terminó. - El momento en que sumas un contador (antes o después de comprobar e imprimir) cambia los mensajes: haz la traza a mano.
- Los acumuladores de productos empiezan en 1, los de sumas en 0 y los de texto en
"". range(1, n + 1)llega hasta n;range(n, 0, -1)cuenta de n a 1; un rango vacío no da error, simplemente no da vueltas.- Si sabes cuántas vueltas habrá,
for; elwhileequivalente necesita que tú lleves el contador. - Sin
input(), una lista hace de teclado y la salida se puede comprobar línea a línea.
Checklist: ¿lo has practicado?
- ¿Has ejecutado los tres programas y comprobado las salidas?
- ¿Sabes explicar por qué el programa del PIN termina en los dos casos?
- ¿Puedes decir, sin ejecutarlo, qué imprime
range(10, 0, -3)? - ¿Has hecho a mano la traza del factorial de 4?
- ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
- ¿Sabrías reescribir la cuenta atrás con
whilesin que se quede colgada?
Si has respondido que sí a casi todo, estás listo para los ejercicios de bucles en Python, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosBucles en Python — ejercicios con FizzBuzz, una inversión que crece, primos y un plan de ahorro (y el break que solo salía de la bandeja)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




Qué es un sistema operativo — teoría con tu portátil en una tarde de estudio, una llamada a write() y la CPU que nunca descansa (y los tres programas que no se ejecutaban a la vez)