Em se tratando de repetição, o Python tem mais recursos do que a maioria das linguagens revela à primeira vista. Vamos percorrer aqui as estruturas de repetição, entender quando cada tipo de loop é a escolha certa e conhecer os padrões que aparecem com mais frequência em processamento de dados e IA.

Antes da sintaxe, uma justificativa. Em Machine Learning e Deep Learning, loops estão em absolutamente tudo: iterar sobre um dataset linha a linha no pré-processamento, executar épocas de treinamento — cada época é uma passagem completa sobre os dados —, percorrer as camadas de uma rede neural, avaliar métricas sobre múltiplos exemplos e testar combinações na busca de hiperparâmetros. Escrever loops eficientes e Pythonicos é, sem exagero, uma das habilidades mais exercitadas no dia a dia de quem trabalha com dados.

O for é o ponto de partida, e vale dizer logo o que ele não é: diferente de C ou Java, o for do Python não é um contador, é um iterador. Ele percorre qualquer objeto iterável — listas, strings, ranges, dicionários, arquivos. Sobre uma lista, a leitura é quase literal:

frutas = ["maçã", "banana", "laranja", "uva"]

for fruta in frutas:
    print(fruta)

Saída:

maçã
banana
laranja
uva

Strings também são iteráveis, então percorrer caractere por caractere não exige nada de especial:

palavra = "Python"

for letra in palavra:
    print(letra)

Saída:

P
y
t
h
o
n

Quando o que você precisa é um índice ou simplesmente repetir algo N vezes, entra o range(), que gera uma sequência de números. Na forma mais simples, recebe só o limite e conta a partir do zero:

# range(stop) — de 0 até stop-1
for i in range(5):
    print(i)

Saída:

0
1
2
3
4

Com dois argumentos, você define onde começa e onde termina:

# range(start, stop) — de start até stop-1
for i in range(2, 7):
    print(i)

Saída:

2
3
4
5
6

E com três, controla também o passo:

# range(start, stop, step) — com passo
for i in range(0, 20, 5):
    print(i)

Saída:

0
5
10
15

Passo negativo faz a contagem andar para trás, o que resolve contagens regressivas sem nenhuma gambiarra:

# Contagem regressiva com passo negativo
for i in range(10, 0, -1):
    print(i)

Saída:

10
9
8
7
6
5
4
3
2
1

Em torno do for existe um conjunto de funções embutidas que tornam os loops bem mais expressivos, e conhecê-las é o que separa código Pythonico de código traduzido de outra linguagem.

A primeira é enumerate(), para quando você precisa do índice e do valor ao mesmo tempo. Ela substitui com vantagem o velho range(len(...)):

# Jeito não-Pythonico (evite)
frutas = ["maçã", "banana", "laranja"]
for i in range(len(frutas)):
    print(f"{i}: {frutas[i]}")

# Jeito Pythonico (prefira)
for i, fruta in enumerate(frutas):
    print(f"{i}: {fruta}")

Saída de ambos:

0: maçã
1: banana
2: laranja

Se a contagem precisa começar em outro número, basta dizer:

for i, fruta in enumerate(frutas, start=1):
    print(f"{i}: {fruta}")

Saída:

1: maçã
2: banana
3: laranja

A segunda é zip(), que percorre dois iteráveis em paralelo — útil sempre que dados relacionados estão em listas separadas:

nomes = ["Ana", "Bruno", "Carla"]
notas = [8.5, 7.0, 9.2]

for nome, nota in zip(nomes, notas):
    print(f"{nome}: {nota}")

Saída:

Ana: 8.5
Bruno: 7.0
Carla: 9.2

Um detalhe que costuma causar perda silenciosa de dados: zip() para quando o menor iterável acaba, e os elementos excedentes são simplesmente ignorados.

Completando o conjunto, reversed() percorre de trás para frente e sorted() percorre em ordem crescente, ambos sem alterar a lista original:

numeros = [1, 2, 3, 4, 5]

for n in reversed(numeros):
    print(n)

Saída:

5
4
3
2
1
notas = [7.5, 9.0, 5.5, 8.0, 6.5]

for nota in sorted(notas):
    print(nota)

Saída:

5.5
6.5
7.5
8.0
9.0

Nem sempre você sabe de antemão quantas iterações serão necessárias, e é aí que o while entra: ele executa enquanto a condição for verdadeira. Na forma mais direta, com um contador:

contador = 0

while contador < 5:
    print(f"Contador: {contador}")
    contador += 1

Saída:

Contador: 0
Contador: 1
Contador: 2
Contador: 3
Contador: 4

O caso realmente interessante para quem trabalha com modelos é treinar até atingir uma condição, e não um número fixo de rodadas — perda abaixo de um limiar, por exemplo:

perda = 1.0
epoca = 0

while perda > 0.1:
    perda = perda * 0.7  # simulando redução da perda
    epoca += 1
    print(f"Época {epoca}: perda = {perda:.4f}")

print(f"\nTreinamento concluído em {epoca} épocas!")

Saída:

Época 1: perda = 0.7000
Época 2: perda = 0.4900
Época 3: perda = 0.3430
Época 4: perda = 0.2401
Época 5: perda = 0.1681
Época 6: perda = 0.1176
Época 7: perda = 0.0824

Treinamento concluído em 7 épocas!

Há ainda o loop deliberadamente infinito, que roda até que algo lá dentro decida pará-lo:

while True:
    entrada = input("Digite um número positivo (ou 'sair'): ")
    if entrada == "sair":
        print("Encerrando.")
        break
    numero = float(entrada)
    if numero > 0:
        print(f"Raiz quadrada de {numero}: {numero ** 0.5:.4f}")
    else:
        print("O número precisa ser positivo. Tente novamente.")

Esse exemplo já usa break, que merece atenção junto com seu par, o continue. O break interrompe o loop na hora, abandonando as iterações restantes:

numeros = [3, 7, 2, 9, 1, 5, 8]

for num in numeros:
    if num == 9:
        print(f"Encontrado o 9! Parando a busca.")
        break
    print(f"Verificando: {num}")

Saída:

Verificando: 3
Verificando: 7
Verificando: 2
Encontrado o 9! Parando a busca.

Já o continue abandona apenas a iteração atual e segue para a próxima:

numeros = range(1, 11)

for num in numeros:
    if num % 2 == 0:
        continue  # pula números pares
    print(num)

Saída:

1
3
5
7
9

Os dois funcionam igualmente dentro de um while, e podem conviver no mesmo loop:

i = 0
while i < 20:
    i += 1
    if i % 3 == 0:
        continue  # pula múltiplos de 3
    if i > 10:
        break     # para quando passa de 10
    print(i)

Saída:

1
2
4
5
7
8
10

Existe ainda um recurso pouco conhecido e bastante elegante: loops em Python aceitam um bloco else, executado quando o loop chega ao fim naturalmente, sem ter sido interrompido por um break. Isso resolve com clareza o padrão "procurei e não achei":

# Buscando um elemento
numeros = [1, 3, 5, 7, 9]
busca = 6

for num in numeros:
    if num == busca:
        print(f"Encontrado: {busca}")
        break
else:
    print(f"{busca} não encontrado na lista")

Saída:

6 não encontrado na lista

Se busca fosse 5, o loop encontraria o número, executaria o break e o else não rodaria — exatamente o comportamento que se espera de uma busca bem-sucedida.

Loops também se aninham, e essa combinação é rotina em processamento de matrizes e em busca de hiperparâmetros. A tabuada é o exemplo clássico:

# Tabuada
for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i} x {j} = {i*j}")
    print("---")

Saída:

1 x 1 = 1
1 x 2 = 2
1 x 3 = 3
---
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
---
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
---

Trocando o conteúdo, a mesma estrutura vira um grid search, que você vai reencontrar mais adiante no curso:

learning_rates = [0.001, 0.01, 0.1]
batch_sizes = [16, 32, 64]

for lr in learning_rates:
    for bs in batch_sizes:
        print(f"Testando: learning_rate={lr}, batch_size={bs}")

Saída:

Testando: learning_rate=0.001, batch_size=16
Testando: learning_rate=0.001, batch_size=32
Testando: learning_rate=0.001, batch_size=64
Testando: learning_rate=0.01, batch_size=16
...

Quando o loop existe apenas para construir uma lista, o Python oferece uma forma condensada de escrevê-lo numa única linha: a list comprehension, cuja sintaxe básica é [expressao for item in iteravel]. Comparando os dois jeitos de fazer a mesma coisa:

nova_lista = [expressao for item in iteravel]
# Jeito tradicional
quadrados = []
for i in range(1, 6):
    quadrados.append(i ** 2)
print(quadrados)

# Com list comprehension
quadrados = [i ** 2 for i in range(1, 6)]
print(quadrados)

Saída de ambos:

[1, 4, 9, 16, 25]

Acrescentando um if ao final, ela também filtra:

# Apenas os pares
pares = [i for i in range(1, 11) if i % 2 == 0]
print(pares)

Saída:

[2, 4, 6, 8, 10]

E a expressão à esquerda pode transformar cada item:

nomes = ["ana", "bruno", "carla"]
nomes_maiusculos = [nome.upper() for nome in nomes]
print(nomes_maiusculos)

Saída:

['ANA', 'BRUNO', 'CARLA']

Aplicado a dados, esse mesmo padrão resolve uma normalização simples em uma linha:

valores = [10, 20, 30, 40, 50]
maximo = max(valores)
normalizados = [v / maximo for v in valores]
print(normalizados)

Saída:

[0.2, 0.4, 0.6, 0.8, 1.0]

Três hábitos fazem diferença na qualidade do código com loops. O primeiro é preferir for a while sempre que o número de iterações for conhecido — o contador manual só adiciona chance de erro:

# Evite
i = 0
while i < len(lista):
    print(lista[i])
    i += 1

# Prefira
for item in lista:
    print(item)

O segundo é nunca modificar uma lista enquanto itera sobre ela. O comportamento é imprevisível porque o índice interno continua avançando enquanto a lista encolhe, e itens acabam sendo pulados em silêncio:

# PERIGOSO — comportamento imprevisível
numeros = [1, 2, 3, 4, 5]
for n in numeros:
    if n % 2 == 0:
        numeros.remove(n)  # NÃO faça isso

# CORRETO — itere sobre uma cópia ou use list comprehension
numeros = [1, 2, 3, 4, 5]
numeros = [n for n in numeros if n % 2 != 0]
print(numeros)  # [1, 3, 5]

O terceiro já apareceu antes e merece ser repetido: use enumerate() no lugar de range(len()).

# Evite
for i in range(len(lista)):
    print(i, lista[i])

# Prefira
for i, item in enumerate(lista):
    print(i, item)

Para ver tudo isso funcionando junto, vale simular um pré-processamento de dataset como o que antecede o treino de qualquer modelo — com validação de registros, descarte controlado e cálculo de estatística no final:

dataset = [
    {"nome": "Alice", "idade": 25, "salario": 3500.0},
    {"nome": "Bob",   "idade": -1, "salario": 4200.0},   # idade inválida
    {"nome": "Carla", "idade": 30, "salario": None},     # salário ausente
    {"nome": "Diego", "idade": 28, "salario": 5100.0},
    {"nome": "Eva",   "idade": 22, "salario": 2900.0},
]

registros_validos = []
erros = []

for i, registro in enumerate(dataset):
    nome = registro["nome"]

    # Verificar idade
    if registro["idade"] <= 0:
        erros.append(f"Registro {i} ({nome}): idade inválida")
        continue

    # Verificar salário
    if registro["salario"] is None:
        erros.append(f"Registro {i} ({nome}): salário ausente")
        continue

    registros_validos.append(registro)

# Calcular média de salários dos registros válidos
salarios = [r["salario"] for r in registros_validos]
media = sum(salarios) / len(salarios)

print("Registros válidos:")
for r in registros_validos:
    print(f"  {r['nome']}: R${r['salario']:.2f}")

print(f"\nMédia salarial: R${media:.2f}")

print("\nErros encontrados:")
for erro in erros:
    print(f"  {erro}")

Saída:

Registros válidos:
  Alice: R$3500.00
  Diego: R$5100.00
  Eva: R$2900.00

Média salarial: R$3833.33

Erros encontrados:
  Registro 1 (Bob): idade inválida
  Registro 2 (Carla): salário ausente

Repare que o enumerate(), o continue e a list comprehension aparecem ali lado a lado, cada um no papel que lhe cabe — é assim que esses recursos costumam se combinar em código real.

Do que foi visto, o essencial é isto: o for itera sobre qualquer iterável e o range(start, stop, step) gera as sequências numéricas; enumerate() entrega índice e valor juntos, zip() percorre iteráveis em paralelo; o while serve quando o número de iterações é desconhecido; break encerra o loop e continue pula para a próxima iteração, sendo que o else do loop só roda se nenhum break aconteceu; a list comprehension constrói listas de forma compacta; e modificar uma lista durante a iteração continua sendo uma má ideia.


