Listas são a estrutura de dados mais utilizada em Python. Elas permitem armazenar múltiplos valores em uma única variável, de forma ordenada e mutável. Dominar listas é essencial — praticamente todo programa Python as utiliza de alguma forma.
Criando Listas
# Lista de strings
linguagens = ["Python", "JavaScript", "Go", "Rust"]
# Lista de inteiros
notas = [7, 8, 9, 10, 6]
# Lista mista
dados = ["Ricardo", 35, 1.78, True]
# Lista vazia
vazia = []
# Lista criada com range
sequencia = list(range(1, 6))
print(sequencia) # [1, 2, 3, 4, 5]
Acessando Elementos
Listas usam índices baseados em zero. Python também suporta índices negativos — que contam a partir do final:
frutas = ["maçã", "banana", "laranja", "uva", "manga"]
print(frutas[0]) # maçã — primeiro
print(frutas[2]) # laranja
print(frutas[-1]) # manga — último
print(frutas[-2]) # uva — penúltimo
Fatiamento (Slicing)
Extrair sublistas com a notação [start:stop:step]:
numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numeros[2:6]) # [2, 3, 4, 5]
print(numeros[:4]) # [0, 1, 2, 3]
print(numeros[6:]) # [6, 7, 8, 9]
print(numeros[::2]) # [0, 2, 4, 6, 8] — de 2 em 2
print(numeros[::-1]) # [9, 8, 7, ..., 0] — invertida
O fatiamento nunca gera erro de índice — se o limite ultrapassar o tamanho da lista, Python simplesmente vai até o fim.
Modificando Listas
Listas são mutáveis — seus elementos podem ser alterados após a criação:
cores = ["vermelho", "verde", "azul"]
# Alterando um elemento
cores[1] = "amarelo"
print(cores) # ['vermelho', 'amarelo', 'azul']
# Alterando um intervalo
cores[0:2] = ["rosa", "roxo"]
print(cores) # ['rosa', 'roxo', 'azul']
Métodos Principais
lista = [3, 1, 4, 1, 5, 9, 2, 6]
# Adicionar ao final
lista.append(7)
# Inserir em posição específica
lista.insert(0, 0) # insere 0 no índice 0
# Remover por valor
lista.remove(1) # remove a primeira ocorrência de 1
# Remover por índice e retornar o valor
ultimo = lista.pop() # remove e retorna o último
terceiro = lista.pop(2) # remove e retorna o índice 2
# Ordenar in-place
lista.sort()
lista.sort(reverse=True) # ordem decrescente
# ATENCAO: sort() ordena no lugar e devolve None.
# Escrever lista = lista.sort() deixa lista valendo None.
# Inverter in-place
lista.reverse()
# Contar ocorrências
print(lista.count(1))
# Localizar índice
print(lista.index(5))
# Limpar a lista
lista.clear()
Funções Nativas com Listas
notas = [7.5, 8.0, 9.5, 6.0, 10.0]
print(len(notas)) # 5 — quantidade de elementos
print(sum(notas)) # 41.0 — soma
print(min(notas)) # 6.0 — menor valor
print(max(notas)) # 10.0 — maior valor
# Ordenar sem modificar a original
ordenadas = sorted(notas)
ordenadas_desc = sorted(notas, reverse=True)
Copiando Listas
Um erro comum é tentar copiar uma lista com = — e o motivo já apareceu em Variáveis e Tipos de Dados: o nome é um rótulo colado num objeto, e atribuir um nome a outro só cola um segundo rótulo no mesmo objeto.
original = [1, 2, 3]
copia = original # não é uma cópia — é o mesmo objeto!
copia.append(4)
print(original) # [1, 2, 3, 4] — original também mudou!
Para copiar corretamente:
# Forma 1 — método copy()
copia = original.copy()
# Forma 2 — fatiamento
copia = original[:]
# Forma 3 — construtor list()
copia = list(original)
As três formas têm o mesmo limite, e ele importa muito daqui a pouco, quando aparecerem as listas aninhadas: todas são cópias rasas. Elas criam uma lista nova, mas os elementos continuam sendo os mesmos objetos. Enquanto a lista guardar números ou textos isso não faz diferença, porque esses tipos são imutáveis e ninguém altera um deles no lugar. Quando a lista guarda outras listas, a história muda:
matriz = [[1, 2], [3, 4]]
copia = matriz.copy() # ou matriz[:], ou list(matriz)
copia[0][0] = 99
print(matriz) # [[99, 2], [3, 4]] — a original mudou!
A lista de fora foi copiada; as de dentro, não. Para uma cópia de verdade em qualquer profundidade existe o deepcopy:
import copy
copia = copy.deepcopy(matriz)
copia[0][0] = 99
print(matriz) # [[1, 2], [3, 4]] — intacta
Não saia usando deepcopy por precaução, no entanto: ele percorre a estrutura inteira e é bem mais caro. A cópia rasa basta na maioria dos casos — o que não basta é ignorar a diferença.
List Comprehension
Uma das características mais elegantes do Python — cria listas de forma compacta e expressiva:
# Forma tradicional
quadrados = []
for i in range(1, 6):
quadrados.append(i ** 2)
# Com list comprehension
quadrados = [i ** 2 for i in range(1, 6)]
print(quadrados) # [1, 4, 9, 16, 25]
# Com filtro
pares = [i for i in range(20) if i % 2 == 0]
print(pares) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Transformando strings
nomes = ["ana", "bruno", "carla"]
maiusculos = [nome.upper() for nome in nomes]
print(maiusculos) # ['ANA', 'BRUNO', 'CARLA']
Listas Aninhadas (Matrizes)
Listas podem conter outras listas — útil para representar matrizes ou tabelas:
matriz = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(matriz[1][2]) # 6 — linha 1, coluna 2
# CUIDADO: multiplicar uma lista NAO cria linhas independentes
ruim = [[0] * 3] * 3
ruim[0][0] = 5
print(ruim) # [[5, 0, 0], [5, 0, 0], [5, 0, 0]] — as tres!
# a forma correta cria uma lista nova a cada volta
bom = [[0] * 3 for _ in range(3)]
bom[0][0] = 5
print(bom) # [[5, 0, 0], [0, 0, 0], [0, 0, 0]]
# Percorrendo uma matriz
for linha in matriz:
for elemento in linha:
print(elemento, end=" ")
print()
# 1 2 3
# 4 5 6
# 7 8 9
Exemplo Completo: Sistema de Notas
def calcular_estatisticas(notas):
"""Calcula média, maior e menor nota de uma lista."""
if not notas:
return None
media = sum(notas) / len(notas)
aprovados = [n for n in notas if n >= 6.0]
reprovados = [n for n in notas if n < 6.0]
return {
"total": len(notas),
"media": round(media, 2),
"maior": max(notas),
"menor": min(notas),
"aprovados": len(aprovados),
"reprovados": len(reprovados),
}
notas_turma = [8.5, 4.0, 7.0, 9.5, 5.5, 6.0, 3.0, 10.0]
stats = calcular_estatisticas(notas_turma)
for chave, valor in stats.items():
print(f"{chave:>12}: {valor}")
Duas confusões explicam a maior parte dos erros que se comete com listas, e as duas vêm da mesma origem: um nome é um rótulo, não uma caixa. A primeira é achar que atribuir copia — não copia, cola um segundo rótulo no mesmo objeto. A segunda é achar que, resolvida a primeira, o problema acabou — mas copy(), [:] e list() copiam só o primeiro nível, e uma lista de listas continua compartilhando o que tem dentro. Enquanto os elementos forem números e textos isso nunca aparece, porque eles são imutáveis; no dia em que forem listas ou dicionários, aparece de uma vez.
A outra linha divisória vale a pena guardar porque organiza dezenas de métodos: o que altera no lugar devolve None, e o que devolve algo não altera. lista.sort(), lista.reverse() e lista.append() estão de um lado; sorted(), reversed() e o fatiamento estão do outro. Não é arbitrário — é uma convenção do Python para que nunca se escreva por engano uma cadeia de chamadas que modifica a lista original no caminho. O preço é o erro de principiante lista = lista.sort(), que deixa a variável valendo None e some com os dados.
Fontes e leituras recomendadas
- Listas — documentação oficial — https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
- List comprehensions (PEP 202) — https://peps.python.org/pep-0202/
- Funções nativas: sorted, min, max, sum — https://docs.python.org/3/library/functions.html
- Módulo copy — cópia profunda de objetos — https://docs.python.org/3/library/copy.html
- MATTHES, Eric. Python Crash Course. 3. ed. No Starch Press, 2023. Cap. 3–4.
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 2.
- SEDGEWICK, Robert; WAYNE, Kevin. Algorithms. 4. ed. Addison-Wesley, 2011. Cap. 1 — para fundamentos de estruturas de dados.
Exercícios
Exercício 1
Uma função recebe um gabarito de prova como lista de listas e devolve uma versão corrigida, fazendo novo = gabarito.copy() antes de alterar. O gabarito original aparece alterado mesmo assim. O que houve?
Ver resposta
✓ Resposta: A cópia é rasa. O copy() cria uma lista nova, mas preenche essa lista com os mesmos objetos que estavam na original — e, quando os elementos são listas, as duas coleções passam a apontar para as mesmas sublistas. Alterar novo[0][2] alcança exatamente a mesma lista interna que gabarito[0][2]. O que a cópia protege é só o primeiro nível: novo.append(...) ou novo[0] = outra_coisa não afetam o original, porque aí se está mexendo na lista de fora. As três formas que o artigo apresenta — copy(), [:] e list() — têm todas esse mesmo comportamento; não existe diferença entre elas nesse ponto. Enquanto a lista guardar números, textos ou tuplas de imutáveis, o problema nunca aparece, e é justamente por isso que ele surpreende: o código funcionou durante meses com listas simples. A solução, quando se precisa mesmo de independência em qualquer profundidade, é copy.deepcopy(gabarito), que reconstrói a estrutura inteira. Ele não deve ser o padrão, porém, porque percorre tudo e custa caro em estrutura grande. Existe uma terceira via, quase sempre melhor que as duas: não copiar — construir a estrutura nova com uma compreensão, novo = [linha.copy() for linha in gabarito] quando há um nível, ou devolver dados novos em vez de alterar os recebidos.
Exercício 2
Para criar um tabuleiro 3 por 3 zerado, um colega escreve tabuleiro = [[0] * 3] * 3. Ao marcar uma jogada em tabuleiro[0][0], três casas mudam. Explique, e mostre a forma correta.
Ver resposta
✓ Resposta: O operador * aplicado a uma lista repete referências, não conteúdo. A expressão de dentro, [0] * 3, funciona como se espera e produz [0, 0, 0], porque os elementos são inteiros imutáveis. A de fora é que engana: ela pega aquela única lista e a repete três vezes — o resultado tem três posições que apontam para o mesmo objeto. Escrever em tabuleiro[0][0] altera a lista compartilhada, e as três "linhas" mostram a mudança, porque são a mesma linha vista três vezes. Dá para confirmar comparando as identidades: tabuleiro[0] is tabuleiro[1] devolve True. A forma correta é a compreensão de lista, que executa a expressão a cada volta e portanto cria uma lista nova por linha: tabuleiro = [[0] * 3 for _ in range(3)]. O sublinhado é a convenção para a variável de laço que não se usa. Vale notar que o problema não é do * em si — [0] * 1000 é a forma idiomática e eficiente de criar uma lista grande de zeros. O problema é multiplicar uma lista cujo elemento é mutável. A mesma armadilha aparece ao usar dict.fromkeys(chaves, []), onde todas as chaves recebem a mesmíssima lista.
Exercício 3
Um processamento consome itens de uma fila usando fila.pop(0) num laço. Com centenas de itens é instantâneo; com centenas de milhares, trava. A lógica está correta. O que explica a lentidão e qual é a alternativa?
Ver resposta
✓ Resposta: A lista do Python é um vetor contíguo: os elementos ficam lado a lado na memória, e é isso que torna o acesso por índice instantâneo. O preço é nas pontas. Remover do fim com pop() é imediato, porque nada precisa se mover; remover do começo com pop(0) obriga a deslocar todos os outros uma posição para trás, e o custo é proporcional ao tamanho da lista. Num laço que esvazia a fila inteira, isso se acumula: o trabalho total cresce com o quadrado do número de itens. Por isso dobrar a entrada não dobra o tempo — quadruplica. É o padrão clássico de código que passa em todos os testes com dados pequenos e morre em produção. A alternativa é collections.deque, uma fila de duas pontas feita exatamente para isso, com popleft() e appendleft() de custo constante — na medição, esvaziar vinte mil itens levou vinte e sete vezes menos tempo que com a lista. A mesma observação vale para insert(0, x), que tem o mesmo defeito na inserção. A lição mais ampla é escolher a estrutura pelo padrão de acesso, não pelo hábito: lista para acessar por índice e crescer no fim, deque para mexer nas duas pontas, set para perguntar se um item existe, dict para associar chave a valor.
Exercício 4
O artigo afirma que o fatiamento nunca gera erro de índice. Isso é conveniente ou perigoso? Compare lista[5] com lista[5:6] numa lista de três elementos.
Ver resposta
✓ Resposta: É as duas coisas, e saber qual delas se quer é o ponto. Numa lista de três elementos, lista[5] levanta IndexError e interrompe o programa ali, apontando o problema; lista[5:6] devolve [], sem reclamar. A conveniência é real e usada o tempo todo: texto[:100] pega os cem primeiros caracteres sem precisar verificar se existem cem, e lista[1:] devolve [] numa lista de um elemento, em vez de exigir um if. O perigo é que o silêncio esconde erro de lógica. Um índice calculado errado — um deslocamento trocado, uma conta de paginação furada — produz uma fatia vazia que segue adiante como se fosse resultado legítimo, e o programa termina normalmente entregando nada. O sintoma aparece páginas depois, como "o relatório veio vazio", sem nenhuma pista da causa. A regra prática: use fatiamento quando a lista poder mesmo ser menor do que o pedido, e índice direto quando o elemento tiver que existir — deixando o IndexError fazer o trabalho de avisar. E vale conhecer duas pegadinhas do fatiamento que decorrem da mesma tolerância: lista[::-1] inverte, mas lista[3:0] devolve [] em vez de inverter o trecho, e passo negativo exige inverter também os limites.
Exercício 5
Reescreva resultado = [] seguido de um for com if e append como uma compreensão de lista. Depois explique quando não vale a pena fazer essa troca.
Ver resposta
✓ Resposta: A forma é resultado = [transformar(x) for x in origem if condicao(x)], lida na mesma ordem do laço: primeiro o for, depois o filtro, e a expressão da frente é o que entra na lista. Além de mais curta, é um pouco mais rápida, porque não paga a busca e a chamada do método append a cada volta, e é mais segura, porque a variável do laço não vaza para fora — coisa que acontece no for comum. Não vale a pena em quatro situações. Quando o corpo faz mais de uma coisa: se além de montar a lista ele registra log, atualiza um contador ou grava algo, o laço explícito diz a verdade sobre o que está acontecendo. Quando a expressão exige condicional embutida aninhada — [a if c1 else b if c2 else d for x in y] é legal e ilegível. Quando há dois ou mais for encadeados e a ordem deles deixa de ser óbvia à primeira leitura. E quando o propósito não é produzir uma lista: usar compreensão só pelo efeito colateral, descartando o resultado, é desperdiçar memória construindo uma lista que ninguém vai ler. Duas variações que valem conhecer: trocar os colchetes por parênteses cria um gerador, que não constrói nada na memória e serve perfeitamente para alimentar um sum() ou um any(); e a mesma sintaxe com chaves produz conjunto ou dicionário, conforme se escreva um valor ou um par.