Home / Lenguajes / Bucles while True en Python: patrones prácticos para 2026
Bucles while True en Python: patrones prácticos para 2026

Bucles while True en Python: patrones prácticos para 2026

14 November 2026 · 21 min de lectura ··

Más allá de la sintaxis: el bucle while como herramienta de diseño

Casi todo el mundo aprende el bucle while en su primera semana con Python y luego lo olvida en favor de for. Es comprensible: recorrer listas, diccionarios y rangos cubre la mayor parte del trabajo cotidiano. Sin embargo, en cuanto un programa tiene que esperar algo que no controla (un usuario que escribe, un servidor que responde, una cola que recibe mensajes, un archivo que crece), el bucle while vuelve a ser imprescindible. Y su variante while True, que muchos consideran un vicio de principiante, es en realidad la base de servidores, clientes de red, juegos y agentes que trabajan sin parar.

Si buscas un repaso de la sintaxis desde cero, en el blog ya tienes una guía sobre el uso correcto de bucles while en Python. Este artículo parte de ahí y se centra en otra pregunta: qué patrones reales se construyen con while y while True, cómo escribirlos de forma legible en Python moderno y qué errores conviene evitar cuando ese código llega a producción.

Todos los ejemplos funcionan en Python 3.15, la rama estable publicada en octubre de 2026 (con la versión 3.15.2 como parche más reciente en el momento de escribir), y la gran mayoría también en 3.12, 3.13 y 3.14, porque las piezas que usaremos llevan años en el lenguaje.

Repaso rápido: cómo evalúa Python un bucle while

Un bucle while ejecuta su bloque una y otra vez mientras una expresión sea verdadera. La condición se evalúa antes de cada vuelta, incluida la primera, por lo que el cuerpo puede no ejecutarse nunca.

saldo = 120

while saldo >= 50:
    saldo -= 50
    print(f"Retiro aprobado, saldo restante: {saldo}")

print("Fondos insuficientes para otro retiro")
Retiro aprobado, saldo restante: 70
Retiro aprobado, saldo restante: 20
Fondos insuficientes para otro retiro

Hay tres detalles que conviene tener presentes desde el principio:

  • La condición no tiene que ser un booleano literal. Python aplica sus reglas de veracidad: una lista vacía, el número cero, una cadena vacía y None cuentan como falsos. Por eso while pendientes: es una forma idiomática de procesar una lista hasta vaciarla.
  • El cuerpo debe modificar algo que afecte a la condición. Si nada cambia, el bucle no terminará jamás.
  • La sangría define el bloque. Una línea mal indentada puede quedar fuera del bucle y provocar justo el bucle infinito que querías evitar.
pendientes = ["factura-001", "factura-002", "factura-003"]

while pendientes:
    actual = pendientes.pop()
    print(f"Procesando {actual}")

Este fragmento aprovecha que una lista vacía es falsa: el bucle se detiene solo cuando no quedan elementos, sin necesidad de contadores.

while frente a for: elegir con criterio

La regla clásica dice que for se usa cuando sabes cuántas veces vas a repetir y while cuando no lo sabes. Es una buena aproximación, pero en la práctica la pregunta útil es otra: ¿existe una secuencia o un iterable que describa lo que voy a recorrer?

Si la respuesta es sí, usa for. Recorrer una lista con un índice manual dentro de un while es más largo, más lento y más propenso a errores:

# Evita este estilo
productos = ["teclado", "ratón", "monitor"]
i = 0
while i < len(productos):
    print(productos[i])
    i += 1

# Prefiere este
for producto in productos:
    print(producto)

Si la respuesta es no, porque la repetición depende de un evento externo, de un estado que cambia de forma impredecible o de una condición compuesta, while expresa mejor la intención. Algunos ejemplos típicos:

  • Pedir datos al usuario hasta que sean válidos.
  • Consultar una API hasta que un trabajo termine.
  • Leer bloques de un archivo o de un socket hasta que no quede nada.
  • Simular un proceso hasta que alcance un estado final, como un juego o un algoritmo de convergencia numérica.
  • Mantener vivo un proceso que atiende peticiones.