Os seis exercícios a seguir vão do uso básico de range() até a escolha entre loop tradicional e comprehension. Resolva cada um antes de abrir a resposta comentada.

  1. Escreva um loop que imprima todos os números de 1 a 100 que sejam simultaneamente divisíveis por 3 e por 5. Use range() e o operador %.

    ✓ Resposta:
    for i in range(1, 101):
        if i % 3 == 0 and i % 5 == 0:
            print(i)
    

    Saída:

    15
    30
    45
    60
    75
    90
    

    Nota: um número divisível por 3 e por 5 é divisível por 15. Poderia também ser escrito como if i % 15 == 0.

  2. Dada a lista abaixo, use list comprehension para criar uma nova lista contendo apenas os números negativos, com seus valores absolutos (positivos).

    numeros = [4, -7, 0, 15, -3, 22, -9, 1, -14]
    

    Resultado esperado: [7, 3, 9, 14]

    ✓ Resposta:
    numeros = [4, -7, 0, 15, -3, 22, -9, 1, -14]
    negativos_absolutos = [abs(n) for n in numeros if n < 0]
    print(negativos_absolutos)
    

    Saída:

    [7, 3, 9, 14]
    

    A list comprehension filtra apenas os negativos (if n < 0) e aplica abs() para pegar o valor absoluto.

  3. Sem rodar o código, diga o que será impresso e explique o comportamento do else no loop:

    valores = [2, 4, 6, 8, 10]
    
    for v in valores:
        if v % 3 == 0:
            print(f"Encontrado múltiplo de 3: {v}")
            break
    else:
        print("Nenhum múltiplo de 3 encontrado")
    

    ✓ Resposta:

    O código imprimirá:

    Nenhum múltiplo de 3 encontrado
    

    Nenhum dos valores [2, 4, 6, 8, 10] é divisível por 3. Portanto o break nunca é executado, o loop termina normalmente, e o bloco else é executado. Se a lista contivesse o número 6, por exemplo, o programa imprimiria "Encontrado múltiplo de 3: 6" e o else NÃO seria executado, pois o break teria interrompido o loop.

  4. Explique por que o código abaixo pode ter comportamento inesperado e reescreva-o de forma correta:

    lista = [1, 2, 3, 4, 5, 6]
    for n in lista:
        if n % 2 == 0:
            lista.remove(n)
    print(lista)
    

    ✓ Resposta:

    O problema é que Python itera sobre a lista usando índices internos. Quando um elemento é removido, os índices de todos os elementos seguintes se deslocam, fazendo com que alguns elementos sejam pulados.

    Para a lista [1, 2, 3, 4, 5, 6], o resultado esperado seria [1, 3, 5], mas o código produz [1, 3, 5] às vezes e em outras situações pula elementos. O comportamento é imprevisível e dependente do Python runtime.

    Forma correta usando list comprehension:

    lista = [1, 2, 3, 4, 5, 6]
    lista = [n for n in lista if n % 2 != 0]
    print(lista)
    

    Saída:

    [1, 3, 5]
    
  5. Use zip() e enumerate() juntos para imprimir a posição e os dados de duas listas pareadas:

    produtos = ["notebook", "mouse", "teclado"]
    precos = [3500.00, 89.90, 149.90]
    

    Saída esperada:

    1. notebook — R$3500.00
    2. mouse — R$89.90
    3. teclado — R$149.90
    

    ✓ Resposta:
    produtos = ["notebook", "mouse", "teclado"]
    precos = [3500.00, 89.90, 149.90]
    
    for i, (produto, preco) in enumerate(zip(produtos, precos), start=1):
        print(f"{i}. {produto} — R${preco:.2f}")
    

    Saída:

    1. notebook — R$3500.00
    2. mouse — R$89.90
    3. teclado — R$149.90
    

    O zip() combina as duas listas em pares, e o enumerate() adiciona o índice. O start=1 faz a contagem começar em 1.

  6. Escreva um loop while que simule o processo de treinamento de um modelo. Comece com perda = 100.0 e a cada iteração reduza a perda em 15%. O loop deve parar quando a perda for menor que 1.0 ou quando atingir 50 épocas, o que vier primeiro. Ao final, imprima quantas épocas foram necessárias e qual foi a perda final.

    ✓ Resposta:
    perda = 100.0
    epoca = 0
    max_epocas = 50
    
    while perda >= 1.0 and epoca < max_epocas:
        perda = perda * 0.85
        epoca += 1
    
    print(f"Épocas executadas: {epoca}")
    print(f"Perda final: {perda:.4f}")
    
    if perda < 1.0:
        print("Parou por convergência (perda < 1.0)")
    else:
        print("Parou por atingir o máximo de épocas")
    

    Saída:

    Épocas executadas: 29
    Perda final: 0.9787
    Parou por convergência (perda < 1.0)
    

Para aprofundar: