Skip to content
This page has been auto-translated and may contain errors.View in English

Flujo de control

Cada programa que has escrito hasta ahora se ejecuta de la misma manera todas las veces: de arriba hacia abajo, una línea a la vez. Eso funciona para scripts simples, pero los programas reales necesitan tomar decisiones y repetir tareas. Un cuestionario necesita verificar si la respuesta es correcta. Un juego necesita seguir corriendo hasta que el jugador gane o pierda. Este capítulo cubre cómo hacer que tu programa se bifurque y repita.

El flujo de control da forma a la ruta de ejecución de tu programa. Las condiciones (if/elif/else) seleccionan entre ramas. Los bucles (while, for) repiten bloques. El bucle for de Python es un protocolo iterador, no un contador basado en índices. Entender tanto la sintaxis como los modelos subyacentes hace que tu código sea más limpio.

Las primitivas de flujo de control de Python son if/elif/else, while y for. for invoca __iter__ en el iterable y llama a next() hasta StopIteration. while evalúa un objeto de condición a través de __bool__ o __len__. break y continue afectan al bucle envolvente más cercano; loop-else se ejecuta solo cuando el bucle se agota sin un break.

Comparaciones

Antes de poder tomar una decisión, necesitas comparar cosas. Los operadores de comparación devuelven True o False. El más importante de entender desde el inicio: = asigna un valor, == verifica si dos valores son iguales. Confundirlos es uno de los errores más comunes de los principiantes.

Los operadores de comparación llaman a los métodos dunder correspondientes (__eq__, __lt__, etc.) y devuelven un bool. Python admite comparaciones encadenadas: 0 < x < 10 se evalúa como 0 < x and x < 10, no de izquierda a derecha como en la mayoría de los lenguajes. La comparación de strings es lexicográfica, basada en los puntos de código Unicode.

Los operadores de comparación llaman a métodos de comparación enriquecida: __eq__, __ne__, __lt__, __le__, __gt__, __ge__. Python admite comparaciones encadenadas que cortocircuitan: a < b < c se convierte en a < b and b < c, pero b se evalúa solo una vez. is verifica la identidad del objeto (igualdad de id()), no la igualdad de valor; usa == para comparaciones de valor e is solo para None, True, False.

python
5 > 3     # True
5 < 3     # False
5 == 5    # True   (nota: doble igual; = es asignación, == es comparación)
5 != 3    # True   ("distinto de")
5 >= 5    # True   ("mayor o igual que")
5 <= 4    # False  ("menor o igual que")

La distinción entre = y == confunde a casi todos al principio. La asignación (=) almacena un valor; la comparación (==) verifica si dos valores son iguales.

También puedes comparar strings. Python los compara alfabéticamente:

python
"manzana" == "manzana"   # True
"manzana" < "banana"     # False  (b viene antes que m)
"manzana" == "Manzana"   # False (distingue mayúsculas y minúsculas)

Combinando condiciones

and, or y not combinan comparaciones. and requiere que ambos lados sean verdaderos. or requiere al menos un lado. not invierte el resultado. Estos te permiten expresar condiciones del mundo real como "el puntaje es aprobatorio Y el usuario está activo".

and y or cortocircuitan: and se detiene en el primer operando falso, or se detiene en el primer operando verdadero. Devuelven el valor real del operando, no solo True o False. not llama a __bool__ en el operando y devuelve el resultado invertido.

and evalúa de izquierda a derecha y devuelve el primer operando falso, o el último operando si todos son verdaderos. or devuelve el primer operando verdadero, o el último si todos son falsos. Este comportamiento de cortocircuito es una garantía: el lado derecho no se evalúa si el lado izquierdo determina el resultado. not x es equivalente a True if not bool(x) else False, pero Python lo optimiza.

python
edad    = 25
puntaje = 88

edad >= 18 and puntaje >= 80    # True  (ambos deben ser verdaderos)
edad < 18 or puntaje >= 80      # True  (al menos uno debe ser verdadero)
not edad >= 18                  # False (invierte el resultado)

and requiere ambos lados. or requiere al menos un lado. not invierte.

Verdadero y falso (truthy y falsy)

Cada valor en Python tiene una interpretación booleana, incluso si no es True o False. Los strings vacíos, el cero, las listas vacías y None se comportan como False en una condición. Todo lo demás se comporta como True. Esto significa que if results: verifica si una lista no está vacía sin tener que escribir if len(results) > 0:.

