Python oferece mais de uma forma de agrupar dados. Enquanto listas são flexíveis e mutáveis, existem situações em que você precisa de coleções que não mudam — ou que garantam que não haja valores duplicados. Para isso existem as tuplas e os sets.
Tuplas
Uma tupla é uma sequência imutável de valores. Uma vez criada, não pode ser alterada — nenhum elemento pode ser adicionado, removido ou modificado.
coordenadas = (10.5, -23.4)
rgb = (255, 128, 0)
pessoa = ("Ricardo", 35, "São Paulo")
# Também pode ser criada sem parênteses
ponto = 4.0, 9.0
print(type(coordenadas)) # <class 'tuple'>
Acessando elementos
O acesso é idêntico ao de listas — índices e fatiamento funcionam normalmente:
pessoa = ("Ricardo", 35, "São Paulo")
print(pessoa[0]) # Ricardo
print(pessoa[-1]) # São Paulo
print(pessoa[1:]) # (35, 'São Paulo')
Por que usar tuplas?
A imutabilidade não é uma limitação — é uma garantia. Tuplas comunicam ao leitor do código que aquele conjunto de dados não deve mudar. Além disso:
- São criadas mais rápido: o literal
(1, 2, 3)vira uma constante única no bytecode, enquanto[1, 2, 3]precisa ser montada elemento a elemento a cada execução — 8,7× de diferença na criação, medido na 3.14. Já na iteração, ao contrário do que se repete por aí, a diferença é ruído: 1,02×, e a memória é praticamente igual (8.048 contra 8.056 bytes para mil elementos) - Podem ser usadas como chaves de dicionário (listas não podem)
- Protegem dados que não devem ser modificados acidentalmente
# Tupla como chave de dicionário — válido
localizacoes = {
(40.7128, -74.0060): "Nova York",
(-23.5505, -46.6333): "São Paulo",
}
# Lista como chave — gera TypeError
# {[1, 2]: "valor"} # ERRO
Desempacotamento
Uma das operações mais elegantes com tuplas:
pessoa = ("Ana", 28, "Recife")
nome, idade, cidade = pessoa
print(nome) # Ana
print(idade) # 28
# Ignorando valores com _
primeiro, _, ultimo = (1, 2, 3)
print(primeiro, ultimo) # 1 3
# Capturando o restante com *
a, *resto = (1, 2, 3, 4, 5)
print(a) # 1
print(resto) # [2, 3, 4, 5]
Tupla com um único elemento
Um erro comum ao criar tuplas de um elemento:
nao_e_tupla = (42) # isso é apenas int com parênteses
e_tupla = (42,) # a vírgula define que é tupla
tambem = 42,
print(type(nao_e_tupla)) # <class 'int'>
print(type(e_tupla)) # <class 'tuple'>
A imutabilidade é rasa
Esta é a parte que quase todo material omite, e a que mais derruba código em produção: o que a tupla congela são as referências que ela guarda, não os objetos apontados por elas. Se um dos elementos for mutável, ele continua mutável dentro da tupla.
t = (1, [2, 3])
t[1].append(4)
print(t) # (1, [2, 3, 4]) — a tupla imutável mudou
t[1] = [9] # TypeError: 'tuple' object does not
# support item assignment
Não há contradição entre as duas linhas: t[1] = [9] falha porque trocaria a referência guardada pela tupla; t[1].append(4) passa porque mexe no objeto para onde a referência aponta. É o mesmo princípio de que nome é rótulo, visto em Variáveis e Tipos de Dados, e o mesmo motivo pelo qual copiar lista com copy() produz cópia rasa, como está em Listas: criação, manipulação e métodos.
A consequência prática aparece na hora de usar a tupla como chave de dicionário. A promessa da seção anterior vale apenas se todos os elementos forem hasheáveis:
hash((1, 2)) # funciona
hash((1, [2, 3])) # TypeError: unhashable type: 'list'
{(1, [2]): "x"} # TypeError: cannot use 'tuple' as a dict key
# (unhashable type: 'list')
Sets (Conjuntos)
Um set é uma coleção não ordenada e sem elementos duplicados. É baseado na estrutura matemática de conjuntos.
linguagens = {"Python", "JavaScript", "Go", "Python", "Go"}
print(linguagens) # {'JavaScript', 'Go', 'Python'} — duplicatas removidas
numeros = {1, 2, 3, 4, 5}
vazio = set() # não use {} — isso cria um dicionário vazio
Como sets não têm ordem, não suportam indexação:
s = {10, 20, 30}
s[0] # TypeError — sets não têm índice
Operações de Conjunto
A verdadeira força dos sets está nas operações matemáticas:
python_devs = {"Ana", "Bruno", "Carla", "Diego"}
java_devs = {"Bruno", "Diego", "Eduardo", "Flávia"}
# União — todos os elementos de ambos
print(python_devs | java_devs)
# {'Ana', 'Bruno', 'Carla', 'Diego', 'Eduardo', 'Flávia'}
# Interseção — elementos em comum
print(python_devs & java_devs)
# {'Bruno', 'Diego'}
# Diferença — em python_devs mas não em java_devs
print(python_devs - java_devs)
# {'Ana', 'Carla'}
# Diferença simétrica — em um ou outro, mas não em ambos
print(python_devs ^ java_devs)
# {'Ana', 'Carla', 'Eduardo', 'Flávia'}
Métodos de Sets
s = {1, 2, 3}
s.add(4) # adiciona um elemento
s.update([5, 6]) # adiciona múltiplos elementos
s.remove(2) # remove — gera KeyError se não existir
s.discard(99) # remove — silencioso se não existir
print(3 in s) # True — pertinência em tempo praticamente constante
print(len(s)) # quantidade de elementos
A expressão muito eficiente é vaga demais para sustentar uma decisão de projeto, então aqui vai o número. Procurando cem mil vezes por um elemento que está no fim de uma coleção de cem mil itens:
set: 0,0022 s
lista: 62,06 s
São quatro ordens de grandeza. A lista compara item por item até achar; o set calcula o hash e vai direto na posição. É por isso que vale converter uma lista para set antes de um laço que faz muitas verificações de pertinência, mesmo pagando o custo da conversão — ela se paga já nas primeiras dezenas de consultas.
O que pode entrar num set
Set e dicionário compartilham a mesma exigência: todo elemento precisa ser hasheável. Lista dentro de set não entra.
{[1, 2]} # TypeError: cannot use 'list' as a set element
# (unhashable type: 'list')
{(1, 2)} # a tupla entra, porque é hasheável
E há um caso que surpreende quem valida dados: bool é subclasse de int, e True == 1. Para o set, os dois são o mesmo elemento.
print({1, True}) # {1}
print({0, False}) # {0}
print(len({1, True, 1.0})) # 1 — o float 1.0 colapsa junto
Operador ou método: a diferença que só aparece em produção
Os operadores |, &, - e ^ exigem set dos dois lados. Os métodos equivalentes aceitam qualquer iterável — e é por isso que o código passa nos testes e quebra quando alguém passa a lista que veio do banco.
{1, 2} | [2, 3] # TypeError: unsupported operand type(s)
# for |: 'set' and 'list'
{1, 2}.union([2, 3]) # {1, 2, 3} — aceita lista, tupla, gerador...
A equivalência é direta: | é union, & é intersection, - é difference e ^ é symmetric_difference. Quando a entrada não é garantidamente um set, prefira o método. Sobre como esses operadores se combinam entre si, veja Operadores e Expressões.
Verificações de subconjunto
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
print(a.issubset(b)) # True — a está contido em b
print(b.issuperset(a)) # True — b contém a
print(a.isdisjoint({6, 7})) # True — nenhum elemento em comum
Removendo duplicatas de uma lista
Um uso clássico e elegante de sets:
emails = [
"ana@email.com",
"bruno@email.com",
"ana@email.com",
"carla@email.com",
"bruno@email.com",
]
emails_unicos = list(set(emails))
print(emails_unicos)
# a ordem muda a cada execução do programa — veja abaixo
Atenção: a conversão para set não preserva a ordem original, e isso é mais forte do que costuma se dizer. Como o Python aleatoriza o hash de strings a cada processo, a ordem muda de uma execução para a outra. O mesmo programa, rodado cinco vezes seguidas:
['bruno@email.com', 'ana@email.com', 'carla@email.com']
['carla@email.com', 'bruno@email.com', 'ana@email.com']
['bruno@email.com', 'carla@email.com', 'ana@email.com']
['carla@email.com', 'bruno@email.com', 'ana@email.com']
['ana@email.com', 'bruno@email.com', 'carla@email.com']
A armadilha é que com inteiros pequenos a ordem costuma sair estável — set([3, 1, 2, 5, 4]) imprime {1, 2, 3, 4, 5} em toda execução — e daí nasce a crença de que set é ordenado. Não é: é coincidência de como inteiros pequenos são hasheados. Nunca escreva teste, nem saída para o usuário, que dependa da ordem de um set.
Se a ordem importa, o padrão idiomático passa por dicionário, que preserva a ordem de inserção desde a 3.7:
emails_unicos = list(dict.fromkeys(emails))
print(emails_unicos)
# ['ana@email.com', 'bruno@email.com', 'carla@email.com']
# esta ordem, sim, é garantida
frozenset: o set que não muda
O set resolve unicidade, mas é mutável — e por isso não pode ser elemento de outro set nem chave de dicionário. Quando você precisa das duas coisas ao mesmo tempo, existe o frozenset: mesma interface de consulta, sem os métodos que alteram.
{{1, 2}} # TypeError: cannot use 'set' as a set element
# (unhashable type: 'set')
{frozenset({1, 2})} # {frozenset({1, 2})} — funciona
permissoes = {
frozenset({"ler"}): "visitante",
frozenset({"ler", "escrever"}): "editor",
}
print(permissoes[frozenset({"escrever", "ler"})]) # editor
Repare na última linha: a ordem em que as permissões foram escritas não importa para encontrar a chave, porque conjunto não tem ordem. É exatamente o que se quer de um conjunto de permissões — e é a mesma relação que a tupla tem com a lista, um degrau acima.
Comparação: Lista, Tupla e Set
| Característica | Lista | Tupla | Set |
|---|---|---|---|
| Ordenada | ✅ | ✅ | ❌ |
| Mutável | ✅ | ❌ | ✅ |
| Permite duplicatas | ✅ | ✅ | ❌ |
| Indexável | ✅ | ✅ | ❌ |
| Hasheável (usável como chave) | ❌ | ✅ * | ❌ ** |
* A tupla só é hasheável se todos os seus elementos também forem: (1, 2) serve de chave, (1, [2]) não.
** O set não serve, mas o frozenset serve — é para isso que ele existe.
Exemplo Completo: Análise de Participação
presenca_semana1 = {"Ana", "Bruno", "Carla", "Diego", "Eduardo"}
presenca_semana2 = {"Bruno", "Carla", "Flávia", "Gustavo", "Ana"}
presenca_semana3 = {"Ana", "Carla", "Bruno", "Henrique"}
# Alunos presentes nas três semanas
sempre_presentes = presenca_semana1 & presenca_semana2 & presenca_semana3
print(f"Presentes nas 3 semanas: {sempre_presentes}")
# Todos os alunos que apareceram ao menos uma vez
todos = presenca_semana1 | presenca_semana2 | presenca_semana3
print(f"Total de alunos únicos: {len(todos)}")
# Alunos que faltaram na semana 3
faltaram = todos - presenca_semana3
print(f"Ausentes na semana 3: {faltaram}")
# Registrando datas como tuplas imutáveis
registros = [
("Ana", (2024, 3, 1)),
("Bruno", (2024, 3, 1)),
("Carla", (2024, 3, 8)),
]
for aluno, data in registros:
ano, mes, dia = data
print(f"{aluno} — {dia:02d}/{mes:02d}/{ano}")
Tuplas e sets resolvem problemas diferentes, e a escolha entre eles quase nunca é questão de gosto. A tupla serve quando o conjunto de valores não deve mudar e quando ele precisa virar chave de dicionário — com as duas ressalvas que este artigo mediu: a imutabilidade é rasa, e a chave só funciona se todos os elementos forem hasheáveis. O set serve quando a pergunta é de pertinência ou de unicidade, e aí o ganho não é estético: consultar um set de cem mil itens custou quatro ordens de grandeza menos do que percorrer a lista equivalente.
O preço do set é a ordem, que ele não tem e não finge ter — a mesma coleção sai numa sequência diferente a cada execução do programa, e depender disso é um defeito esperando a hora de aparecer. Quando unicidade e ordem são exigidas juntas, o caminho é dict.fromkeys; quando o conjunto precisa ser imutável para virar chave ou elemento de outro conjunto, é frozenset. Entre a lista que muda, a tupla que não muda e o set que não repete, a estrutura certa costuma ser a que torna o erro impossível, não a que deixa o código mais curto.
Fontes e leituras recomendadas
- Tuplas — documentação oficial — https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences
- Sets — documentação oficial — https://docs.python.org/3/tutorial/datastructures.html#sets
- Operações de conjuntos — https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset
- dict.fromkeys() para deduplicação com ordem — https://docs.python.org/3/library/stdtypes.html#dict.fromkeys
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 2 — análise detalhada de sequências e sets em Python.
- GOODRICH, Michael T. et al. Data Structures and Algorithms in Python. Wiley, 2013. Cap. 1–2 — fundamentos de estruturas de dados com exemplos em Python.
- CORMEN, Thomas H. et al. Introduction to Algorithms. 4. ed. MIT Press, 2022. Cap. 11 — tabelas hash, base teórica de sets e dicionários.
Exercícios
Exercício 1
Um sistema guarda os parâmetros de cobrança numa tupla, com o comentário de que assim ninguém altera por engano. Meses depois, a lista de feriados que estava dentro dessa tupla aparece com dois itens a mais em produção, e o código que fez isso não recebeu nenhuma reclamação do Python. Explique como isso é possível e diga o que a tupla, afinal, protegeu.
Ver resposta
✓ Resposta: A imutabilidade da tupla é rasa: o que ela congela são as referências que guarda, não os objetos apontados por elas. Com config = (0.05, ["01-01", "25-12"]), a atribuição config[1] = [] levanta TypeError: 'tuple' object does not support item assignment, porque trocaria a referência — mas config[1].append("07-09") passa sem um ruído, porque mexe no objeto para onde a referência aponta, e esse objeto é uma lista, que é mutável. A tupla protegeu a estrutura, não o conteúdo: garante que continuam sendo dois campos, na mesma ordem, apontando para os mesmos objetos. Nada além disso. A consequência mais visível aparece no dicionário: hash((1, 2)) funciona, e hash((1, [2, 3])) levanta TypeError: unhashable type: 'list', de modo que a promessa de que tupla serve como chave vale apenas quando todos os elementos também são imutáveis. Para congelar de verdade, a lista de dentro precisa virar tupla — (0.05, ("01-01", "25-12")) — e aí a estrutura inteira passa a ser hasheável. Vale desconfiar do argumento de que a tupla foi escolhida por desempenho: medido na 3.14, criar o literal é cerca de 8,7 vezes mais rápido que o da lista, porque a tupla é uma constante única no bytecode, mas percorrer uma e outra dá praticamente o mesmo número, 1,02 vezes, e a memória é igual a menos de dez bytes em mil elementos. Tupla se escolhe pela garantia que ela comunica, não pelo cronômetro.
Exercício 2
Um relatório diário lista os clientes únicos do dia com list(set(nomes)). O teste automatizado compara a saída com uma lista fixa e passou por semanas. Hoje ele começou a falhar de forma intermitente: às vezes passa, às vezes não, sem ninguém ter tocado no código. O que está acontecendo, e por que só agora?
Ver resposta
✓ Resposta: Set não tem ordem, e no caso de strings isso é ainda mais forte do que parece: o Python aleatoriza a semente de hash a cada processo, como defesa contra ataques de colisão, e por isso a ordem em que os elementos saem muda de uma execução do programa para a outra. Rodando cinco vezes o mesmo list(set(emails)) com três endereços, saíram quatro ordens diferentes. O teste nunca esteve correto; ele vinha passando por sorte, e provavelmente começou a falhar quando o conjunto de nomes mudou de tamanho e as colisões passaram a cair em posições diferentes. Fixar PYTHONHASHSEED faz a ordem voltar a ser estável e é exatamente a correção errada: esconde o defeito e ainda desliga a proteção. O que engana muita gente é que com inteiros pequenos a ordem costuma sair estável — set([3, 1, 2, 5, 4]) imprime {1, 2, 3, 4, 5} em toda execução, porque o hash de um inteiro pequeno é ele mesmo — e daí nasce a crença de que set é ordenado. Há duas saídas, conforme o que se quer: se a ordem precisa ser a de primeira aparição, list(dict.fromkeys(nomes)) resolve, porque dicionário preserva ordem de inserção desde a 3.7; se o relatório deve sair em ordem alfabética, sorted(set(nomes)) diz isso explicitamente. Em qualquer dos casos, o teste deve comparar set com set, ou uma lista ordenada com outra — nunca a saída crua de um set.
Exercício 3
Uma rotina recebe identificadores vindos de duas origens e os junta para remover repetidos: ids = {1, 2, 3} da primeira, e da segunda chega True, porque um campo booleano foi lido na mesma coluna. O time esperava quatro elementos no set final e encontrou três, sem erro nenhum. Explique.
Ver resposta
✓ Resposta: Em Python, bool é subclasse de int: isinstance(True, int) devolve True, e True == 1. Como set e dicionário decidem identidade por hash e igualdade, e hash(True) == hash(1), os dois são literalmente o mesmo elemento para essas estruturas. Daí {1, True} resultar em {1}, e {0, False} em {0}. O efeito não para no booleano: len({1, True, 1.0}) é 1, porque o float 1.0 também é igual a 1 e colapsa junto. Qual dos três sobrevive depende de quem entrou primeiro, já que o set mantém o elemento que já estava e descarta o novo — o que significa que o tipo do que ficou no conjunto depende da ordem de inserção, um detalhe traiçoeiro quando esse valor é usado adiante. O mesmo vale para chaves de dicionário: {1: 'a'}[True] devolve 'a'. A correção não é no set, é na fronteira: identificador é dado de domínio e precisa ser convertido e validado na entrada, com int() explícito e uma checagem que rejeite booleano — lembrando que isinstance(valor, int) sozinho aceita True de bom grado, e por isso é preciso testar type(valor) is int ou barrar bool antes. Vale ainda notar que {[1, 2]} nem chega a ser aceito: levanta TypeError: cannot use 'list' as a set element, porque todo elemento de set precisa ser hasheável.
Exercício 4
Uma função calcula quais permissões faltam a um usuário com necessarias - usuario.permissoes. Passa em todos os testes, onde as permissões são escritas como {"ler", "escrever"}. Em produção, o objeto vem do banco e o campo chega como lista. A função quebra. Diga qual é o erro e qual a correção mais robusta.
Ver resposta
✓ Resposta: O erro é TypeError: unsupported operand type(s) for -: 'set' and 'list'. Os operadores de conjunto — |, &, - e ^ — exigem set dos dois lados, e recusam qualquer outro iterável. Os métodos equivalentes não: union, intersection, difference e symmetric_difference aceitam lista, tupla, gerador, qualquer coisa percorrível. Por isso {1, 2} | [2, 3] levanta TypeError enquanto {1, 2}.union([2, 3]) devolve {1, 2, 3} tranquilamente. Esse é um defeito de fronteira clássico, e a assimetria entre teste e produção é o que o torna invisível: no teste alguém escreve o literal com chaves e obtém um set; em produção o dado atravessa serialização — JSON não tem tipo conjunto, então toda viagem por JSON devolve lista. Trocar o operador pelo método faz a função parar de quebrar, e é a correção certa quando a função é uma fronteira que aceita o que vier. A correção mais robusta, porém, é normalizar na entrada: converter para set logo ao ler do banco, de modo que o tipo esteja garantido em todo o resto do código e os operadores voltem a ser seguros e legíveis. A regra prática: use o método quando não puder confiar no tipo que chega, e o operador depois de ter normalizado — nunca deixe a escolha ao acaso do caminho que o dado percorreu.
Exercício 5
Um script cruza duas bases: para cada um dos cem mil registros da primeira, verifica se o documento aparece na segunda, que também tem cem mil e está numa lista. O script leva mais de um minuto. Um colega sugere trocar a lista por um set. Estime o ganho e explique de onde ele vem. Depois diga o que fazer se a chave de busca precisar ser um conjunto de campos, e não um valor só.
Ver resposta
✓ Resposta: O ganho é de quatro ordens de grandeza. Medindo cem mil buscas por um elemento que está no fim de uma coleção de cem mil itens, a lista levou 62,06 s e o set, 0,0022 s — cerca de 28 mil vezes mais rápido. A razão é estrutural: x in lista compara item por item até achar, e portanto o custo cresce junto com o tamanho; x in set calcula o hash de x e vai direto à posição, com custo praticamente constante seja qual for o tamanho. Converter a segunda base para set custa uma passada única sobre ela, e esse custo se paga já nas primeiras dezenas de consultas — com cem mil, nem entra na conta. Vale o alerta de que o ganho só existe para pertinência, unicidade e operações de conjunto; se o código precisa de ordem ou de índice, o set não serve, porque não tem nem um nem outro. Para a segunda parte: se a chave é um conjunto de campos, a escolha depende de a ordem importar. Sendo uma combinação fixa e ordenada, como CPF mais data, o natural é a tupla, que é hasheável e serve de chave direto. Sendo um conjunto de verdade, em que a ordem não deve contar — um jogo de permissões, um grupo de etiquetas —, aí é frozenset, a versão imutável do set: {{1, 2}} levanta TypeError: cannot use 'set' as a set element, mas {frozenset({1, 2})} funciona, e permissoes[frozenset({"escrever", "ler"})] encontra a chave gravada como frozenset({"ler", "escrever"}), porque conjunto não tem ordem.