También hay un territorio intermedio en el que ambos encajan. Un algoritmo de búsqueda binaria, por ejemplo, se escribe de forma natural con while bajo <= alto: porque los límites se mueven en cada vuelta y no hay una secuencia que recorrer de principio a fin.

def busqueda_binaria(valores: list[int], objetivo: int) -> int:
    bajo, alto = 0, len(valores) - 1
    while bajo <= alto:
        medio = (bajo + alto) // 2
        if valores[medio] == objetivo:
            return medio
        if valores[medio] < objetivo:
            bajo = medio + 1
        else:
            alto = medio - 1
    return -1


print(busqueda_binaria([3, 8, 15, 21, 42, 57], 21))
3

while True: un bucle infinito controlado a propósito

while True crea un bucle cuya condición nunca deja de cumplirse. La única forma de salir es desde dentro: con break, con return si estás en una función, lanzando una excepción o terminando el proceso. Eso que suena peligroso es, bien usado, un bucle infinito controlado a propósito por el programador, y tiene una ventaja clara: permite colocar la comprobación de salida en el punto exacto del cuerpo donde tiene sentido, no obligatoriamente al principio.

Compara estas dos versiones de un menú de consola. En la primera, la condición está arriba y obliga a inicializar la variable con un valor artificial:

opcion = ""
while opcion != "q":
    opcion = input("[a]ñadir, [l]istar, [q]salir: ").strip().lower()
    if opcion == "a":
        print("Añadiendo tarea...")
    elif opcion == "l":
        print("Listando tareas...")

En la segunda, la salida está donde ocurre la decisión y no hace falta ningún valor inicial inventado:

while True:
    opcion = input("[a]ñadir, [l]istar, [q]salir: ").strip().lower()
    if opcion == "q":
        print("Hasta pronto")
        break
    if opcion == "a":
        print("Añadiendo tarea...")
    elif opcion == "l":
        print("Listando tareas...")
    else:
        print("Opción no reconocida")

La segunda versión se lee de arriba abajo como una historia: pido una opción, si es salir me voy, si no la proceso. Ese es el criterio para elegir while True: úsalo cuando la condición de salida se conoce a mitad del cuerpo, no al inicio.

Este mismo patrón es la forma estándar de emular en Python un bucle que se ejecuta al menos una vez, ya que el lenguaje no tiene una sentencia específica para ello. Si te interesa ese caso concreto, puedes ver cómo implementar un bucle do-while en Python con varias alternativas.

break y continue: salir o saltar con intención

break termina el bucle más interno en el que se encuentra. continue abandona la vuelta actual y salta directamente a evaluar la condición de nuevo. Ambas sentencias son útiles, pero su abuso convierte un bucle en un laberinto.

Un uso sano de continue es descartar casos al principio del cuerpo, como si fueran cláusulas de guarda:

lineas = [
    "# configuración de ejemplo",
    "",
    "puerto=8080",
    "host=0.0.0.0",
    "   ",
    "debug=false",
]

configuracion: dict[str, str] = {}
indice = 0

while indice < len(lineas):
    linea = lineas[indice].strip()
    indice += 1
    if not linea or linea.startswith("#"):
        continue
    clave, _, valor = linea.partition("=")
    configuracion[clave] = valor

print(configuracion)
{'puerto': '8080', 'host': '0.0.0.0', 'debug': 'false'}

Fíjate en un detalle importante: el incremento de indice está antes del continue. Si lo colocaras al final del cuerpo, cualquier línea vacía haría que el bucle saltara el incremento y se quedara atrapado para siempre en la misma posición. Es uno de los errores más comunes con continue dentro de un while, y no existe en un for porque allí el avance lo gestiona el propio iterador.

En cuanto a break, recuerda que solo sale de un nivel. Si tienes bucles anidados y necesitas salir de todos, la solución más limpia suele ser extraer el código a una función y usar return:

def buscar_asiento_libre(sala: list[list[str]]) -> tuple[int, int] | None:
    fila = 0
    while fila < len(sala):
        columna = 0
        while columna < len(sala[fila]):
            if sala[fila][columna] == "libre":
                return fila, columna
            columna += 1
        fila += 1
    return None


sala = [
    ["ocupado", "ocupado", "ocupado"],
    ["ocupado", "libre", "ocupado"],
]
print(buscar_asiento_libre(sala))
(1, 1)