Reglas de veracidad de Python: los valores falsos son False, 0, 0.0, "", [], (), {}, set() y None. Todo lo demás es verdadero. Las condiciones llaman a __bool__ en el objeto, recurriendo a __len__ si __bool__ no está definido. Un objeto con __len__ de longitud cero es falso.

La prueba de verdad llama a __bool__. Si __bool__ no está definido, Python recurre a __len__: un objeto con __len__ == 0 es falso. Las clases personalizadas controlan la veracidad implementando cualquiera de los dos métodos. Los valores falsos estándar son: False, 0, 0.0, 0j, "", b"", [], (), {}, set(), frozenset(), None y cualquier objeto cuyo __bool__ devuelva False o __len__ devuelva 0.

python
# Todos estos se comportan como False en una condición:
False, 0, 0.0, "", [], {}, (), None

# Todo lo demás se comporta como True

Esto significa que if results: es una forma natural de decir "si la lista no está vacía", y if name: verifica si un string tiene algún contenido.

if / elif / else

La sentencia if ejecuta un bloque de código solo cuando su condición es True. elif agrega más condiciones para verificar si la primera fue falsa. else captura todo lo que no coincidió con ninguna condición. Python usa indentación, no llaves, para definir qué pertenece dentro de cada bloque.

if/elif/else evalúa las condiciones de arriba hacia abajo y ejecuta el primer bloque coincidente. Python usa indentación (4 espacios por convención) para definir el alcance del bloque; una indentación inconsistente es un SyntaxError. Solo se ejecuta una rama: una vez que una condición coincide, todos los elif y else posteriores se omiten.

Python usa la indentación como delimitadores de bloque, aplicada por el parser. El intérprete genera bytecodes SETUP_BLOCK para cada rama; exactamente una rama se ejecuta. elif es azúcar sintáctico: evita el anidamiento que crearía una cadena de sentencias if simples. Cada condición se evalúa de forma diferida, solo si todas las condiciones precedentes fueron falsas.

python
puntaje = 87

if puntaje >= 90:
    print("Calificación A")
elif puntaje >= 80:
    print("Calificación B")
elif puntaje >= 70:
    print("Calificación C")
else:
    print("Por debajo de C")

Las reglas:

  • if es obligatorio y siempre va primero
  • elif (abreviatura de "else if") es opcional y puedes tener tantos como necesites
  • else es opcional, maneja todo lo que no coincidió y va al final
  • Python usa indentación (4 espacios) para marcar qué pertenece dentro de cada bloque; no hay llaves

La indentación no es opcional ni cosmética. Python la usa para definir la estructura. La indentación inconsistente es un error de sintaxis.

Condiciones de una línea

Para asignaciones simples de sí/no, Python tiene una forma compacta de una línea llamada expresión ternaria: valor_si_verdadero if condicion else valor_si_falso. Úsala solo cuando la lógica sea genuinamente simple y se lea como una oración.

La expresión condicional (operador ternario) evalúa a uno de dos valores según una condición. Es una expresión, no una sentencia, por lo que puede aparecer en cualquier lugar donde se espere un valor: dentro de una f-string, como argumento de función, en una asignación. Úsala para casos simples de sí/no; para cualquier cosa que involucre elif, escribe la versión completa.

La expresión condicional x if condition else y es una sola expresión que evalúa la condición y devuelve x o y sin ejecutar la otra rama. Se mapea a una combinación de bytecodes POP_JUMP_IF_FALSE. A diferencia de los bloques if/else, no puede abarcar múltiples sentencias y no puede contener elif; para ramificaciones complejas, los bloques completos if/elif/else son más claros.

python
etiqueta = "aprobado" if puntaje >= 50 else "reprobado"

Esta es una expresión ternaria; se lee como una oración. Úsala cuando la lógica sea genuinamente simple. Para cualquier cosa que involucre elif, escribe la versión completa.

Bucles while

Un bucle while repite su bloque mientras su condición sea True. Úsalo cuando no sepas de antemano cuántas veces debe ejecutarse el bucle, por ejemplo esperando una entrada válida o reintentando hasta que una tarea tenga éxito.

