Laços de Repetição: for e while

Laços de Repetição: for e while

O for do Python não conta: percorre. A diferença parece cosmética e muda o jeito de escrever tudo — range, enumerate, iteração direta sobre strings e coleções, o while para quando não se sabe quantas voltas, e o else de laço, que não é o else que você conhece. Com a armadilha de remover itens durante a iteração.
Python

• • 14 min de leitura

Computadores são excepcionalmente bons em repetir tarefas — muito melhor do que humanos. Os laços de repetição são o mecanismo que permite instruir um programa a executar um bloco de código múltiplas vezes, seja um número fixo de vezes ou enquanto uma condição for verdadeira. Python oferece dois laços principais: for e while.

O Laço for

O for em Python é diferente do for clássico de C ou Java. Em vez de controlar um contador manualmente, ele itera sobre uma sequência — percorrendo cada item de uma coleção:

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

for fruta in frutas:
    print(fruta)

# maçã
# banana
# laranja

A variável fruta assume o valor de cada item a cada iteração. O nome é escolhido por você — o importante é que seja descritivo.

A Função range()

Para repetir um bloco um número específico de vezes, usa-se range():

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

# range(start, stop)
for i in range(1, 6):
    print(i)
# 1, 2, 3, 4, 5

# range(start, stop, step)
for i in range(0, 20, 5):
    print(i)
# 0, 5, 10, 15

# Contagem regressiva
for i in range(10, 0, -1):
    print(i)
# 10, 9, 8, ..., 1

range() não cria uma lista na memória — ela gera os valores sob demanda, o que a torna eficiente mesmo para intervalos grandes.

Iterando com Índice: enumerate()

Quando você precisa do índice e do valor ao mesmo tempo:

linguagens = ["Python", "JavaScript", "Go", "Rust"]

for indice, linguagem in enumerate(linguagens):
    print(f"{indice + 1}. {linguagem}")

# 1. Python
# 2. JavaScript
# 3. Go
# 4. Rust

Evite o padrão for i in range(len(lista)) — ele funciona, mas é considerado não-pythônico. Prefira enumerate().

Iterando sobre Strings

Strings são sequências de caracteres e podem ser percorridas diretamente:

palavra = "Python"

for letra in palavra:
    print(letra)

# P, y, t, h, o, n

O Laço while

O while repete um bloco enquanto uma condição for verdadeira. É ideal quando não se sabe antecipadamente quantas iterações serão necessárias:

tentativas = 0
senha_correta = "python123"

while tentativas < 3:
    senha = input("Digite a senha: ")
    if senha == senha_correta:
        print("Acesso concedido!")
        break
    tentativas += 1
    print(f"Senha incorreta. Tentativas restantes: {3 - tentativas}")

if tentativas == 3:
    print("Conta bloqueada.")

Repare no if depois do laço: ele existe só para descobrir se a repetição terminou por esgotamento ou por break, e depende de o contador ter sobrado com o valor certo. Python tem uma construção própria para essa pergunta, que aparece logo adiante neste artigo — e ela vale para o while também, não só para o for.

break e continue

Dois comandos controlam o fluxo dentro de um laço:

break — encerra o laço imediatamente:

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

for numero in numeros:
    if numero == 9:
        print("Número 9 encontrado!")
        break
    print(numero)
# 3, 7, 2 — para ao encontrar 9

continue — pula para a próxima iteração, ignorando o restante do bloco:

for i in range(10):
    if i % 2 == 0:
        continue  # pula os pares
    print(i)
# 1, 3, 5, 7, 9

Uma armadilha que vale conhecer antes de esbarrar nela: não remova itens de uma lista enquanto a percorre. O for avança por posição, e cada remoção encurta a lista sob os pés dele — o item seguinte desliza para a posição já visitada e nunca é examinado.

numeros = [2, 4, 6]

for n in numeros:
    if n % 2 == 0:
        numeros.remove(n)

print(numeros)   # [4] — e nao [], como se esperava

O pior é que nada reclama: o programa termina e entrega a lista errada. Com dicionário o Python avisa, levantando RuntimeError: dictionary changed size during iteration, mas com lista o erro é silencioso. A saída idiomática é construir uma lista nova em vez de mexer na original: numeros = [n for n in numeros if n % 2 != 0].

A Cláusula else em Laços

Aviso antes de começar, porque o nome engana: este else não se comporta como o else do if visto em Estruturas de Controle: if, elif e else. Lá ele é a alternativa de uma condição falsa; aqui ele é o bloco que roda quando o laço chega ao fim sem ter encontrado o break. Muita gente leria melhor se a palavra fosse nobreak.

Python permite um else ao final de laços — um recurso pouco conhecido mas útil. O bloco else é executado quando o laço termina normalmente, sem ser interrompido por break:

numeros = [2, 4, 6, 8]

for numero in numeros:
    if numero % 2 != 0:
        print("Número ímpar encontrado!")
        break
else:
    print("Todos os números são pares.")

# Todos os números são pares.