La cláusula else del while: la gran desconocida

Python permite añadir un bloque else a un bucle while. Ese bloque se ejecuta cuando la condición se vuelve falsa de forma natural, y no se ejecuta si el bucle termina con break. La cláusula else del while resulta muy útil para distinguir entre “terminé porque encontré lo que buscaba” y “terminé porque se agotaron los intentos”.

import random

intentos_restantes = 5
objetivo = random.randint(1, 20)

while intentos_restantes > 0:
    intento = int(input(f"Adivina el número (quedan {intentos_restantes}): "))
    if intento == objetivo:
        print("¡Acertaste!")
        break
    print("Más alto" if intento < objetivo else "Más bajo")
    intentos_restantes -= 1
else:
    print(f"Sin intentos. El número era {objetivo}")

Sin else, tendrías que crear una variable booleana como acertado = False, actualizarla dentro del bucle y comprobarla al final. Con else, la lógica queda en su sitio.

El nombre else confunde a mucha gente, porque sugiere que se ejecuta “si el bucle no se ejecutó”. Una forma de recordarlo es leerlo como “si no hubo break”. Si en tu equipo genera dudas, añade un comentario breve como # se ejecuta solo si no hubo break. Lo que no conviene hacer es usarlo en bucles while True, porque allí la condición nunca se vuelve falsa y el else sería código muerto.

Validar la entrada del usuario sin repetir código

Una de las tareas donde while True brilla es en validar la entrada del usuario de forma robusta. El planteamiento ingenuo repite la llamada a input() antes del bucle y dentro de él; el planteamiento con while True la escribe una sola vez y combina la conversión de tipos con el manejo de excepciones.

def pedir_entero(mensaje: str, minimo: int, maximo: int) -> int:
    while True:
        texto = input(mensaje).strip()
        try:
            valor = int(texto)
        except ValueError:
            print(f"'{texto}' no es un número entero, inténtalo de nuevo.")
            continue
        if minimo <= valor <= maximo:
            return valor
        print(f"El valor debe estar entre {minimo} y {maximo}.")


edad = pedir_entero("Introduce tu edad: ", 0, 130)
print(f"Edad registrada: {edad}")
Introduce tu edad: veinte
'veinte' no es un número entero, inténtalo de nuevo.
Introduce tu edad: 200
El valor debe estar entre 0 y 130.
Introduce tu edad: 34
Edad registrada: 34

Algunas buenas prácticas que este ejemplo aplica:

  • La función devuelve el valor con return, que sale del bucle y de la función a la vez. No hace falta break.
  • El bloque try envuelve solo la conversión, no todo el cuerpo. Así no ocultas errores ajenos a la validación.
  • Los mensajes de error explican qué está mal y qué se espera.

En scripts que se ejecutan en terminal también conviene contemplar que el usuario pulse Ctrl+C o Ctrl+D. La primera combinación lanza KeyboardInterrupt y la segunda provoca EOFError al llamar a input(). Si quieres una salida limpia en lugar de un rastro de error, captúralas fuera del bucle:

try:
    edad = pedir_entero("Introduce tu edad: ", 0, 130)
except (KeyboardInterrupt, EOFError):
    print("\nOperación cancelada por el usuario")
    raise SystemExit(1)

Centinelas: detenerse con un valor especial

Un centinela es un valor que marca el final de una secuencia de datos. El caso típico es pedir números hasta que el usuario escriba una palabra concreta, o leer de una fuente que devuelve un valor vacío cuando ya no tiene nada más.

FIN = "fin"
gastos: list[float] = []

while True:
    entrada = input("Importe del gasto (o 'fin'): ").strip().lower()
    if entrada == FIN:
        break
    try:
        gastos.append(float(entrada))
    except ValueError:
        print("Importe no válido")

print(f"Total: {sum(gastos):.2f} en {len(gastos)} gastos")

Python ofrece además una forma muy compacta de iterar con centinela que mucha gente desconoce: la función iter() con dos argumentos. Si le pasas una función sin argumentos y un valor centinela, crea un iterador que llama a la función repetidamente hasta que devuelve ese valor. Eso convierte un while True en un for limpio:

from functools import partial

