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

Flujo de control

Todos los programas que has escrito hasta ahora se ejecutan de la misma forma: de arriba hacia abajo, línea por línea. Eso funciona para scripts simples, pero los programas reales necesitan tomar decisiones y repetir trabajo. Un cuestionario necesita verificar si la respuesta es correcta. Un juego necesita seguir ejecutándose hasta que el jugador gane o pierda. Este capítulo te enseña cómo hacer que tu programa se ramifique y se repita.

El flujo de control forma el camino que sigue tu programa. Las condiciones (if/elif/else) eligen entre ramas; los bucles (while, for) repiten un bloque. El for de Python recorre los elementos uno por uno en lugar de contar un índice, y por eso itera sobre muchísimas cosas de forma limpia. Este capítulo trata cuándo usar cada herramienta y qué errores trae cada una.

Python te da tres herramientas de flujo de control: if/elif/else para ramificarse, y while y for para repetir. Un bucle for recorre un iterable (cualquier cosa por la que puedas pasar elemento por elemento, como una lista o un archivo) y se detiene solo cuando se acaban los elementos, así que rara vez manejas un índice manualmente. Un bucle while sigue adelante mientras su condición sea verdadera. break y continue solo afectan el bucle más cercano, y la cláusula else del bucle (que veremos después) se ejecuta solo cuando un bucle termina sin un break. El resto de este capítulo habla de elegir la herramienta correcta y evitar los errores que trae cada una.

Comparaciones

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

Los operadores de comparación devuelven un bool, y Python te permite encadenarlos: 0 < x < 10 significa 0 < x and x < 10, lo que la mayoría de lenguajes leen de izquierda a derecha y se equivocan. Las strings se comparan carácter por carácter en orden alfabético, así que las mayúsculas se ordenan antes que las minúsculas. Usa == para comparar valores; casi nunca quieres is, que hace una pregunta diferente (cubierta en el Deep Dive).

Las comparaciones devuelven un bool, y dos detalles importan en código real. Primero, Python soporta comparaciones encadenadas: a < b < c se lee como a < b and b < c, y el valor del medio b se evalúa solo una vez, lo que vale la pena saber si es una llamada cara. Segundo, == comprueba si dos valores son iguales, mientras que is comprueba identidad (si dos nombres señalan exactamente el mismo objeto en memoria). No son intercambiables: compara valores con ==, y usa is para None, True y False, donde realmente quieres ese único objeto. Usar is en strings o números causa bugs que pasan con entradas pequeñas y fallan con grandes.

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

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

También puedes comparar strings. Python las compara alfabéticamente, carácter por carácter, que el capítulo Strings cubre con más detalle:

python
"apple" == "apple"   # True
"apple" < "banana"   # True  (a viene antes que b)
"apple" == "Apple"   # False (sensible a mayúsculas)
JunoComparaciones= guarda un valor, == comprueba si dos valores son iguales. Sigo bajando la velocidad y doble-comprobando ese cuando estoy cansado, así que estás en buena compañía si te confunde. Las comparaciones devuelven True o False, y funcionan en strings también, alfabéticamente.
JunoComparaciones Las comparaciones devuelven un bool. Las formas encadenadas como 0 < x < 10 se leen como 0 < x and x < 10, que la mayoría de lenguajes entienden mal. La comparación de strings es por orden Unicode, así que se ordena según mayúsculas y minúsculas.
JunoComparaciones Usa == para valor, is para identidad, y mantén is para None, True y False. La trampa es usar is en strings o ints: funciona con entradas pequeñas y falla con grandes. Las comparaciones encadenadas evalúan el término del medio una sola vez, útil cuando es caro.

Combinar condiciones

and, or y not combinan comparaciones. and requiere que ambos lados sean verdaderos. or requiere que al menos un lado sea verdadero. not invierte el resultado. Esto te permite expresar condiciones del mundo real como "la puntuación es satisfactoria AND el usuario está activo".

and y or usan cortocircuito: and se detiene en el primer operando falso, or en el primero verdadero. Devuelven ese operando real, no siempre un True o False plano, y por eso name or "guest" es una forma limpia de dar un valor por defecto. not siempre te devuelve un bool puro.