while evalúa su condición antes de cada iteración y ejecuta el bloque solo cuando la condición es verdadera. Úsalo para bucles donde la condición de salida depende de algo que cambia dentro del bucle. Cuando el número de iteraciones es conocido o estás iterando una colección, for suele ser más limpio.

while llama a __bool__ (o __len__) en la expresión de condición antes de cada iteración. El cuerpo puede modificar la condición. while True con un break interior es el patrón idiomático "loop-until" cuando la condición de salida debe evaluarse en el medio o al final del cuerpo. Los bucles infinitos a los que les falta un break son una fuente común de bloqueos.

python
vidas = 3

while vidas > 0:
    print(f"Vidas restantes: {vidas}")
    vidas -= 1

print("Fin del juego")

while es mejor cuando no sabes de antemano cuántas veces se ejecutará el bucle. Cuando sí lo sabes, o cuando estás iterando sobre una colección, for es más limpio.

break y continue

break sale del bucle inmediatamente, sin importar cuántas iteraciones queden. continue omite el resto de la iteración actual y salta de regreso a la verificación de la condición. Ambos solo afectan al bucle más interno en el que están.

break termina el bucle envolvente más cercano, transfiriendo el control a la primera sentencia después de él. continue omite el resto del cuerpo del bucle actual y reinicia desde la verificación de la condición (o la siguiente iteración en un bucle for). Ambos solo afectan al bucle envolvente más interno.

break emite un bytecode BREAK_LOOP que sale del bloque de código del bucle y omite cualquier cláusula else asociada. continue emite CONTINUE_LOOP (o JUMP_ABSOLUTE dependiendo del contexto), reanudando desde el encabezado del bucle. Ambos están limitados al bucle envolvente más cercano; no hay break etiquetado en Python. Para salir de bucles anidados, usa una bandera booleana o reestructura en una función con return.

break sale del bucle inmediatamente:

python
objetivo = 5
num      = 0

while True:
    num += 1
    if num == objetivo:
        print(f"Encontrado {objetivo}")
        break   # detiene el bucle

while True: con un break es un patrón válido y común cuando la condición de salida es compleja o necesita ocurrir al final del cuerpo del bucle.

continue omite el resto de la iteración actual y vuelve a la verificación de la condición:

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # omite los números pares
    print(num)      # solo imprime los impares: 1, 3, 5, 7, 9

Bucles for

Un bucle for recorre una secuencia un elemento a la vez: una lista, un string, un rango de números. La variable que nombras después de for recibe cada elemento por turno. Tú no gestionas un contador ni verificas la longitud.

for invoca iter() en el iterable para obtener un iterador, luego llama a next() en él hasta StopIteration. Esto significa que for funciona con cualquier cosa que implemente el protocolo iterador: listas, strings, diccionarios, archivos, rangos y objetos personalizados. No está limitado a secuencias indexadas.

for target in iterable llama a iter(iterable) para obtener un objeto iterador, luego llama repetidamente a next(iterator) y vincula el resultado a target hasta que se lance StopIteration. El bucle for atrapa StopIteration internamente. Cualquier objeto que implemente __iter__ (o ambos __iter__ y __next__) es iterable. Esto incluye objetos perezosos como generadores y objetos de archivo.

python
jugadores = ["Sofía", "Mateo", "Valentina"]

for jugador in jugadores:
    print(f"¡Hola, {jugador}!")

Los bucles for también funcionan con strings (iterando carácter por carácter) y con cualquier otro tipo de secuencia.

range()

range() genera una secuencia de números para que recorras. range(5) te da 0, 1, 2, 3, 4. Puedes controlar el inicio, el final y el tamaño del paso. Úsalo cuando necesites que un bucle se ejecute un número específico de veces.

range(start, stop, step) produce enteros desde start hasta (pero sin incluir) stop, avanzando por step. Es una secuencia perezosa: no crea una lista, genera números bajo demanda. Esto hace que range(10_000_000) sea eficiente en memoria. Las tres formas aceptan argumentos negativos para contar al revés.

range es un tipo, no una función. range(n) crea un objeto range que calcula la membresía y la indexación en O(1) sin materializar la secuencia. Admite len(), in, slicing e iteración inversa. Internamente solo almacena start, stop y step. Prefiérelo a list(range(n)) cuando solo necesites iteración.

python
for i in range(5):
    print(i)    # 0, 1, 2, 3, 4

