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

Flujo de control

docs.scrimba.com

Cada programa que has escrito hasta ahora funciona de la misma manera: 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 cubre cómo hacer que tu programa se ramifique y repita.

El flujo de control forma el camino que toma tu programa. Las condiciones (if/elif/else) eligen entre ramas; los bucles (while, for) repiten un bloque. El for de Python recorre elementos uno a la vez en lugar de contar un índice, es por eso que itera sobre tantas cosas de manera limpia. Este capítulo cubre cuándo usar cada herramienta y los errores que cada una invita.

Python te da tres herramientas de flujo de control: if/elif/else para ramificaciones, y while y for para repeticiones. Un bucle for recorre un iterable (cualquier cosa por la que puedas iterar un elemento a la vez, como una lista o un archivo) y se detiene automáticamente cuando se agotan los elementos, así que rara vez necesitas manejar un índice manualmente. Un bucle while continúa mientras su condición sea truthy. break y continue solo afectan el bucle más cercano alrededor de ellos, y la cláusula else del bucle (cubierta más adelante) se ejecuta solo cuando un bucle termina sin un break. El resto de este capítulo trata sobre elegir el correcto y evitar los modos de fallo que cada uno trae.

Comparaciones

Antes de que puedas tomar una decisión, necesitas comparar cosas. Los operadores de comparación devuelven True o False. El más importante para acertar al principio: = 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 devuelven un bool, y Python te permite encadenarlos: 0 < x < 10 significa 0 < x and x < 10, que la mayoría de otros lenguajes leen de izquierda a derecha y se equivocan. Las cadenas se comparan carácter por carácter en orden de diccionario, 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 medio b se evalúa solo una vez, lo cual es útil saber si es una llamada costosa. Segundo, == verifica si dos valores son iguales, mientras que is verifica identidad (si dos nombres apuntan al mismo objeto exacto en memoria). No son intercambiables: compara valores con ==, y mantén is para None, True, y False, donde realmente quieres el único objeto. Usar is en cadenas o números lleva a bugs que pasan en entradas pequeñas y fallan en entradas grandes.

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

La distinción = vs == 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 cadenas. Python las compara alfabéticamente, carácter por carácter, lo 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/minúsculas)
JunoComparaciones= almacena un valor, == verifica si dos valores son iguales. Todavía voy lentamente y reviso eso cuando estoy cansado, así que estás en buena compañía si te confunde. Las comparaciones devuelven True o False, y también funcionan en cadenas, 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 otros lenguajes interpreta mal. La comparación de cadenas es por orden Unicode, así que se ordena sensible a mayúsculas/minúsculas.
JunoComparaciones Usa == para valor, is para identidad, y mantén is para None, True, y False. La trampa es is en cadenas o ints: pasa en entradas pequeñas y falla en entradas grandes. Las comparaciones encadenadas evalúan el término medio una sola vez, útil cuando es costoso.

Combinando 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. Estos te permiten expresar condiciones del mundo real como "la puntuación es aprobatoria Y el usuario está activo".

and y or cortocircuitan: and se detiene en el primer operando falsy, or en el primero truthy. Devuelven ese operando real, no siempre un True o False simple, es por eso que name or "guest" es una forma limpia de proporcionar un valor predeterminado. not siempre te da un bool simple.

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

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 un lado. not invierte.

JunoCombinando condicionesand quiere que ambos lados sean verdaderos, or quiere que al menos uno sea verdadero, not invierte la respuesta. Eso cubre la mayoría de condiciones reales que escribirás, como "la puntuación es aprobatoria and el usuario está activo". Python deja de verificar en el momento en que conoce el resultado.
JunoCombinando condicionesand se detiene en el primer valor falsy, or en el primero truthy, y ambos devuelven ese operando real, no siempre un True/False simple. Ese cortocircuito es por qué x and x.method() permanece seguro cuando x está vacío.
JunoCombinando condicionesand/or devuelven un operando, no un bool, y el lado derecho se salta una vez que el lado izquierdo decide el resultado. Apóyate en él para guardias como user and user.name. Observa el otro extremo: un efecto secundario en el lado derecho silenciosamente nunca se dispara cuando cortocircuita.

Truthy y falsy

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

