List, dict, set comprehension em Python: escrever conciso e legível

1. Visão geral

Comprehension é uma sintaxe característica do Python. Ela permite criar list, dict ou set a partir de um iterável em apenas uma linha. Escrito corretamente, o código fica mais conciso e claro do que um for. Abusando, o código vira uma linha longa e incompreensível.

Este artigo passa por list, dict, set comprehension e generator expression através de exemplos práticos. Após ler, você saberá quando usar comprehension, quando voltar para for e como escrever testes para lógica de transformação de dados. Todos os exemplos rodam em Python 3.12.

2. List comprehension

2.1. Sintaxe básica

Suponha que você tenha uma lista de pedidos e precise extrair o valor total de cada um. A forma tradicional usando for seria assim:

from dataclasses import dataclass

@dataclass
class Order:
    id: int
    customer: str
    total: float
    status: str

orders = [
    Order(1, "An", 250_000, "PAID"),
    Order(2, "Bình", 90_000, "CANCELLED"),
    Order(3, "An", 1_200_000, "PAID"),
    Order(4, "Chi", 450_000, "PENDING"),
]

totals = []
for order in orders:
    totals.append(order.total)
print(totals)

Reescrito com list comprehension:

totals = [order.total for order in orders]
print(totals)
[250000, 90000, 1200000, 450000]