range() tiene tres formas:

LlamadaLo que produce
range(5)0, 1, 2, 3, 4
range(2, 6)2, 3, 4, 5
range(0, 10, 2)0, 2, 4, 6, 8 (paso de 2)
range(5, 0, -1)5, 4, 3, 2, 1 (contando hacia atrás)

range() no crea una lista. Produce números uno a la vez, lo que es eficiente incluso para rangos muy grandes.

enumerate()

enumerate() te da tanto el índice como el valor mientras recorres, así no necesitas llevar un contador por separado. La parte i, jugador recibe automáticamente un par de valores en cada iteración.

enumerate(iterable, start=0) envuelve cualquier iterador y produce tuplas (índice, valor). El parámetro start desplaza el contador pero no cambia el índice subyacente. Prefiere enumerate() sobre gestionar una variable contadora; es más limpio y menos propenso a errores.

enumerate envuelve cualquier iterador en un objeto enumerate que produce pares (count, value). El argumento start establece el valor inicial del contador. El desempaque en el encabezado del for (for i, v in enumerate(...)) funciona porque cada elemento producido es una tupla. enumerate es O(1) por paso sin asignación de memoria adicional más allá del contador.

python
jugadores = ["Sofía", "Mateo", "Valentina"]

for i, jugador in enumerate(jugadores):
    print(f"{i + 1}. {jugador}")
# 1. Sofía
# 2. Mateo
# 3. Valentina

La sintaxis i, jugador se llama desempaque. Python divide el par (índice, valor) en dos nombres automáticamente.

Por defecto enumerate() comienza en 0. Pasa un valor inicial para cambiarlo:

python
for i, jugador in enumerate(jugadores, start=1):
    print(f"{i}. {jugador}")    # comienza en 1

Bucles anidados

Puedes poner un bucle dentro de otro bucle. El bucle interno se ejecuta completamente por cada iteración del bucle externo. Así es como procesas cuadrículas, combinaciones o cualquier dato con dos niveles de estructura.

Los bucles anidados tienen un conteo de iteraciones O(m × n) para una longitud externa m y una longitud interna n. break y continue dentro de un bucle anidado solo afectan al bucle más interno. Para salir de múltiples niveles, usa una variable bandera o reestructura en una función.

Cada llamada de bucle for crea un nuevo objeto iterador. Los bucles anidados componen sus iteradores de forma independiente. break solo sale del bucle más interno; no hay break etiquetado en Python. La solución común es una variable bandera o encapsular el bucle interno en una función y usar return. Para productos cartesianos, itertools.product es más legible que los bucles for anidados.

python
filas     = [1, 2, 3]
columnas  = ["A", "B"]

for fila in filas:
    for col in columnas:
        print(f"{col}{fila}", end=" ")
    print()   # nueva línea después de cada fila
# A1 B1
# A2 B2
# A3 B3

break y continue dentro de un bucle anidado solo afectan al bucle más interno.

Loop-else

Los bucles de Python pueden tener una cláusula else que se ejecuta solo si el bucle terminó sin encontrar un break. No se usa comúnmente, pero es la forma más limpia de escribir "buscar en una lista, y si no se encontró nada, hacer esto".

El else en un bucle for o while se ejecuta si el bucle se completa normalmente (agota el iterable o la condición se vuelve falsa) sin encontrar un break. Es el patrón idiomático para "buscar y reportar si no se encuentra" sin necesidad de una variable bandera separada.

El else del bucle está controlado por el bytecode BREAK_LOOP: si se dispara un break, no se ingresa al código de configuración del bloque else. Si el bucle se agota, el bloque else se ejecuta. Esto es semánticamente diferente de un else simple en un if. El principal uso práctico es el patrón de búsqueda con break; fuera de eso, se ve raramente y puede confundir a lectores no familiarizados.

python
objetivo = "Diego"
nombres  = ["Sofía", "Mateo", "Valentina"]

for nombre in nombres:
    if nombre == objetivo:
        print(f"Encontrado {objetivo}")
        break
else:
    print(f"{objetivo} no está en la lista")   # se ejecuta porque break nunca se disparó

Si se ejecuta break, el else se omite. Si el bucle agota la secuencia, else se ejecuta. Es un patrón de nicho pero más limpio que una variable bandera.

Ordenamiento