Los valores falsy de Python son False, 0, 0.0, "", [], (), {}, set(), y None. Todo lo demás es truthy, así que un contenedor vacío es falsy y uno no vacío es truthy. Eso es lo que hace que if results: sea la verificación de vacío idiomática: 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 que Python llama detrás de escenas). Si un tipo no define __bool__, Python vuelve a __len__, así que cualquier cosa con longitud cero cuenta como falsa. Los valores falsy estándar son False, 0, 0.0, "", contenedores vacíos ([], (), {}, set()), y None; todo lo demás es truthy. La trampa a observar: if results: es falso para una lista vacía y también para None, así que si necesitas específicamente distinguir "vacío" de "no establecido aún", prueba if results is None: en lugar de confiar en truthiness.

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 una cadena tiene contenido.

JunoTruthy y falsy Las cosas vacías y 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.
JunoTruthy y falsy Falsy es False, cero, cadenas vacías, contenedores vacíos, y None; todo lo demás es truthy. Así que una lista vacía es falsy y una llena es truthy. Esa es la razón completa por la que if my_list: funciona como una verificación de vacío, sin len() necesario.
JunoTruthy y falsy Las condiciones llaman a __bool__, luego a __len__ si ese falta, así que tus propias clases pueden elegir su truthiness. La trampa: if results: no puede distinguir una lista vacía de None. Cuando esa distinción importa, alcanza is None en su lugar.

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 condiciones de arriba hacia abajo y ejecuta el primer bloque coincidente. Python usa indentación (4 espacios por convención) para definir alcance de 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 verifica las condiciones de arriba hacia abajo y ejecuta exactamente una rama: la primera cuya condición es truthy, o el else si ninguna coincide. Cada condición se evalúa perezosamente, significando que Python solo la prueba si cada condición arriba de ella fue falsa, así que el orden es parte de la lógica. Pon la prueba más barata 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 opció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("Por debajo de 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 último
  • Python usa indentación (4 espacios) para marcar qué pertenece dentro de cada bloque; no hay llaves

La indentación no es opcional o 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 la primera coincidencia se ejecuta. Lo que me atrapó al principio: la indentación es lo que le dice a Python dónde comienza y termina cada bloque, así que mantenla consistente.
Junoif / elif / else Las condiciones se verifican de arriba hacia abajo y solo la primera coincidencia 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 confundirla es un SyntaxError, no un bug silencioso.
Junoif / elif / else Las condiciones se evalúan perezosamente, de arriba hacia abajo, así que pon la prueba más barata o más probable primero y ordena los límites superpuestos de estrecho a amplio o una rama se vuelve muerta. La indentación es estructura, no estilo, y tabulaciones vs espacios es un TabError en lugar de un cambio silencioso.

Condiciones de una línea

Para asignaciones simples sí/no, Python tiene una forma compacta de una línea llamada expresión ternaria: value_if_true if condition else value_if_false. Úsala solo cuando la lógica es pequeña y se lee 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 sí/no simples; para cualquier cosa que involucre 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 de otro modo arrojaría error. Porque es una expresión, se encaja en cualquier lugar donde va un valor: una f-string, un argumento de función, un valor predeterminado. La disciplina es saber cuándo parar. No puede sostener 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, alcanza el if/elif/else completo. El lector 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 es pequeña. Para cualquier cosa que involucre elif, escribe la versión completa.

JunoCondiciones de una líneavalue_if_true if condition else value_if_false elige uno de dos valores en una sola línea. Es encantador para pequeñas decisiones ordenadas como "aprobado" if score >= 50 else "reprobado". En el momento en que la lógica crece más allá de eso, vuelve a un bloque `if` normal, se leerá mejor.
JunoCondiciones de una línea El ternario es una expresión, así que se encaja en cualquier lugar donde un valor va: una f-string, un argumento, una asignación. Úsalo para elecciones simples de dos vías. Cualquier cosa con un `elif` en ella pertenece a un bloque completo.
JunoCondiciones de una líneax if cond else y ejecuta solo el lado que elige, así que es seguro donde una rama levantaría, y se deja caer en cualquier ranura de expresión. No los encadenes: los ternarios apilados 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 sea 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 truthy. Úsalo para bucles donde la condición de salida depende de algo que cambia dentro del bucle. Cuando el número de iteraciones se conoce 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 (disminuir el contador, consumir la cola) y obtienes un bucle que nunca termina y un proceso que se cuelga. La forma idiomática cuando la salida tiene que decidirse en el 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 recorriendo una colección o ejecutando un número conocido de veces, un bucle for es más limpio y elimina la posibilidad de un off-by-one en el contador.

python
lives = 3

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

print("Juego terminado")

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

JunoBucles whilewhile mantiene ejecutando su bloque mientras la condición sea verdadera. Alcánzalo cuando no sepas 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 depende de algo que sucede dentro del bucle. Si estás contando o recorriendo una colección, `for` es la opción más limpia.
JunoBucles while El cuerpo tiene que moverse hacia la salida o obtienes un cuelgue, así que eso es lo primero a verificar. while True con un `break` interior es la forma limpia cuando la salida cae a mitad del cuerpo. Para un conteo conocido o una colección, prefiere `for` y salta el riesgo off-by-one.

break y continue

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

break termina el bucle más cercano, transfiriendo control a la primera declaración después de él. continue salta 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 el bucle más interno que los rodea.

break deja el bucle de inmediato y salta su cláusula else si la tiene; continue salta directamente 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 ambas. Una bandera funciona, pero cada if found: adicional que añadas para esquivar la etiqueta faltante es un lugar donde una edición futura puede equivocarse.

break sale del bucle inmediatamente:

python
target = 5
num = 0

while True:
    num += 1
    if num == target:
        print(f"Encontrado {target}")
        break   # detener 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 salta 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    # saltar números pares
    print(num)      # solo imprimen números impares: 1, 3, 5, 7, 9
Junobreak y continuebreak salta fuera 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 bucle hasta que algo sucede.
Junobreak y continuebreak deja el bucle más cercano, continue lo reinicia desde la condición. Ninguno alcanza un bucle exterior. Para salir de bucles anidados, establece 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 qué se basa el patrón de búsqueda.

Bucles for

Un bucle for recorre una secuencia un elemento a la vez: una lista, una cadena, un rango de números. La variable que nombras después de for recibe cada elemento a su turno. No necesitas manejar un contador o verificar la longitud tú mismo.

for le pide a la cosa que le das un elemento a la vez y se detiene cuando se agotan los elementos, en lugar de contar a través de posiciones de índice. Es por eso que funciona en mucho más que listas: cadenas, diccionarios, rangos, y objetos de archivo todos se buclan de la misma manera, sin verificación de longitud o contador de tu parte. Alcanza for sobre while siempre que estés recorriendo una colección.

for no cuenta índices, le pide a la cosa que le das un iterador (un objeto que entrega un elemento a la vez y recuerda dónde se detuvo) y extrae elementos hasta que se agotan. El beneficio práctico es que for funciona en mucho más que listas: cadenas, diccionarios, rangos, objetos de archivo, y fuentes perezosas como generadores (secuencias computadas bajo demanda, nunca todas mantenidas en memoria) se buclan de la misma manera. 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, buclearlo 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", "Juan", "Carlos"]

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

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

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

range()

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

range(start, stop, step) produce enteros desde start hasta (pero no incluyendo) 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 conteo inverso.

Un range nunca construye la lista de números, solo almacena el inicio, fin y paso y calcula cada valor sobre la marcha. Eso hace que range(10_000_000) cueste la misma memoria que range(10), así que cuando solo necesitas iterar, bucla el range directamente en lugar de envolverlo en list() y pagar por diez millones de enteros almacenados. Aún se comporta como una secuencia donde cuenta: puedes indexarlo, cortarlo, preguntar len(), y probar membresía con in, todo barato. Llamar list(range(n)) solo vale la pena cuando realmente necesitas una lista real y reutilizable.

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

range() tiene tres formas:

LlamadaQué 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 cual 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. Alcánzalo cuando quieras un bucle que 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 genera números bajo demanda en lugar de construir una lista, es por eso que `range(10_000_000)` cuesta casi nada. Los pasos negativos cuentan hacia atrás.
Junorange() Un `range` almacena solo inicio, fin y paso, así que es memoria plana sin importar qué tan grande. Buclalo directamente en lugar de `list(range(n))` a menos que realmente necesites una lista reutilizable. Aún indexa, corta, y soporta `in` y `len()` barato.

enumerate()

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

enumerate(iterable, start=0) envuelve cualquier iterador y devuelve tuplas (index, value). El parámetro start desplaza el contador pero no cambia el índice subyacente. Prefiere enumerate() a 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 de desempaque (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 simple. La razón para siempre alcanzarlo sobre un index = 0; index += 1 manejado a mano es confiabilidad: la versión manual invita al bug donde olvidas el incremento o lo golpeas en el lugar equivocado, y enumerate elimina toda esa clase de error. Pasa start=1 cuando estés numerando salida para personas, que cuentan desde uno, no cero.

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

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

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

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

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 no hay contador separado que mantener en sincronía. La parte `i, player` captura ambos a la vez. Pasa `start=1` cuando estés numerando una lista para que alguien lea.
Junoenumerate()enumerate(iterable, start=0) devuelve tuplas `(index, value)` que se desempaquetan directamente 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 todo el bug "olvidé incrementar" que un índice manual invita, así que alcánzalo por defecto. Cada elemento es una tupla, es por eso que el desempaque `for i, v` funciona. `start=1` para numeración orientada al humano.

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 cuadrículas, 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 reestructura 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 alcanzar un tercer nivel, pregunta si la estructura de datos es la verdadera solución (una búsqueda de diccionario puede colapsar un bucle de búsqueda interior). Cuando solo necesitas cada combinación 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, eleva los bucles en 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 para cada una de las rondas del bucle exterior. Así es cómo manejas cuadrículas 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 completamente por paso exterior, así que el conteo es m veces n, vigílalo en entradas grandes. `break` y `continue` solo alcanzan el bucle más interno. Para escapar de ambos, usa una bandera o extrae en 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 de 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 la forma limpia de salir de ambos.

else de bucle

Los bucles de Python pueden tener una cláusula else que se ejecuta solo si el bucle terminó sin golpear un break. No es comúnmente usado, pero es la forma más limpia de escribir "busca una lista, y si nada fue encontrado, 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 necesidad de una variable bandera separada encontrada.

El bucle else se ejecuta solo cuando el bucle termina sin un break, que es lo opuesto de lo que la mayoría de las personas lo leen (no es "else, el bucle no se ejecutó"). Ese nombre es exactamente por qué confunde a los 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 reporta la falta. Reemplaza la variable bandera found = False que de otro modo llevarías a través del bucle. En cualquier otro lado, la inteligencia cuesta más en revisores confundidos que lo que ahorra, así que un comentario en el else vale la línea.

python
target = "David"
names = ["María", "Juan", "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.

Junoelse de bucle Un bucle puede tener un `else` que se ejecute solo cuando el bucle termine sin un `break`. No se usa a menudo, pero es la forma más clara de decir "busca esta lista, y si nada coincidió, haz esto". El nombre es un poco engañoso, así que un comentario breve ayuda.
Junoelse de bucle El `else` del bucle se dispara cuando el bucle termina normalmente, sin `break`. Es el patrón limpio "busca e informa si no se encuentra", sin bandera encontrada. Mantenlo a ese único uso, ya que el nombre confunde a la gente.
Junoelse de bucle Se ejecuta cuando el bucle termina sin un `break`, que es lo inverso de cómo se lee la palabra. Mantenlo al caso búsqueda-y-no-encontrado, donde reemplaza una bandera `found = False`, y coméntalo. En cualquier otro lado 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 distinto del valor bruto. Por ejemplo, ordenar nombres sin distinguir entre mayúsculas/minúsculas u ordenar tuplas de jugadores por su puntuación.

sorted() es el predeterminado 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, significando que elementos que se comparan iguales mantienen su orden original, que es lo que te permite ordenar en etapas: primero ordena por nombre, luego por puntuación, y los jugadores con la misma puntuación se mantienen en orden de nombre. La función key= se ejecuta una vez por elemento, no una vez por comparación, así que una clave costosa se calcula n veces, no n log n veces, y raramente necesitas guardarla tú mismo en caché. .sort() devuelve None a propósito, para impedirte escribir scores = scores.sort() y silenciosamente borrar tu lista, así que la llamada en su lugar debe estar en su propia línea. Alcanza 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", "Juan"]
sorted(names, key=str.lower)       # ordenamiento sin distinguir mayúsculas

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

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

Para casos simples, usa sorted(). Para listas donde quieres modificar en su lugar, usa .sort().

JunoOrdenamientosorted() devuelve una nueva lista ordenada y deja el original solo, .sort() reorganiza la lista misma y devuelve None. Pasa `key=` para ordenar por algo distinto del valor bruto, como nombres sin distinguir mayúsculas. Cuando esté en duda, alcanza `sorted()`, es el seguro.
JunoOrdenamientosorted() nunca toca el original, .sort() muta en su lugar y devuelve None. Ambos toman `reverse=True` y una función `key=` aplicada a cada elemento antes de comparar. Por defecto `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-guarde en caché. Y `.sort()` devuelve `None` a propósito: `x = x.sort()` borra tu lista, así que mantén la llamada en su lugar en su propia línea.

En práctica

Recorre puntuaciones, acumula un total, cuenta calificaciones aprobatorias, 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"Aprobatoria: {passing}/{len(raw_scores)}")
print(f"Puntuación superior: {sorted(raw_scores, reverse=True)[0]}")

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

python
files = [
    {"name": "report_jan.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 para errores, luego usa un bucle de reintento que sale por éxito o cuando se alcanza el límite de intento:

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 solicitudes:")
    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   # simular éxito en el segundo intento

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