and y or no devuelven True o False, devuelven uno de los operandos. and devuelve el primer operando falso, o el último si todos son verdaderos; or devuelve el primer operando verdadero, o el último si todos son falsos. El lado derecho no se evalúa una vez que el izquierdo resuelve el resultado, y esa garantía de cortocircuito (Python se detiene tan pronto como sabe la respuesta) es algo en lo que construyes: user and user.name lee user.name solo cuando user existe, así que nunca falla en un valor faltante. El lado opuesto es un modo de fallo real: si el lado derecho tiene un efecto secundario en el que contabas, como una función que registra o escribe, silenciosamente nunca se ejecuta cuando el lado izquierdo hace cortocircuito.

python
age = 25
score = 88

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

and requiere ambos lados. or requiere al menos uno. not invierte.

JunoCombinar condicionesand quiere ambos lados verdaderos, or quiere al menos uno, not invierte la respuesta. Eso cubre la mayoría de condiciones reales que escribirás, como "la puntuación es satisfactoria and el usuario está activo". Python deja de verificar en el momento en que sabe el resultado.
JunoCombinar condicionesand se detiene en el primer valor falso, or en el primero verdadero, y ambos devuelven ese operando real, no siempre un True/False puro. Ese cortocircuito es por qué x and x.method() se mantiene seguro cuando x está vacío.
JunoCombinar condicionesand/or devuelven un operando, no un bool, y el lado derecho se salta una vez que el izquierdo decide el resultado. Aprovecha con guardias como user and user.name. Vigila el otro borde: un efecto secundario en el lado derecho silenciosamente nunca se ejecuta cuando hace cortocircuito.

Valores verdaderos y falsos

Cada valor en Python tiene una interpretación booleana, incluso si no es True o False. Las strings vacías, 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: comprueba si una lista está no vacía sin escribir if len(results) > 0:.

Los valores falsos de Python son False, 0, 0.0, "", [], (), {}, set() y None. Todo lo demás es verdadero, así que un contenedor vacío es falso y uno no vacío es verdadero. Eso es lo que hace que if results: sea la forma idiomática de verificar si está vacío: escríbelo en lugar de if len(results) > 0:.

Cuando pones un objeto en una condición, Python le pide una respuesta sí-o-no llamando a su método __bool__ (un dunder, uno de los métodos de doble guion bajo que Python llama detrás de escenas). Si un tipo no define __bool__, Python recurre a __len__, así que cualquier cosa con longitud cero cuenta como falsa. Los valores falsos estándar son False, 0, 0.0, "", contenedores vacíos ([], (), {}, set()) y None; todo lo demás es verdadero. La trampa que hay que vigilar: if results: es falso para una lista vacía y también para None, así que si específicamente necesitas distinguir "vacío" de "no establecido aún", prueba if results is None: en lugar de confiar en la verdosidad.

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: comprueba si una string tiene contenido.

JunoValores verdaderos y falsos Las cosas vacías y el cero cuentan como False en una condición: 0, "", [] y None. Todo lo demás cuenta como True. Por eso if results: se lee tan limpiamente como "si hay algo en la lista", sin len() necesario.
JunoValores verdaderos y falsos Falso es False, cero, strings vacías, contenedores vacíos y None; todo lo demás es verdadero. Así que una lista vacía es falsa y una llena es verdadera. Esa es toda la razón por la que if my_list: funciona como una verificación de vacío, sin len() necesario.
JunoValores verdaderos y falsos Las condiciones llaman a __bool__, luego a __len__ si falta ese, así que tus propias clases pueden elegir su verdosidad. La trampa: if results: no puede distinguir una lista vacía de None. Cuando esa distinción importa, opta por is None.

if / elif / else

La declaración if ejecuta un bloque de código solo cuando su condición es True. elif añade 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 que coincide. Python usa indentación (4 espacios por convención) para definir el alcance del bloque; la indentación inconsistente es un SyntaxError. Solo una rama se ejecuta: una vez que una condición coincide, todos los elif y else posteriores se saltan.

Python comprueba las condiciones de arriba hacia abajo y ejecuta exactamente una rama: la primera cuya condición es verdadera, o el else si ninguna coincide. Cada condición se evalúa perezosamente, significa que Python solo la prueba si todas las condiciones arriba de ella fueron falsas, así que el orden es parte de la lógica. Pon el test más barato o más probable primero, y ordena rangos superpuestos para que el límite más estrecho venga antes que el más amplio, o una rama posterior se vuelve inalcanzable. La indentación es la estructura de bloque aquí, no una elección de estilo, así que mezclar tabulaciones y espacios es un TabError en lugar de un cambio silencioso de significado.

python
score = 87

if score >= 90:
    print("Calificación A")
elif score >= 80:
    print("Calificación B")
elif score >= 70:
    print("Calificación C")
else:
    print("Menor que C")

Las reglas:

  • if es obligatorio y siempre viene primero
  • elif (abreviatura de "else if") es opcional y puedes tener tantos como necesites
  • else es opcional, maneja todo lo que no coincidió, y viene 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 estructura. La indentación inconsistente es un error de sintaxis.

Junoif / elif / elseif ejecuta su bloque cuando la condición es verdadera, elif verifica más casos, else captura el resto. Solo el primer coincidente se ejecuta. Lo que me confundió al principio: la indentación es lo que le dice a Python dónde empieza y termina cada bloque, así que mantenla consistente.
Junoif / elif / else Las condiciones se verifican de arriba hacia abajo y solo el primero coincidente se ejecuta, así que el orden importa: pon rangos más estrechos antes que más amplios. La indentación (4 espacios) marca el bloque, y mezclarlo es un SyntaxError, no un bug silencioso.
Junoif / elif / else Las condiciones se evalúan perezosamente, de arriba hacia abajo, así que pon el test más barato o más probable primero y ordena límites superpuestos de estrecho a amplio o una rama muere. La indentación es estructura, no estilo, y tabulaciones versus espacios es un TabError en lugar de un cambio silencioso.

Condiciones en una línea

Para asignaciones simples de sí/no, Python tiene una forma compacta en una línea llamada expresión ternaria: value_if_true if condition else value_if_false. Úsala solo cuando la lógica sea pequeña y lea como una oración.

La expresión condicional (operador ternario) se evalúa a uno de dos valores basado en una condición. Es una expresión, no una declaración, así que puede aparecer en cualquier lugar donde se espera 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 implique elif, escribe la versión completa.

La expresión condicional x if condition else y evalúa la condición y devuelve un lado sin ejecutar el otro, así que es seguro usarla donde una rama causaría error. Como es una expresión, cabe en cualquier lugar donde va un valor: una f-string, un argumento de función, un valor por defecto. La disciplina es saber cuándo parar. No puede contener un elif y encadenarla (a if p else b if q else c) se lee peor que el bloque que reemplaza, así que una vez que tienes más de una decisión, opta por el if/elif/else completo. La persona que mantenga esto después te lo agradecerá.

python
label = "aprobado" if score >= 50 else "reprobado"

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

JunoCondiciones en una líneavalue_if_true if condition else value_if_false elige uno de dos valores en una sola línea. Es hermoso para pequeñas elecciones ordenadas como "aprobado" if score >= 50 else "reprobado". El momento en que la lógica crece más allá de eso, vuelve a un bloque normal if, se leerá mejor.
JunoCondiciones en una línea La ternaria es una expresión, así que cabe en cualquier lugar donde va un valor: una f-string, un argumento, una asignación. Úsala para elecciones simples de dos vías. Cualquier cosa con un elif pertenece en un bloque completo.
JunoCondiciones en una líneax if cond else y solo ejecuta el lado que elige, así que es seguro donde una rama levantaría, y se mete en cualquier ranura de expresión. No las encadenes: las ternarias apiladas se leen peor que el bloque que reemplazan, así que después de una decisión, escríbelo.

Bucles while

Un bucle while repite su bloque mientras su condición es True. Úsalo cuando no sepas de antemano cuántas veces debe ejecutarse el bucle, por ejemplo esperando entrada válida o reintentando hasta que un trabajo 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 es usualmente más limpio.