with open("registro.bin", "rb") as archivo:
    for bloque in iter(partial(archivo.read, 4096), b""):
        print(f"Leídos {len(bloque)} bytes")

Cuando el centinela no puede ser un valor del dominio (por ejemplo, porque None o la cadena vacía son datos válidos), la convención tradicional es crear un objeto único con object() y comparar por identidad con is. Python 3.15 incorpora además un tipo integrado para centinelas, fruto de la PEP 661, pensado precisamente para que estos marcadores tengan una representación legible y se comporten bien con los verificadores de tipos; si tu proyecto ya exige 3.15, merece la pena revisarlo en la documentación oficial antes de seguir con el truco de object().

_SIN_DATOS = object()

def siguiente_mensaje(cola: list) -> object:
    return cola.pop(0) if cola else _SIN_DATOS

cola = ["hola", None, "", "adiós"]

while (mensaje := siguiente_mensaje(cola)) is not _SIN_DATOS:
    print(repr(mensaje))
'hola'
None
''
'adiós'

Observa que None y la cadena vacía se procesan como datos normales: el bucle solo termina con el objeto centinela.

El operador morsa: asignar y comprobar en la condición

El último ejemplo ha usado el operador morsa de Python (:=), disponible desde la versión 3.8. Permite asignar un valor dentro de una expresión, lo que encaja perfectamente con los bucles while que leen datos y comprueban si se agotaron.

Sin morsa, el patrón de lectura por bloques obliga a duplicar la llamada o a recurrir a while True con break:

# Versión con duplicación
bloque = archivo.read(1024)
while bloque:
    procesar(bloque)
    bloque = archivo.read(1024)

Con morsa, la lectura y la comprobación quedan en una sola línea:

while bloque := archivo.read(1024):
    procesar(bloque)

Otro caso frecuente es el de expresiones regulares que se aplican repetidamente, o el consumo de una cola hasta vaciarla:

import queue

trabajos: queue.Queue[str] = queue.Queue()
for nombre in ("thumbnail-1", "thumbnail-2", "thumbnail-3"):
    trabajos.put(nombre)

def tomar(cola: queue.Queue[str]) -> str | None:
    try:
        return cola.get_nowait()
    except queue.Empty:
        return None

while (trabajo := tomar(trabajos)) is not None:
    print(f"Generando {trabajo}")

Algunas recomendaciones para usar la morsa con cabeza:

  • Envuelve la asignación entre paréntesis cuando la compares con algo: while (x := f()) is not None:. Sin paréntesis, la precedencia haría que x guardara el resultado de la comparación, no el de la función.
  • Úsala cuando elimine duplicación real. Si la línea se vuelve difícil de leer, un while True con break es igual de válido.
  • No la encadenes con varias asignaciones en la misma condición; la legibilidad se resiente rápidamente.

Reintentos con backoff exponencial

Cualquier programa que habla con la red se encontrará con fallos transitorios: tiempos de espera agotados, servicios que devuelven errores 503 o límites de peticiones. La respuesta profesional no es reintentar de inmediato y sin límite, sino aplicar reintentos con backoff exponencial y un poco de aleatoriedad (jitter) para no saturar al servidor ni sincronizar a todos los clientes.

Un bucle while es la estructura natural para este patrón, porque el número de vueltas depende de cuándo llegue el éxito:

import random
import time


class ErrorTransitorio(Exception):
    """Fallo que merece la pena reintentar."""


def llamar_servicio() -> str:
    if random.random() < 0.6:
        raise ErrorTransitorio("Servicio no disponible")
    return "respuesta correcta"


def con_reintentos(
    funcion,
    max_intentos: int = 5,
    espera_base: float = 0.5,
    espera_maxima: float = 8.0,
):
    intento = 1
    while True:
        try:
            return funcion()
        except ErrorTransitorio as error:
            if intento >= max_intentos:
                raise RuntimeError(
                    f"Fallo tras {max_intentos} intentos"
                ) from error
            espera = min(espera_maxima, espera_base * 2 ** (intento - 1))
            espera = random.uniform(0, espera)  # jitter completo
            print(f"Intento {intento} fallido: {error}. Esperando {espera:.2f}s")
            time.sleep(espera)
            intento += 1