Isso é especialmente útil em buscas — se o item não for encontrado, o else executa:

alunos = ["Ana", "Bruno", "Carla"]
busca = "Diego"

for aluno in alunos:
    if aluno == busca:
        print(f"{busca} encontrado!")
        break
else:
    print(f"{busca} não está na lista.")

Laços Aninhados

É possível colocar um laço dentro de outro. Um uso clássico é percorrer estruturas bidimensionais:

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

# 1 x 1 = 1
# 1 x 2 = 2
# 1 x 3 = 3
# ---
# 2 x 1 = 2
# ...

Cuidado com laços aninhados profundos — o custo computacional cresce exponencialmente e a legibilidade cai.

Exemplo Completo: Jogo de Adivinhação

import random

numero_secreto = random.randint(1, 100)
tentativas = 0
max_tentativas = 7

print("Adivinhe o número entre 1 e 100!")

while tentativas < max_tentativas:
    tentativas += 1
    chute = int(input(f"Tentativa {tentativas}/{max_tentativas}: "))

    if chute == numero_secreto:
        print(f"Parabéns! Você acertou em {tentativas} tentativa(s)!")
        break
    elif chute < numero_secreto:
        print("Muito baixo!")
    else:
        print("Muito alto!")
else:
    print(f"Fim de jogo! O número era {numero_secreto}.")

A diferença entre o for do Python e o das linguagens com chaves não é de sintaxe, é de ideia. Lá o laço controla um contador e você usa esse contador para alcançar os itens; aqui o laço pede os itens direto a quem sabe entregá-los, e o índice só aparece quando alguém pede por ele. É por isso que for i in range(len(lista)) soa errado para quem escreve Python há algum tempo: não é questão de elegância, é sinal de que se está simulando um mecanismo que a linguagem já oferece pronto, e de um jeito que quebra assim que a coleção deixar de ter índice.

Duas coisas daqui costumam ser aprendidas tarde e do jeito difícil. A primeira é o else de laço, que resolve com clareza a pergunta "terminou sem achar?" — a mesma que normalmente se responde com uma variável de controle criada só para isso. A segunda é que alterar uma coleção enquanto se caminha sobre ela produz resultado errado sem avisar, no caso da lista, e exceção, no caso do dicionário. As duas apontam para o mesmo hábito: quando o laço precisar decidir algo sobre o conjunto inteiro, ou modificar o conjunto, vale parar e perguntar se não existe uma construção da linguagem para aquilo.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma rotina de limpeza remove de uma lista de pedidos todos os que já foram cancelados, percorrendo a lista com for e chamando remove(). Em produção, sempre sobra um punhado de cancelados. Ninguém encontra erro no código. Onde está o defeito?

Ver resposta

✓ Resposta: Está em modificar a lista enquanto se caminha sobre ela. O for avança por posição: ele guarda um índice interno e pede o item seguinte a cada volta. Quando remove() tira um elemento, todos os posteriores deslizam uma casa para trás — e o que ocupou a posição recém-visitada nunca é examinado. Com [2, 4, 6] e a regra de remover pares, o resultado é [4]: remove-se o 2, o 4 escorrega para a posição zero que já passou, o laço olha a posição um e encontra o 6, remove, e acaba. O que torna esse defeito caro é ele ser silencioso e proporcional: quanto mais itens a remover, mais escapam, e nada é levantado. Um detalhe revelador: com dicionário o Python avisa — RuntimeError: dictionary changed size during iteration — porque a estrutura detecta a alteração de tamanho; a lista não faz essa checagem. A correção idiomática é não modificar, e sim construir: pedidos = [p for p in pedidos if not p.cancelado]. Se a lista precisa mesmo ser a mesma em memória, porque outra parte do sistema guarda uma referência a ela, use pedidos[:] = [...], que substitui o conteúdo no lugar. Iterar ao contrário, com reversed(), também funciona, porque a remoção só desloca o que já passou.

Exercício 2

O artigo diz para evitar for i in range(len(lista)) e preferir enumerate(), chamando o primeiro de "não-pythônico". Isso é só questão de gosto, ou há motivo técnico?

Ver resposta

✓ Resposta: Há motivo técnico, e ele fica evidente no momento em que a coleção muda de tipo. O range(len(x)) exige duas coisas do objeto: que ele saiba dizer seu tamanho e que aceite acesso por índice. Muita coisa que se percorre em Python não oferece nem uma nem outra — um conjunto, um arquivo aberto, um gerador, o resultado de zip() ou de map(). Troque a lista por qualquer um desses e o código quebra, enquanto a versão com enumerate() continua funcionando sem uma linha alterada, porque ela só pede que o objeto seja percorrível. Há também o custo de legibilidade: lista[i] repetido dentro do bloco obriga quem lê a resolver a indireção mentalmente a cada aparição, e é onde nasce o erro de usar i num lugar e i + 1 noutro. E há o desempenho, menor mas real, de uma indexação por volta. Vale conhecer dois detalhes do enumerate que economizam trabalho: ele aceita um segundo argumento com o número inicial, então enumerate(lista, 1) dispensa o indice + 1 que o artigo usa para numerar a saída; e, quando o que se quer é percorrer duas coleções em paralelo, a ferramenta não é o índice, é zip(nomes, notas) — de preferência com strict=True, que levanta erro se as duas não tiverem o mesmo tamanho em vez de parar calado na menor.

