Em uma API, encontrei esta operação de compra:

{
  "operation": "buy",
  "unit-cost": 10.00,
  "quantity": 100
}

Parece um JSON qualquer, até você tentar tipar essa entrada com TypedDict.

Repare na chave unit-cost.

O problema Link para o cabeçalho

Normalmente um TypedDict é declarado como uma classe, e cada chave vira um atributo.

class RawOperation(TypedDict):
    operation: Literal["buy", "sell"]
    # e agora?
    unit-cost: float
    quantity: int
# SyntaxError: illegal target for annotation
# para o Python, unit-cost parece uma subtração entre unit e cost

O código nem chega ao mypy, ele para antes disso.

A primeira vontade é trocar o hífen por sublinhado. Mas unit_cost não é a chave que veio da API.

Inventar outro nome só faz o tipo mentir sobre o dado que ele representa.

A solução Link para o cabeçalho

TypedDict também aceita uma sintaxe funcional. Nela os campos ficam em um dicionário comum e podem ter qualquer nome que o JSON aceite.

from typing import Literal, TypedDict

RawOperation = TypedDict(
    "RawOperation",
    {
        "operation": Literal["buy", "sell"],
        "unit-cost": float,
        "quantity": int,
    },
)

Pronto. O editor entende raw["unit-cost"], o type checker sabe que ali vem um float e ninguém precisa adivinhar qual nome a API usa.

É o que a documentação recomenda para chaves com hífen e para nomes que não podem virar atributos.

E dentro da aplicação? Link para o cabeçalho

Eu deixo esse tipo perto da leitura do JSON e logo depois converto para um objeto do domínio.

import json


def parse_json_line(line: str) -> list[Operation]:
    raw_ops_list: list[RawOperation] = json.loads(line)
    # Operation é o dataclass do domínio, não um TypedDict
    return [
        Operation(
            operation=raw["operation"],
            unit_cost=Money(str(raw["unit-cost"])),
            quantity=raw["quantity"],
        )
        for raw in raw_ops_list
    ]

O hífen fica no contrato externo, onde ele pertence, e o resto do código só conhece unit_cost.

E se a API mandar "operation": "transfer"?

O mypy continua feliz, ele acredita no Literal que eu escrevi. Na hora de rodar, o match lá no cálculo do imposto cai no assert_never.

AssertionError: Expected code to be unreachable, but got: 'transfer'

Quebrou longe de onde o dado entrou. TypedDict descreve o que eu espero receber, conferir se foi isso que chegou é outro trabalho.

De onde veio o exemplo Link para o cabeçalho

O exemplo veio do capital-gains, uma CLI que calcula imposto sobre operações de renda variável. É o mesmo projeto do artigo “O Objeto Dinheiro”, de onde sai o Money usado aqui.

Então é isso, pessoal!

Até a próxima!

{}’s