print(con_reintentos(llamar_servicio))
Intento 1 fallido: Servicio no disponible. Esperando 0.31s
Intento 2 fallido: Servicio no disponible. Esperando 0.74s
respuesta correcta

Los elementos clave de este diseño son:

  • Un límite de intentos. Sin él, while True se convierte en un bucle infinito real en cuanto el servicio cae durante horas.
  • Una espera máxima, para que el crecimiento exponencial no produzca pausas absurdas.
  • Jitter aleatorio, que reparte los reintentos de muchos clientes en el tiempo.
  • Una excepción final con from error, que conserva la causa original en el rastro y facilita la depuración.
  • Reintentar solo errores transitorios. Un error de autenticación o un dato mal formado no se arreglará esperando.

En proyectos grandes es razonable delegar esta lógica en librerías especializadas como tenacity, o en los mecanismos de reintento que traen clientes HTTP como httpx o urllib3. Aun así, entender el bucle que hay debajo te ayuda a configurarlas correctamente y a detectar cuándo se están comportando mal.

Sondeo y esperas con límite de tiempo

Un caso parecido es el sondeo (polling): consultar periódicamente el estado de algo hasta que cambie. Por ejemplo, esperar a que un trabajo de exportación termine. Aquí lo importante es poner un límite de tiempo y medirlo con un reloj monótono, que no se ve afectado por cambios en la hora del sistema:

import time


def esperar_hasta(comprobar, tiempo_limite: float, intervalo: float = 1.0) -> bool:
    fin = time.monotonic() + tiempo_limite
    while time.monotonic() < fin:
        if comprobar():
            return True
        time.sleep(intervalo)
    return False


inicio = time.monotonic()
listo = esperar_hasta(lambda: time.monotonic() - inicio > 3, tiempo_limite=10)
print("Exportación lista" if listo else "Tiempo de espera agotado")

Usar time.time() en lugar de time.monotonic() es un error sutil: si el sistema sincroniza su reloj por NTP mientras esperas, el bucle puede terminar antes de tiempo o alargarse inesperadamente.

Bucles de eventos y procesos de larga duración

Si alguna vez has abierto el código de un servidor, un bot o un juego, habrás encontrado un while True en el corazón del programa. Es el bucle de eventos: espera algo, lo procesa y vuelve a esperar. En Python moderno, buena parte de ese trabajo lo hace asyncio, pero el patrón sigue estando ahí y a menudo lo escribirás tú mismo.

Un trabajador que consume tareas de una cola y se detiene de forma ordenada cuando recibe una señal podría ser así:

import queue
import signal
import threading
import time

detener = threading.Event()
tareas: queue.Queue[str] = queue.Queue()


def manejar_senal(signum, frame):
    print("Señal recibida, terminando tras la tarea actual...")
    detener.set()


signal.signal(signal.SIGINT, manejar_senal)
signal.signal(signal.SIGTERM, manejar_senal)

for numero in range(1, 4):
    tareas.put(f"email-{numero}")

while not detener.is_set():
    try:
        tarea = tareas.get(timeout=1)
    except queue.Empty:
        continue
    print(f"Enviando {tarea}")
    time.sleep(0.5)
    tareas.task_done()

print("Trabajador detenido correctamente")

Hay varias decisiones importantes en este fragmento. La condición while not detener.is_set() permite apagar el bucle desde fuera sin matar el proceso a mitad de una tarea. El get(timeout=1) evita que el bucle se quede bloqueado indefinidamente esperando trabajo, lo que le da la oportunidad de revisar la bandera de parada cada segundo. Y el manejo de SIGTERM es lo que permite que contenedores y orquestadores detengan el proceso con elegancia.

La versión asíncrona sigue la misma idea, pero cediendo el control con await en lugar de bloquear el hilo:

import asyncio


async def latido(intervalo: float, parada: asyncio.Event) -> None:
    contador = 0
    while not parada.is_set():
        contador += 1
        print(f"Latido {contador}")
        try:
            await asyncio.wait_for(parada.wait(), timeout=intervalo)
        except TimeoutError:
            pass


async def main() -> None:
    parada = asyncio.Event()
    tarea = asyncio.create_task(latido(0.5, parada))
    await asyncio.sleep(2)
    parada.set()
    await tarea


asyncio.run(main())

Desde Python 3.11, asyncio.TimeoutError es un alias del TimeoutError integrado, así que capturar este último es la forma recomendada en código actual. Lo que nunca debes hacer dentro de una corrutina es un while True sin ningún await: bloquearía el bucle de eventos entero y ninguna otra tarea podría avanzar.

Errores comunes y cómo detectarlos

Estos son los fallos que más se repiten en revisiones de código relacionadas con while:

  • Olvidar actualizar la variable de control. El bucle nunca termina. Revisa siempre que cada rama del cuerpo avance hacia la condición de salida, incluidas las que terminan en continue.
  • Comparar números de coma flotante con igualdad exacta. Una condición como while x != 1.0: con incrementos de 0.1 puede no cumplirse jamás por errores de redondeo. Usa <, > o math.isclose().
  • Modificar una lista mientras se recorre por índice. Si eliminas elementos dentro de un while i < len(lista), los índices se desplazan y te saltarás elementos. Recorre una copia o construye una lista nueva.
  • while True sin límite en operaciones de red. Un servicio caído convierte el programa en un proceso zombi que consume CPU y llena los registros.
  • Bucles de espera activa. Un while not listo: pass consume el cien por cien de un núcleo. Añade time.sleep(), usa threading.Event.wait() o una primitiva de sincronización adecuada.
  • Capturar todas las excepciones dentro del bucle. Un except Exception: continue oculta errores de programación y puede convertir un fallo puntual en un bucle infinito silencioso.
import math

x = 0.0
pasos = 0
while not math.isclose(x, 1.0):
    x += 0.1
    pasos += 1
    if pasos > 100:  # red de seguridad durante el desarrollo
        raise RuntimeError("La condición nunca se cumplió")

print(f"Llegamos a {x} en {pasos} pasos")
Llegamos a 0.9999999999999999 en 10 pasos

Si un script se te queda colgado en la terminal, Ctrl+C lanza KeyboardInterrupt y detiene el bucle en la mayoría de los casos. Cuando ni eso funciona (por ejemplo, porque el código captura la excepción por error), puedes terminar el proceso desde otra terminal con kill en Linux y macOS o desde el Administrador de tareas en Windows. Para entender dónde estaba atascado, faulthandler es tu aliado:

python -X faulthandler mi_script.py

Con esta opción activada, Python vuelca la pila de llamadas de todos los hilos si el proceso recibe ciertas señales fatales, y puedes registrar un volcado bajo demanda con faulthandler.register() en sistemas Unix.

Rendimiento: cuándo importa y cuándo no

En CPython, un bucle while con contador manual suele ser algo más lento que el for equivalente, porque cada vuelta ejecuta en Python la comparación y el incremento, mientras que for sobre range() delega el avance en código C. Puedes comprobarlo tú mismo con timeit:

import timeit

codigo_while = """
i = 0
total = 0
while i < 1_000_000:
    total += i
    i += 1
"""

codigo_for = """
total = 0
for i in range(1_000_000):
    total += i
"""

print("while:", timeit.timeit(codigo_while, number=10))
print("for:  ", timeit.timeit(codigo_for, number=10))
print("sum:  ", timeit.timeit("sum(range(1_000_000))", number=10))

Los números exactos dependen de tu máquina y de la versión del intérprete, pero el orden suele mantenerse: while es el más lento, for va algo por delante y la función integrada sum() gana con diferencia. El intérprete especializado que llegó en 3.11 y el compilador JIT experimental, que sigue mejorando en 3.15, han reducido distancias, pero no cambian la recomendación general.

Las conclusiones prácticas son sencillas:

  • No sustituyas un for por un while buscando velocidad; ocurre justo lo contrario.
  • En bucles que esperan red, disco o al usuario, el coste del propio bucle es irrelevante comparado con la espera.
  • Si un bucle numérico es el cuello de botella, piensa antes en funciones integradas, comprensiones o librerías vectorizadas como NumPy que en microoptimizar el while.
  • Para medir dónde se va el tiempo en un programa real, usa un perfilador. Python 3.15 incluye un perfilador estadístico por muestreo en la biblioteca estándar, además de los clásicos cProfile y profile.

Buenas prácticas para código de producción