Exercício 3

Você precisa procurar um item numa lista e executar uma ação caso ele não seja encontrado. Escreva as duas versões — com uma variável de controle e com o else do laço — e explique por que a segunda é preferível.

Ver resposta

✓ Resposta: A versão com variável cria um encontrado = False antes do laço, marca True junto do break, e testa if not encontrado: depois. A versão com else dispensa a variável: o bloco preso ao laço executa exatamente quando a repetição termina sem passar por um break. A preferência tem três razões. A primeira é que a variável de controle é um estado paralelo, que precisa ser inicializado, atualizado no lugar certo e lido depois — três oportunidades de errar, e o erro mais comum é esquecer de marcá-la em um dos caminhos de saída. A segunda é que o else declara a intenção na estrutura: quem lê sabe de imediato que aquele bloco é o caso "não achou", sem precisar rastrear onde a variável muda. A terceira é que o vínculo é com o break, e não com a condição — o bloco roda mesmo quando a lista está vazia e o laço não deu volta nenhuma, que é justamente o caso-limite esquecido na versão manual. Duas observações: o else também existe no while, e teria evitado no exemplo da senha deste artigo o if tentativas == 3 depois do laço; e a palavra é uma escolha infeliz do Python — Guido van Rossum já admitiu publicamente que nobreak teria sido mais claro. É essa confusão de nome que faz um recurso útil ser tão pouco usado.

Exercício 4

O artigo afirma que range() não cria uma lista na memória. Que consequências práticas isso tem, e em que situação você precisaria transformá-lo numa lista de verdade?

Ver resposta

✓ Resposta: A consequência imediata é que range(10_000_000) ocupa a mesma memória que range(10) — um punhado de bytes — porque ele guarda apenas início, fim e passo, e calcula cada valor na hora em que é pedido. Isso permite percorrer intervalos enormes sem risco, algo que no Python 2 exigia escolher entre range e xrange; a versão preguiçosa venceu e ficou com o nome curto. Um efeito menos óbvio: range não é um gerador comum, é uma sequência, e por isso aceita coisas que um gerador não aceitaria — len(), indexação como r[5], fatiamento, e o teste 7 in range(0, 100, 2), que é resolvido por aritmética e não percorrendo tudo. Ele também pode ser percorrido mais de uma vez, ao contrário de um gerador, que se esgota na primeira passada. Converter para lista com list(range(...)) faz sentido em poucos casos concretos: quando é preciso modificar o resultado, já que range é imutável; quando se quer embaralhar com random.shuffle, que exige uma sequência mutável; e quando se quer ver os números, no REPL ou num log, porque imprimir o objeto mostra range(0, 5) e não os valores. Fora isso, converter é desperdiçar memória sem ganhar nada.

Exercício 5

Depois de for i in range(3): pass, a variável i continua existindo e vale 2. Isso é intencional? Que problema pode causar, e o que acontece se a sequência estiver vazia?

Ver resposta

✓ Resposta: É intencional, e decorre de Python não ter escopo de bloco. Só função, classe e módulo criam escopo; if, for e while não criam. A variável do laço é uma variável comum da função onde o laço está, e sobrevive a ele com o último valor atribuído — comportamento diferente do de C, Java ou JavaScript com let, onde o contador morre no fecha-chaves. Os problemas são dois. O primeiro é o vazamento de nome: um i, x ou item deixado para trás pode ser lido por engano dezenas de linhas adiante, e o código funciona — com o valor da última volta de um laço que ninguém lembra mais. Em função longa isso é difícil de enxergar. O segundo é o caso vazio: se a sequência não tiver nenhum item, o laço não dá volta nenhuma e a variável nunca chega a existir. Usá-la depois levanta NameError, e só quando a coleção vier vazia — o defeito que passa em todos os testes e aparece no primeiro dia calmo em produção. Daí a prática de inicializar antes do laço qualquer variável que for lida depois dele. Vale acrescentar que a compreensão de lista não tem esse problema: desde o Python 3 ela roda em escopo próprio, e o n de [n for n in lista] não vaza para fora.

Comentários

Mais em Python

FastAPI: APIs modernas com tipagem e documentação automática
FastAPI: APIs modernas com tipagem e documentação automática

O FastAPI valida, converte e documenta a partir dos tipos, e os erros moram no…

Interfaces, Protocolos e Composição
Interfaces, Protocolos e Composição

Por que preferir composição à herança, e como declarar contratos em Python sem…

Decoradores e Metaprogramação
Decoradores e Metaprogramação

Decoradores, closures e metaprogramação em Python, do açúcar sintático ao que…