while re-prueba su condición antes de cada pasada, y el cuerpo es lo que cambia esa condición, así que el modo de fallo es estructural: olvida moverte hacia la salida (decrementa el contador, consume la cola) y obtienes un bucle que nunca termina y un proceso que se cuelga. La forma idiomática cuando la salida debe decidirse en medio o al final del cuerpo es while True con un break interior, que mantiene la condición donde realmente sucede en lugar de forzarla al encabezado. Para cualquier cosa que camine una colección o ejecute un número conocido de veces, un bucle for es más limpio y elimina la posibilidad de un error por uno en el contador.

python
lives = 3

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

print("Fin del juego")

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

JunoBucles whilewhile sigue ejecutando su bloque mientras la condición se mantenga verdadera. Opta por él cuando no sabes cuántas rondas necesitas, como esperar entrada válida. Asegúrate de que algo dentro del bucle se mueva hacia la salida, o se ejecutará para siempre, un error que he cometido más de una vez.
JunoBucles whilewhile verifica su condición antes de cada pasada y el cuerpo es lo que la cambia. Úsalo cuando la salida dependa de algo que sucede dentro del bucle. Si estás contando o caminando una colección, for es la mejor opción.
JunoBucles while El cuerpo tiene que moverse hacia la salida o obtienes un cuelgue, así que eso es lo primero que verificas. while True con un break interior es la forma limpia cuando la salida llega a mitad de cuerpo. Para un conteo conocido o una colección, prefiere for y salta el riesgo de error por uno.

break y continue

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

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

break sale del bucle de inmediato y salta su cláusula else si tiene una; continue salta directo de vuelta al encabezado del bucle para la siguiente pasada. Ambos se vinculan al bucle más cercano, y Python no tiene break etiquetado, así que no pueden alcanzar un bucle exterior desde dentro de uno interior. Cuando necesitas escapar varios niveles a la vez, el movimiento limpio es elevar los bucles anidados a una función y return, que se lee mejor que pasar una variable bandera a través de ambos. Una bandera funciona, pero cada if found: extra que añades para esquivar la etiqueta faltante es un lugar donde una edición futura puede cometer un error.

break sale del bucle inmediatamente:

python
target = 5
num = 0

while True:
    num += 1
    if num == target:
        print(f"Encontrado {target}")
        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 suceder al final del cuerpo del bucle.

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

python
num = 0

while num < 10:
    num += 1
    if num % 2 == 0:
        continue    # salta números pares
    print(num)      # solo números impares se imprimen: 1, 3, 5, 7, 9
Junobreak y continuebreak sale del bucle de inmediato, continue salta el resto de esta ronda y vuelve a verificar la condición. Ambos solo tocan el bucle en el que están directamente. while True: con un break se lee extraño al principio, pero es una forma normal de hacer un bucle hasta que algo suceda.
Junobreak y continuebreak sale del bucle más cercano, continue lo reinicia desde la condición. Ninguno alcanza un bucle exterior. Para salir de bucles anidados, pon una bandera o extrae el bucle interior a una función y return.
Junobreak y continue Ambos se vinculan al bucle más cercano, y no hay break etiquetado, así que escapar varios niveles significa una bandera o, más limpio, una función con return. break también salta el else del bucle, que es exactamente en lo que se basa el patrón de búsqueda.

Bucles for

Un bucle for recorre una secuencia un elemento a la vez: una lista, una string, un rango de números. La variable que nombras después de for recibe cada elemento a su vez. No manejas un contador ni verificas la longitud tú mismo.

for le pide a lo que le des un elemento a la vez y se detiene cuando se acaban los elementos, en lugar de contar a través de posiciones de índice. Por eso funciona en mucho más que listas: strings, diccionarios, rangos y objetos de archivo todos se recorren de la misma forma, sin verificación de longitud ni contador por tu parte. Opta por for sobre while siempre que estés caminando una colección.

for no cuenta índices, le pide a lo que le des un iterador (un objeto que entrega un elemento a la vez y recuerda dónde se detuvo) y extrae elementos hasta que se agoten. La ventaja práctica es que for funciona en mucho más que listas: strings, diccionarios, rangos, objetos de archivo e iteradores perezosos como generadores (secuencias computadas bajo demanda, nunca todas mantenidas en memoria) se recorren de la misma forma. En ese último grupo es donde vive la trampa: un iterador perezoso es de un solo uso, así que una vez que un bucle for agota un generador o un archivo, recorrerlo de nuevo no te da nada. Si necesitas los elementos dos veces, materialízalos en una lista primero o abre la fuente de nuevo.

python
players = ["María", "Bob", "Carlos"]

for player in players:
    print(f"¡Hola, {player}!")

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

JunoBucles forfor item in sequence recorre cada elemento a su vez, y nunca tocas un contador o una longitud tú mismo. Funciona en listas, strings, rangos, muchas cosas. Viniendo de otros lenguajes, no manejar el índice se sintió extraño al principio, luego se sintió como un regalo.
JunoBucles forfor extrae elementos uno a la vez hasta que se agoten, así que no está atado a secuencias indexadas. Listas, strings, dicts, rangos, archivos: el mismo bucle para todos. Sin aritméticas de índice, sin verificaciones de longitud.
JunoBucles forfor impulsa cualquier iterador, así que listas, archivos y generadores se recorren igual. La trampa son los perezosos: un iterador de generador o archivo es de un solo uso, agotado después de una pasada. ¿Necesitas los elementos dos veces? Mételos en una lista primero o reabre la fuente.

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 fin 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 de a 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 hacia atrás.

Un range nunca construye la lista de números, solo almacena el inicio, el fin y el paso e idea cada valor sobre la marcha. Eso hace que range(10_000_000) cueste la misma memoria que range(10), así que cuando solo necesites iterar, recorre el range directamente en lugar de envolverlo en list() y pagar diez millones de enteros almacenados. Sigue comportándose como una secuencia donde cuenta: puedes indexarlo, cortarlo, pedir len(), y probar pertenencia con in, todo barato. Llamar list(range(n)) solo vale la pena cuando realmente necesitas una lista real y reutilizable para guardar.

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.

Junorange()range(5) te da 0, 1, 2, 3, 4, y puedes establecer un inicio, fin y paso. Opta por él cuando quieras que un bucle se ejecute un número establecido de veces. No construye una lista, te entrega un número por ronda.
Junorange()range(start, stop, step) cuenta desde start hasta pero sin incluir stop. Es una secuencia perezosa, así que hace números bajo demanda en lugar de construir una lista, y por eso range(10_000_000) cuesta casi nada. Los pasos negativos cuentan hacia atrás.
Junorange() Un range solo almacena inicio, fin y paso, así que es memoria plana sin importar cuán grande sea. Recórrelo directamente en lugar de list(range(n)) a menos que realmente necesites una lista reutilizable. Sigue indexando, cortando, y soporta in y len() barato.

enumerate()

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

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

enumerate envuelve cualquier iterable y devuelve pares (count, value), que el encabezado for divide en dos nombres a través del desempaquetamiento (Python asignando cada parte de una tupla a su propio nombre en un paso). Es perezoso y no añade nada más allá del contador, así que cuesta lo mismo que el bucle sin cobertura. La razón de siempre optar por él sobre un index = 0; index += 1 manejado a mano es confiabilidad: la versión manual invita al bug donde olvidas el incremento o lo haces en el lugar equivocado, y enumerate elimina esa clase completa de error. Pasa start=1 cuando estés numerando salida para personas, que cuentan desde uno, no cero.

python
players = ["María", "Bob", "Carlos"]

for i, player in enumerate(players):
    print(f"{i + 1}. {player}")
# 1. María
# 2. Bob
# 3. Carlos

La sintaxis i, player se llama desempaquetamiento. Python divide el par (index, value) en dos nombres automáticamente.

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

python
for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")    # comienza en 1
Junoenumerate()enumerate() te entrega la posición y el valor juntos, así que sin contador separado para mantener sincronizado. La parte i, player atrapa ambos a la vez. Pasa start=1 cuando estés numerando una lista para que alguien lea.
Junoenumerate()enumerate(iterable, start=0) cede pares (index, value) que se desempaquetan justo en el encabezado for. Prefierelo a una variable contador, es más limpio y no puedes olvidar incrementarlo. start solo desplaza el número mostrado, no el índice real.
Junoenumerate() Perezoso, gratis más allá del contador, y elimina el bug completo de "olvidé incrementar" que un índice manual invita, así que opta por él por defecto. Cada elemento es una tupla, y por eso el desempaquetamiento for i, v funciona. start=1 para numeración orientada a humanos.

Bucles anidados

Puedes poner un bucle dentro de otro bucle. El bucle interior se ejecuta completamente para cada una de las iteraciones del bucle exterior. Así es como procesas grillas, combinaciones o cualquier dato con dos niveles de estructura.

Los bucles anidados tienen un conteo de iteración O(m × n) para longitud exterior m y longitud interior n. break y continue dentro de un bucle anidado solo afectan el bucle más interno. Para salir de múltiples niveles, usa una variable bandera o reestructúrate en una función.

Los bucles anidados se multiplican: un bucle exterior de longitud m alrededor de uno interior de longitud n ejecuta el cuerpo m veces n veces, así que el costo sube rápido y un par de bucles sobre entradas grandes es el culpable usual detrás de un script lento. Antes de opta por un tercer nivel, pregúntate si la estructura de datos es la verdadera solución (una búsqueda en diccionario puede colapsar un bucle de búsqueda interior). Cuando solo necesitas todas las combinaciones de dos secuencias (un producto cartesiano, cada emparejamiento de una con la otra), itertools.product de la biblioteca estándar se lee mejor que el anidamiento. Y recuerda que break sale solo del bucle interior: para dejar ambos, eleválos a una función y return.

python
rows = [1, 2, 3]
cols = ["A", "B"]