Para cerrar la parte técnica, estas son las pautas que conviene aplicar cuando un bucle while forma parte de un servicio real:

  • Haz explícita la condición de salida. Si usas while True, que el break o el return sean fáciles de encontrar, idealmente cerca del principio del cuerpo.
  • Pon límites siempre que dependas de algo externo: número máximo de intentos, tiempo máximo de espera o tamaño máximo de datos.
  • Extrae los bucles complejos a funciones con nombre. esperar_exportacion() comunica mucho más que un bloque de veinte líneas en mitad de otra función.
  • Registra lo que ocurre en cada reintento o vuelta significativa con logging, no con print, para poder diagnosticar problemas en producción.
  • Escribe pruebas que cubran la salida del bucle: el caso de éxito inmediato, el de éxito tras varios intentos y el de agotamiento. Inyectar la función de espera (por ejemplo, pasando time.sleep como parámetro) permite que esas pruebas se ejecuten en milisegundos.
  • Prefiere for cuando exista un iterable. Muchos while se pueden reescribir con iter() y centinela, con generadores o con itertools, y el resultado suele ser más claro.
def con_reintentos_testeable(funcion, max_intentos=3, dormir=time.sleep):
    intento = 1
    while True:
        try:
            return funcion()
        except ErrorTransitorio:
            if intento >= max_intentos:
                raise
            dormir(0.1 * 2 ** intento)
            intento += 1


# En las pruebas:
llamadas = []
resultado = con_reintentos_testeable(
    lambda: "ok",
    dormir=lambda segundos: llamadas.append(segundos),
)
assert resultado == "ok" and llamadas == []

Casos de uso donde while es la mejor opción

Para fijar ideas, este es un resumen de escenarios reales en los que el bucle while resulta la herramienta más adecuada:

  • Interfaces de consola interactivas: menús, asistentes de configuración y juegos de texto.
  • Clientes de red con reconexión automática tras una caída.
  • Consumidores de colas de mensajes que deben ejecutarse de forma indefinida y apagarse limpiamente.
  • Algoritmos iterativos que terminan al converger, como el método de Newton para aproximar raíces o los descensos de gradiente simples.
  • Lectura incremental de archivos grandes o flujos de datos por bloques.
  • Herramientas que siguen un archivo de registro a medida que crece, al estilo de tail -f.

Como ejemplo final, aquí tienes una aproximación de la raíz cuadrada con el método de Newton, un caso clásico de bucle que se detiene cuando la mejora ya no es significativa:

def raiz_cuadrada(numero: float, tolerancia: float = 1e-10) -> float:
    if numero < 0:
        raise ValueError("No se admiten números negativos")
    if numero == 0:
        return 0.0
    estimacion = numero
    while abs(estimacion * estimacion - numero) > tolerancia:
        estimacion = (estimacion + numero / estimacion) / 2
    return estimacion


print(raiz_cuadrada(2))
print(raiz_cuadrada(1764))
1.4142135623746899
42.0

Si estás empezando y quieres situar los bucles dentro de un recorrido más amplio por el lenguaje, la guía completa para aprender Python desde cero en 2026 te ayudará a ordenar los siguientes pasos: funciones, estructuras de datos, módulos y pruebas.

Conclusiones

El bucle while es mucho más que la alternativa al for para cuando no sabemos cuántas vueltas dar. Es la estructura que usamos cuando un programa depende del mundo exterior: de lo que escribe un usuario, de lo que responde un servidor o de lo que llega a una cola. Y while True, lejos de ser una mala práctica, es la forma más legible de expresar un bucle cuya salida se decide a mitad del cuerpo.

Lo que separa un bucle de juguete de uno listo para producción son los detalles: condiciones de salida claras, límites de intentos y de tiempo, esperas con time.monotonic(), backoff con jitter, apagado ordenado mediante eventos y señales, y pruebas que verifiquen cómo termina. Herramientas como la cláusula else, el operador morsa y iter() con centinela ayudan a escribir ese código con menos variables auxiliares y menos duplicación.

Como regla final, pregúntate siempre si existe un iterable que describa lo que quieres recorrer. Si existe, usa for. Si no, escribe un while con una salida que cualquier compañero pueda encontrar en cinco segundos, y tu código será más fácil de mantener en Python 3.15 y en las versiones que vengan después.