sorted() devuelve una nueva lista ordenada y deja la original sin cambios. .sort() ordena la lista en el lugar y devuelve None. El argumento key= te permite ordenar por algo distinto al valor crudo. Por ejemplo, ordenar nombres sin distinguir mayúsculas o ordenar tuplas de jugadores por su puntaje.

sorted() es el predeterminado seguro: nunca modifica el original. .sort() modifica en el lugar y devuelve None. Ambos aceptan reverse=True para orden descendente. El argumento key= toma una función aplicada a cada elemento antes de la comparación. Esto separa el criterio de ordenamiento de los datos.

Ambos usan Timsort: O(n log n) en el peor caso, O(n) en datos casi ordenados, estable. .sort() devuelve None deliberadamente (separación comando-consulta). La función key= se llama una vez por elemento, no una vez por comparación, por lo que los cálculos costosos de claves no se repiten. key=str.lower es una referencia a un método no ligado; key=lambda p: p[1] es una función inline para acceder a un campo específico.

python
puntajes = [87, 42, 96, 55, 71]

ranking = sorted(puntajes)         # [42, 55, 71, 87, 96] (nueva lista)
puntajes.sort()                    # ordena la lista original, devuelve None
puntajes.sort(reverse=True)        # [96, 87, 71, 55, 42]

Ambos aceptan un argumento key=: una función aplicada a cada elemento antes de la comparación:

python
nombres = ["Valentina", "Sofía", "Mateo"]
sorted(nombres, key=str.lower)       # ordenamiento sin distinguir mayúsculas

jugadores = [("Sofía", 87), ("Mateo", 96), ("Valentina", 55)]
sorted(jugadores, key=lambda p: p[1])   # ordenar por puntaje

¿Qué es un lambda?

lambda p: p[1] es una función de una línea. Toma una tupla de jugador y devuelve el puntaje. Las funciones lambda se cubren en el capítulo Lambda, comprensiones y zip.

Para casos simples, usa sorted(). Para listas donde quieras modificar en el lugar, usa .sort().

En la práctica

Recorre puntajes, acumula un total, cuenta las notas aprobatorias e imprime un resumen:

python
puntajes_crudos = [87, 42, 96, 55, 71, 63]

total      = 0
aprobados  = 0

for puntaje in puntajes_crudos:
    total += puntaje
    if puntaje >= 60:
        aprobados += 1

promedio = total / len(puntajes_crudos)
print(f"Promedio: {promedio:.1f}")
print(f"Aprobados: {aprobados}/{len(puntajes_crudos)}")
print(f"Puntaje más alto: {sorted(puntajes_crudos, reverse=True)[0]}")

Procesa una lista de archivos en orden, omite los que sean demasiado grandes e informa cuántos fueron omitidos:

python
archivos = [
    {"name": "report_jan.csv", "size_mb": 12},
    {"name": "report_feb.csv", "size_mb": 850},
    {"name": "report_mar.csv", "size_mb": 7},
]

MAX_SIZE = 100
omitidos = 0

for f in sorted(archivos, key=lambda x: x["name"]):
    if f["size_mb"] > MAX_SIZE:
        print(f"Omitiendo {f['name']} ({f['size_mb']} MB, demasiado grande)")
        omitidos += 1
    else:
        print(f"Procesando {f['name']}...")

print(f"\nListo. {omitidos} archivo(s) omitido(s).")

Escanea un log de solicitudes en busca de errores, luego usa un bucle de reintento que sale al tener éxito o al alcanzar el límite de intentos:

python
solicitudes = [
    {"method": "GET",  "path": "/users",  "status": 200},
    {"method": "POST", "path": "/users",  "status": 201},
    {"method": "GET",  "path": "/broken", "status": 500},
]

errores = []

for req in solicitudes:
    if req["status"] >= 400:
        errores.append(req)

if errores:
    print(f"{len(errores)} error(es) en el log de solicitudes:")
    for err in errores:
        print(f"  {err['method']} {err['path']} -> {err['status']}")
else:
    print("Todas las solicitudes tuvieron éxito")

intentos     = 0
max_retries  = 3
exito        = False

while intentos < max_retries and not exito:
    intentos += 1
    print(f"Intento {intentos}...")
    exito = intentos >= 2   # simula éxito en el segundo intento

print("Conectado" if exito else "Falló después de todos los reintentos")