for row in rows:
    for col in cols:
        print(f"{col}{row}", 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 el bucle más interno.

JunoBucles anidados Un bucle dentro de un bucle ejecuta el bucle interior completo por cada única ronda del exterior. Así es como manejas grillas y combinaciones. break y continue solo actúan en el bucle en el que están, el interior aquí.
JunoBucles anidados El bucle interior se ejecuta en completo por paso exterior, así que el conteo es m por n, vigílalo en entradas grandes. break y continue solo llegan al bucle más interno. Para escapar ambos, usa una bandera o extrae a una función y return.
JunoBucles anidados Los costos se multiplican, así que los bucles anidados sobre entradas grandes son el punto lento usual; a veces una búsqueda en dict reemplaza el interior completamente. Para cada emparejamiento, itertools.product vence al anidamiento. break golpea solo el bucle interior, así que una función con return es el camino limpio fuera de ambos.

Loop-else

Los bucles de Python pueden tener una cláusula else que se ejecuta solo si el bucle termina sin golpear un break. No se usa comúnmente, pero es la forma más limpia de escribir "busca en una lista, y si no se encuentra nada, haz 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 golpear un break. Es el patrón idiomático para "busca e informa si no se encuentra" sin necesitar una variable bandera separada.

El else del bucle se ejecuta solo cuando el bucle termina sin un break, que es lo opuesto a lo que la mayoría de personas lee (no es "si no, el bucle no se ejecutó"). Ese nombramiento es exactamente por qué confunde lectores, así que la regla en producción es usarlo para una cosa y solo una cosa: el patrón de búsqueda-y-no-encontrado, donde break marca un golpe y el else informa la falla. Reemplaza la bandera found = False que de otro modo llevarías a través del bucle. En cualquier otro lugar, la astucia cuesta más en revisores confundidos que lo que ahorra, así que un comentario en el else vale la línea.

python
target = "Dave"
names = ["María", "Bob", "Carlos"]

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

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

JunoLoop-else Un bucle puede tener un else que se ejecute solo cuando el bucle termina sin un break. No se usa a menudo, pero es la forma más ordenada de decir "busca esta lista, y si nada coincidió, haz esto". El nombre es un poco engañoso, así que un comentario corto ayuda.
JunoLoop-else El else del bucle se dispara cuando el bucle termina normalmente, sin break. Es el patrón ordenado "busca e informa si no se encuentra", sin bandera necesaria. Mantenlo a ese único uso, ya que el nombre confunde a la gente.
JunoLoop-else Se ejecuta cuando el bucle termina sin un break, que es la inversa de cómo se lee la palabra. Mantenlo al caso de búsqueda-y-no-encontrado, donde reemplaza una bandera found = False, y comenta. En cualquier otro lugar cuesta más en revisores confundidos que lo que ahorra.

Ordenamiento

sorted() devuelve una nueva lista ordenada y deja el original sin cambios. .sort() ordena la lista en su lugar y devuelve None. El argumento key= te permite ordenar por algo que no sea el valor crudo. Por ejemplo, ordenar nombres sin importar mayúsculas o ordenar tuplas de jugador por su puntuación.

sorted() es el estándar seguro: nunca modifica el original. .sort() modifica en su 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.

El ordenamiento de Python es estable, significa que elementos que se comparan iguales mantienen su orden original, lo que te permite ordenar en etapas: ordena por nombre primero, luego por puntuación, y jugadores con la misma puntuación se quedan en orden de nombre. La función key= se ejecuta una vez por elemento, no una vez por comparación, así que una clave cara se calcula n veces, no n log n veces, y rara vez necesitas almacenarla tú mismo. .sort() devuelve None a propósito, para detenerte escribiendo scores = scores.sort() e silenciosamente borrar tu lista, así que la llamada in-place debe estar en su propia línea. Opta por sorted() por defecto y mantén .sort() para cuando realmente quieras mutar el original.

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

ranked = sorted(scores)           # [42, 55, 71, 87, 96] (nueva lista)
scores.sort()                     # ordena la lista original, devuelve None
scores.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
names = ["Carlos", "María", "Bob"]
sorted(names, key=str.lower)       # ordenamiento sin importar mayúsculas

players = [("María", 87), ("Bob", 96), ("Carlos", 55)]
sorted(players, key=lambda p: p[1])   # ordena por puntuación

lambda p: p[1] es una función en una línea: toma una tupla de jugador y devuelve la puntuación. Las lambdas reciben tratamiento completo en el capítulo Lambdas y comprensiones.

Para casos simples, usa sorted(). Para listas donde quieras modificar in-place, usa .sort().

JunoOrdenamientosorted() entrega una nueva lista ordenada y deja el original solo, .sort() reacomoda la lista misma y devuelve None. Pasa key= para ordenar por algo que no sea el valor crudo, como nombres sin importar mayúsculas. Cuando dudes, opta por sorted(), es el seguro.
JunoOrdenamientosorted() nunca toca el original, .sort() muta in-place y devuelve None. Ambos toman reverse=True y una función key= aplicada a cada elemento antes de comparar. Por defecto opta por sorted(); usa .sort() solo cuando quieras cambiar la lista.
JunoOrdenamiento El ordenamiento es estable, así que ordena en etapas y los empates mantienen su orden. key= se ejecuta una vez por elemento, no por comparación, así que no pre-caches. Y .sort() devuelve None a propósito: x = x.sort() borra tu lista, así que mantén la llamada in-place en su propia línea.

En la práctica

Recorre puntuaciones, acumula un total, cuenta calificaciones aprobadas e imprime un resumen:

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

total = 0
passing = 0

for score in raw_scores:
    total += score
    if score >= 60:
        passing += 1

average = total / len(raw_scores)
print(f"Promedio: {average:.1f}")
print(f"Aprobadas: {passing}/{len(raw_scores)}")
print(f"Puntuación máxima: {sorted(raw_scores, reverse=True)[0]}")

Procesa una lista de archivos en orden ordenado, salta los que son demasiado grandes e informa cuántos fueron saltados:

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

MAX_SIZE = 100
skipped = 0

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

print(f"\nListo. {skipped} archivo(s) saltado(s).")

Escanea un registro de solicitudes en busca de errores, luego usa un bucle de reintentos que sale en éxito o cuando se alcanza el límite de intentos:

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

errors = []

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

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

attempts = 0
max_retries = 3
success = False

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

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