A estrutura é [expressão for elemento in iterável]. Leia da esquerda para a direita: “pegue order.total para cada order em orders”. Você não precisa declarar uma lista vazia nem chamar append. A intenção “estou criando uma nova lista” fica clara de imediato pelo [.

2.2. Filtrando elementos com if

Se quiser apenas pedidos já pagos, adicione uma cláusula if no final:

paid_ids = [order.id for order in orders if order.status == "PAID"]
print(paid_ids)
[1, 3]

if no final funciona como um filtro. Elementos que não satisfazem a condição são descartados, então a lista resultante pode ser menor que a original.

2.3. Expressão if-else no início

Quando quer transformar cada elemento baseado em uma condição em vez de filtrar, coloque if-else na expressão:

labels = ["grande" if order.total >= 500_000 else "pequeno" for order in orders]
print(labels)
['pequeno', 'pequeno', 'grande', 'pequeno']

Este é um ponto em que muitas pessoas se confundem. Você só precisa lembrar duas regras:

  • if depois de for é usado para filtrar. Esta cláusula não tem else.
  • a if cond else b antes de for é uma expressão condicional. Esta cláusula exige else e mantém a mesma quantidade de elementos.

Escrever [x for x in data if cond else y] causará SyntaxError.

3. Dict comprehension

Dict comprehension usa chaves {} com pares key: value. Por exemplo, você precisa consultar pedidos rapidamente pelo id:

orders_by_id = {order.id: order for order in orders}
print(orders_by_id[3])
Order(id=3, customer='An', total=1200000, status='PAID')

Você também pode transformar um dict existente. Por exemplo, aplicar 10% de imposto a uma tabela de preços:

prices = {"café": 30_000, "chá_com_leite": 45_000, "pão": 20_000}
prices_with_vat = {name: round(price * 1.1) for name, price in prices.items()}
print(prices_with_vat)
{'café': 33000, 'chá_com_leite': 49500, 'pão': 22000}

prices.items() retorna pares (key, value), e você desempacota direto no for.

Uma dica útil é inverter o dict:

status_codes = {"PAID": 1, "PENDING": 2, "CANCELLED": 3}
code_to_status = {code: status for status, code in status_codes.items()}
print(code_to_status[2])
PENDING

Nota: Se dois elementos resultarem na mesma key, o elemento posterior sobrescreverá silenciosamente o anterior. Por exemplo, {order.customer: order.total for order in orders} mantém apenas o último pedido de “An” (1.200.000), e o de 250.000 desaparece sem nenhum aviso. Quando precisar agrupar, use defaultdict com for (veja a seção 7).

4. Set comprehension

Set comprehension também usa {} mas com apenas uma expressão, sem :. O resultado remove automaticamente duplicatas. Por exemplo, você quer saber quais clientes fizeram pedidos:

customers = {order.customer for order in orders}
print(sorted(customers))
['An', 'Bình', 'Chi']

Cada cliente aparece apenas uma vez, mesmo que “An” tenha dois pedidos. A ordem dentro do set não é garantida, então o exemplo usa sorted ao imprimir.

Set comprehension é muito útil quando você precisa verificar associação rapidamente:

banned_emails = {"spam@x.com", "Fake@Y.com"}
normalized = {email.strip().lower() for email in banned_emails}
print("fake@y.com" in normalized)
True

Nota: {} vazio é um dict, não um set. Para criar um set vazio, você precisa escrever set().

5. Múltiplos for e comprehension aninhada

Você pode escrever múltiplos for em uma comprehension. A ordem de leitura é exatamente igual a quando você escreve loops for aninhados de cima para baixo:

carts = {
    "An": ["café", "pão"],
    "Chi": ["chá_com_leite"],
}

items = [(customer, item) for customer, products in carts.items() for item in products]
print(items)
[('An', 'café'), ('An', 'pão'), ('Chi', 'chá_com_leite')]

A comprehension acima é equivalente ao seguinte código:

items = []
for customer, products in carts.items():
    for item in products:
        items.append((customer, item))

O caso mais comum é achatar (flatten) uma lista aninhada:

matrix = [[1, 2, 3], [4, 5, 6]]
flat = [value for row in matrix for value in row]
print(flat)
[1, 2, 3, 4, 5, 6]

Forma de lembrar: o loop for mais externo vem primeiro. Se você precisar parar e ler uma segunda vez para entender a ordem, é sinal de que deve dividir em loops for comuns.

6. Generator expression: quando não precisa de uma list

Se trocar [] por (), você terá uma generator expression. Python não cria uma list em memória. Os valores são gerados sob demanda conforme o código os consome.

revenue = sum(order.total for order in orders if order.status == "PAID")
print(revenue)
1450000

Quando o generator é o único argumento de uma função, você pode omitir um par de parênteses. Isso é adequado para sum, any, all, max, min:

has_big_order = any(order.total > 1_000_000 for order in orders)
all_positive = all(order.total > 0 for order in orders)
print(has_big_order, all_positive)
True True

any para assim que encontra o primeiro elemento que satisfaz a condição. Portanto, com dados grandes, o generator economiza memória e é mais rápido do que any([...]).

Nota: Você pode iterar um generator apenas uma vez. A segunda iteração não retornará nada:

paid = (o for o in orders if o.status == "PAID")
print(len(list(paid)))
print(len(list(paid)))
2
0

Se precisar usar o resultado várias vezes, use list comprehension.

7. Mantendo comprehension legível

7.1. Nomeando funções para lógica complexa

Condições longas tornam a comprehension difícil de ler:

vip = [o.customer for o in orders if o.status == "PAID" and o.total >= 1_000_000 and not o.customer.startswith("test")]

Você pode separar a condição em uma função com nome significativo:

def is_vip_order(order: Order) -> bool:
    return (
        order.status == "PAID"
        and order.total >= 1_000_000
        and not order.customer.startswith("test")
    )

vip = [order.customer for order in orders if is_vip_order(order)]
print(vip)
['An']

Agora a comprehension lê como uma frase, e a lógica de negócio tem seu próprio lugar para ser testada.

7.2. Quebras de linha sensatas

Comprehension não precisa estar em uma única linha. Dentro dos parênteses, você pode quebrar linhas à vontade:

summary = {
    order.id: order.total
    for order in orders
    if order.status != "CANCELLED"
}
print(summary)
{1: 250000, 3: 1200000, 4: 450000}

Cada linha tem um papel: expressão, fonte, filtro.

7.3. Walrus operator para evitar calcular duas vezes

Às vezes você quer filtrar e usar o resultado do mesmo cálculo. O operador := (Python 3.8+) permite calcular apenas uma vez:

def discount_for(order: Order) -> float:
    return order.total * 0.05 if order.total >= 400_000 else 0

discounts = {
    order.id: d
    for order in orders
    if (d := discount_for(order)) > 0
}
print(discounts)
{3: 60000.0, 4: 22500.0}

Use o walrus com parcimônia. A variável d vai “vazar” para o escopo fora da comprehension. Além disso, muitos leitores ainda não estão familiarizados com essa sintaxe.

8. Testando lógica de transformação de dados

Comprehension geralmente fica dentro de funções de transformação de dados, então você deve testá-las como qualquer outra lógica. Primeiro, instale pytest:

pip install pytest

Suponha que você tenha um módulo reports.py:

# reports.py
from dataclasses import dataclass

@dataclass
class Order:
    id: int
    customer: str
    total: float
    status: str

def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
    customers = {o.customer for o in orders if o.status == "PAID"}
    return {
        c: sum(o.total for o in orders if o.customer == c and o.status == "PAID")
        for c in customers
    }

E o arquivo de teste correspondente:

# test_reports.py
from reports import Order, revenue_by_customer

def test_revenue_only_counts_paid_orders():
    orders = [
        Order(1, "An", 250_000, "PAID"),
        Order(2, "An", 1_200_000, "PAID"),
        Order(3, "Bình", 90_000, "CANCELLED"),
        Order(4, "Chi", 450_000, "PAID"),
    ]

    result = revenue_by_customer(orders)

    assert result == {"An": 1_450_000, "Chi": 450_000}
    assert "Bình" not in result

def test_empty_orders_returns_empty_dict():
    assert revenue_by_customer([]) == {}
pytest -q
..                                                                       [100%]
2 passed in 0.01s

O primeiro teste afirma duas coisas: pedidos cancelados não são contados, e clientes com apenas pedidos cancelados não aparecem no resultado. O segundo teste verifica o caso extremo com uma lista vazia.

A função acima está correta mas tem complexidade O(n × número de clientes) porque cada cliente varre todos os orders. Com dados grandes, um loop for com defaultdict é mais rápido e legível:

from collections import defaultdict

def revenue_by_customer(orders: list[Order]) -> dict[str, float]:
    totals: dict[str, float] = defaultdict(float)
    for order in orders:
        if order.status == "PAID":
            totals[order.customer] += order.total
    return dict(totals)

Os dois testes antigos continuam passando sem modificação. Esse é precisamente o benefício de ter testes: você pode refatorar com confiança entre comprehension e for ou vice-versa.

9. Quando NÃO usar comprehension

  • Quando precisa de side effects. Não escreva [print(o) for o in orders] ou [db.save(o) for o in orders]. Isso cria uma lista None sem sentido e engana o leitor pensando que você precisa do resultado. Use um loop for.
  • Quando aninhado além de dois níveis ou com múltiplos if intercalados. Três loops for em uma linha quase sempre são mais difíceis de ler do que a versão separada.
  • Quando precisar agrupar ou acumular como somar ou contar. Use defaultdict, Counter ou um loop for.
  • Quando precisar tratar exceções para cada elemento. Comprehension não pode conter try/except. Você deve extrair em uma função separada e chamá-la na comprehension, ou usar um loop for.
  • Quando os dados são muito grandes e você os itera apenas uma vez. Então use generator expression em vez de list.

Nota: Em Python 3, a variável de loop do comprehension não vazará. Após [x for x in range(3)], o nome x não existe no escopo externo, ao contrário de um loop for comum. A única exceção é a variável atribuída com := conforme mencionado na seção 7.3.

10. Conclusão

Comprehension ajuda você a expressar “criar uma nova coleção de uma coleção existente” de forma concisa:

  • [...] para list, {k: v ...} para dict, {...} para set, e (...) para generator.
  • if depois de for é usado para filtrar, enquanto a if cond else b antes de for é para transformar.
  • Extraia condições complexas em funções com nomes significativos, e quebre linhas quando a expressão for longa.
  • Use generator expression com sum, any, all para economizar memória.
  • Volte para for quando há side effects, quando precisar agrupar, tratar exceções, ou quando a comprehension ficar difícil de ler.

A regra mais simples: se um colega precisar ler sua comprehension duas vezes para entender, reescreva com um loop for. Código conciso só tem valor se permanecer legível.

pythoncomprehensionclean codegeneratorpytest