Trilha de aprendizado · Nível 12 · Tutorial 11

Converter dados externos em valores validados e tipados

Construir uma fronteira de entrada que receba dados externos sem garantias, valide sua estrutura e suas regras e entregue valores com um contrato estático confiável.

  • Nível: Avançado
  • Duração: 24 min
  • 8 passos
Converter dados externos em valores validados e tipados

O que você vai percorrer

  1. Delimitar a fronteira de entrada Posicione uma função validadora entre o JSON externo e o restante do programa, impedindo que um valor dinâmico se espalhe como Any. 2 min
  2. Reconhecer o que não valida os dados Diferencie afirmações estáticas de verificações reais feitas em tempo de execução. 3 min
  3. Validar a estrutura antes de acessar os campos Rejeite uma raiz incompatível e chaves obrigatórias ausentes antes de extrair valores ainda não validados. 3 min
  4. Estreitar os tipos de cada campo Use verificações progressivas para transformar valores locais anotados como object em valores seguros para uso tipado. 4 min
  5. Validar regras que os tipos não expressam Aplique regras do domínio depois do estreitamento de tipos: normalize o nome, aceite quantidades não negativas e mantenha explícita a política para etiquetas. 3 min
  6. Construir o resultado tipado Integre todas as verificações em uma função que separa a entrada externa de um Produto construído com valores validados. 3 min
  7. Combinar mypy com testes de rejeição Use análise estática e testes de execução como evidências complementares para uma fronteira que transforma dados externos em Produto. 4 min
  8. Aplicar e revisar a fronteira completa Aplique a validação de um Produto, verifique-a localmente e consolide as garantias obtidas na fronteira de entrada. 4 min

O que você vai aprender

  • Conter tipos dinâmicos vindos de uma desserialização em uma entrada anotada como object.
  • Validar campos progressivamente e construir um resultado compatível com um TypedDict.
  • Distinguir uma afirmação feita com cast de uma verificação real dos dados.
  • Combinar a execução do mypy com testes de entradas estrutural ou semanticamente inválidas.

Antes de começar

  • Descrever registros com TypedDict
  • Verificar anotações com mypy
  • Tratar valores opcionais e uniões de tipos
  • Salvar e carregar dados em JSON
  • Validar entradas e sinalizar falhas com raise
  • Selecionar casos de teste e verificar exceções

Passo 1 de 8

Delimitar a fronteira de entrada

Posicione uma função validadora entre o JSON externo e o restante do programa, impedindo que um valor dinâmico se espalhe como Any.

Da entrada externa ao contrato interno

Uma fronteira para dados sem garantias

Texto JSON pode ser desserializado com sucesso e ainda assim ter uma forma incompatível com o programa. Por isso, trate o resultado da desserialização como uma entrada sem garantias.

No caso condutor, o código consumidor precisa receber um Produto com três campos obrigatórios:

  • nome: str
  • quantidade: int
  • etiquetas: list[str]

A função validadora será a fronteira: ela recebe o dado externo, verifica-o nas próximas etapas e só então devolve um Produto — ou sinaliza uma falha.

Fluxo da fronteira de entrada

O valor externo não deve seguir diretamente para o código que depende do contrato.

Diagrama horizontal mostrando texto JSON passando por desserialização para um valor sem garantias, depois por uma função validadora, chegando a um registro Produto tipado usado pelo código consumidor.

A validação separa o mundo externo, sem contrato confiável, do código que consome um Produto.

Conter o Any na chegada

Anote como object

json.loads é dinâmico: seu resultado pode ser qualquer valor JSON. Em vez de deixar esse Any alcançar o restante do programa, atribua-o a uma variável anotada como object e receba object na fronteira.

Isso não verifica nada em tempo de execução. A anotação apenas limita, para o mypy, as operações que você pode fazer antes de justificar o tipo do valor.

Esqueleto da fronteira

Este é apenas o contrato inicial. As verificações que permitem retornar o valor serão construídas nos próximos passos.

python
import json
from typing import TypedDict


class Produto(TypedDict):
    nome: str
    quantidade: int
    etiquetas: list[str]


def validar_produto(dados: object) -> Produto:
    """Retorna Produto após validar os dados; falha se a entrada for inválida."""
    raise NotImplementedError


texto_json = '{"nome": "Cabo USB", "quantidade": 3, "etiquetas": ["eletrônicos"]}'
dado_externo: object = json.loads(texto_json)

produto = validar_produto(dado_externo)
print(produto["nome"])

Prática: organize a passagem pela fronteira

Ordem do fluxo

Coloque as etapas na ordem em que devem ocorrer para impedir que dados externos sejam usados como se já fossem um Produto.

  1. Passar o object para a função validadora
  2. Guardar o resultado externo em um valor anotado como object
  3. Ler o texto JSON e desserializá-lo
  4. Consumir o Produto retornado pela função

Passo 2 de 8

Reconhecer o que não valida os dados

Diferencie afirmações estáticas de verificações reais feitas em tempo de execução.

Afirmação não é inspeção

O que cast realmente faz

cast(Produto, valor) diz ao verificador estático: “trate valor como um Produto”. Ele não examina o valor em tempo de execução, não converte seus campos e não cria uma cópia.

Por isso, um cast pode silenciar um diagnóstico do mypy, mas não torna dados externos confiáveis.

Dois caminhos diferentes

Compare uma afirmação de tipo com uma validação efetiva.

Diagrama dividido mostrando um dado externo desorganizado que recebe apenas um selo de tipo no caminho da esquerda e, no caminho da direita, passa por inspeções antes de seguir como registro tipado.

Um selo de tipo não altera o dado; a validação precisa inspecionar o valor recebido.

Atenção

Não confunda os papéis

cast serve para informar algo ao analisador estático quando você já tem uma justificativa externa para o tipo. Ele não deve ser usado como substituto para validar dados vindos de JSON, arquivos, rede ou usuário.

O erro continua dentro do objeto

Um cast que não protege o acesso

Considere que bruto veio de uma fonte externa e contém:

{"nome": 42, "quantidade": "muitas", "etiquetas": ["novo", 7]}

python
from typing import TypedDict, cast

class Produto(TypedDict):
    nome: str
    quantidade: int
    etiquetas: list[str]

bruto: object = {
    "nome": 42,
    "quantidade": "muitas",
    "etiquetas": ["novo", 7],
}

produto = cast(Produto, bruto)
print(produto["nome"].upper())  # falha em execução: 42 não tem upper()

O que aconteceu?

Depois do cast, o mypy passa a considerar produto["nome"] como str. Porém, o objeto original ainda guarda o inteiro 42. Ao chamar .upper(), o programa falha.

O cast também não cria um novo dicionário: produto e bruto apontam para o mesmo objeto.

O contêiner externo também não basta

O alcance de isinstance

isinstance(valor, dict) confirma apenas que o valor é um dicionário. Não confirma que as chaves obrigatórias existem nem que seus valores têm os tipos esperados. Do mesmo modo, isinstance(valor, list) não confirma o tipo de cada elemento.

Testes que verificam só a camada externa

Os dois testes abaixo podem passar mesmo para dados incompatíveis com Produto.

python
valor: object = {
    "nome": 42,
    "etiquetas": ["novo", 7],
}

print(isinstance(valor, dict))                 # True
print(isinstance(valor["etiquetas"], list))    # True
# Ainda falta "quantidade"; nome não é str;
# e um elemento de etiquetas não é str.

Atenção

Sem atalhos com tipos tipados

Não use isinstance(valor, list[str]): tipos parametrizados não são validadores de execução. Também não use isinstance(valor, Produto): TypedDict descreve um contrato estático e não funciona como classe verificável nesse teste.

Primeiro, verifique contêineres concretos como dict e list; depois, inspecione separadamente a estrutura e os valores.

Verifique a distinção

Qual trecho valida o contrato completo?

Qual alternativa, sozinha, realmente comprova que um valor externo satisfaz o contrato completo de Produto?

Passo 3 de 8

Validar a estrutura antes de acessar os campos

Rejeite uma raiz incompatível e chaves obrigatórias ausentes antes de extrair valores ainda não validados.

Primeiro, confirme a forma da entrada

Acesso seguro começa pela estrutura

A função recebe object, portanto ainda não pode usar dado["nome"]. Primeiro, confirme que a raiz é um dicionário. Depois, confirme separadamente a presença de nome, quantidade e etiquetas.

Uma chave ausente é uma falha estrutural. Já uma chave presente com valor None passou apenas pela verificação de presença: seu valor ainda será validado em outra etapa. Assim, a fronteira produz ValueError previsível, em vez de deixar um KeyError acidental escapar.

Ordem das verificações estruturais

Diagrama de fluxo mostrando um valor externo: se não for um dicionário, falha na raiz; se for dicionário, cada uma das chaves nome, quantidade e etiquetas é verificada; só então os três valores são extraídos como object. Uma chave extra fica fora da saída.

Verificar a raiz e as chaves vem antes de consultar qualquer campo obrigatório.

Extrair sem supor os tipos dos valores

A estrutura não comprova os campos

Mesmo após isinstance(dado, dict), os valores armazenados no dicionário não ganharam os tipos do contrato. Por isso, extraia cada campo para uma variável anotada como object.

Neste caso, chaves extras são permitidas, mas ignoradas: a próxima etapa trabalhará somente com os três valores necessários. Ainda não valide str, int ou os elementos das etiquetas aqui.

Portão estrutural do Produto

python
def extrair_campos_obrigatorios(dado: object) -> tuple[object, object, object]:
    if not isinstance(dado, dict):
        raise ValueError("raiz: esperado um objeto JSON")

    for campo in ("nome", "quantidade", "etiquetas"):
        if campo not in dado:
            raise ValueError(f"raiz.{campo}: campo obrigatório ausente")

    # As chaves existem, mas seus valores continuam sem tipo comprovado.
    nome: object = dado["nome"]
    quantidade: object = dado["quantidade"]
    etiquetas: object = dado["etiquetas"]

    # Chaves extras não são extraídas nem transferidas adiante.
    return nome, quantidade, etiquetas

Diagnosticar a falha estrutural

Associe o caso à decisão correta

Relacione cada entrada ou situação com a verificação que deve tratá-la primeiro.

Toque em um item e depois no par correspondente.

Passo 4 de 8

Estreitar os tipos de cada campo

Use verificações progressivas para transformar valores locais anotados como object em valores seguros para uso tipado.

De object para componentes conhecidos

Verifique antes de usar

Após validar a estrutura, os valores extraídos ainda são object. Portanto, não chame operações específicas nem construa o resultado final antes de estreitá-los.

Cada guarda que falha encerra aquele caminho com raise. Nos caminhos restantes, o mypy pode reconhecer o tipo confirmado por isinstance.

Estreitamento progressivo

Diagrama mostrando três valores genéricos passando por guardas de tipo: texto se torna str, número se torna int após excluir bool, e uma lista genérica tem cada elemento verificado antes de formar list[str].

Cada etapa produz uma garantia menor e mais específica: do valor externo genérico para um componente com tipo conhecido.

Atenção

int também aceita bool

Em Python, bool é subtipo de int. Logo, isinstance(True, int) é True. Se o contrato aceita quantidade inteira, mas não valores booleanos, teste e rejeite bool explicitamente antes de aceitar int.

Aplicar guardas e criar uma nova lista

Estreitamento dos três campos

Este trecho recebe os valores já extraídos da estrutura. Ele ainda não monta o Produto: apenas devolve componentes com tipos conhecidos.

python
def estreitar_campos(
    nome_raw: object,
    quantidade_raw: object,
    etiquetas_raw: object,
) -> tuple[str, int, list[str]]:
    if not isinstance(nome_raw, str):
        raise ValueError("nome deve ser str")
    nome = nome_raw

    if isinstance(quantidade_raw, bool):
        raise ValueError("quantidade não pode ser bool")
    if not isinstance(quantidade_raw, int):
        raise ValueError("quantidade deve ser int")
    quantidade = quantidade_raw

    if not isinstance(etiquetas_raw, list):
        raise ValueError("etiquetas deve ser list")

    etiquetas: list[str] = []
    for indice, item in enumerate(etiquetas_raw):
        etiqueta: object = item
        if not isinstance(etiqueta, str):
            raise ValueError(f"etiquetas[{indice}] deve ser str")
        etiquetas.append(etiqueta)

    return nome, quantidade, etiquetas

Dica

Não afirme o tipo da lista original

Mesmo depois de confirmar que etiquetas_raw é uma list, seus elementos ainda precisam ser tratados como object. A nova list[str] só recebe elementos que passaram pela verificação individual. Não use cast para declarar que a lista externa já é list[str].

Proteja a quantidade contra bool

Complete a primeira guarda

Complete a condição antes da verificação de int:

if ________:
    raise ValueError("quantidade não pode ser bool")

if not isinstance(quantidade_raw, int):
    raise ValueError("quantidade deve ser int")

Verifique cada etiqueta

Complete a guarda do elemento

A lista externa já foi confirmada como list, mas cada item ainda precisa ser validado:

etiquetas: list[str] = []
for indice, item in enumerate(etiquetas_raw):
    etiqueta: object = item
    if ________:
        raise ValueError(f"etiquetas[{indice}] deve ser str")
    etiquetas.append(etiqueta)

Passo 5 de 8

Validar regras que os tipos não expressam

Aplique regras do domínio depois do estreitamento de tipos: normalize o nome, aceite quantidades não negativas e mantenha explícita a política para etiquetas.

Tipo correto ainda não basta

Contrato de tipo × regra do domínio

Depois de confirmar os tipos, ainda falta decidir se os valores são aceitáveis para o domínio. Um str pode ser vazio ou conter apenas espaços; um int pode ser negativo. Essas situações são compatíveis com as anotações, mas podem violar o contrato de Produto.

Duas etapas de aceitação

Primeiro, o valor precisa ter o tipo esperado. Depois, precisa obedecer às regras definidas para o campo.

Diagrama de um fluxo de validação em duas etapas: verificação de tipo seguida de verificação semântica. Um texto composto só de espaços e um inteiro negativo passam pela forma do tipo, mas são rejeitados pelas regras do domínio.

str e int descrevem a forma do valor; regras semânticas decidem se ele é aceito.

Exemplo

Mesmo tipo, resultados diferentes

  • nome = " ": é str, mas será rejeitado após normalização.
  • quantidade = -3: é int, mas será rejeitado pela regra de não negatividade.
  • quantidade = 0: é int e é aceito pelo contrato deste caso.
  • quantidade = "3": tem aparência numérica, mas é str; foi rejeitado antes, na etapa de tipo.

Normalizar e validar o nome

A ordem importa

Use strip() somente depois de já saber que nome é str. A normalização remove espaços das extremidades; em seguida, compare o resultado com a string vazia. Assim, um nome como " Caderno " é aceito como "Caderno", enquanto " " é rejeitado.

Regra semântica para nome

Considere que nome já foi estreitado para str em uma etapa anterior.

python
nome_normalizado = nome.strip()
if nome_normalizado == "":
    raise ValueError("nome: não pode ficar vazio após normalização")

Dica

Não confunda normalizar com converter

A normalização preserva o fato de o valor ser texto. Ela não autoriza chamar strip() em um object ainda não verificado nem transforma valores de outros tipos em nomes válidos.

Quantidade e etiquetas: políticas explícitas

Valores de fronteira do contrato

Para quantidade, a regra é quantidade >= 0: zero representa uma quantidade válida; valores negativos são rejeitados. Não há coerção implícita: "12" não vira 12.

Para etiquetas, uma lista vazia é aceita. A regra é que cada elemento existente seja str; ela não exige ao menos uma etiqueta.

Regras após o estreitamento

Considere que quantidade já é um int não booleano e que cada etiqueta já foi confirmada como str.

python
if quantidade < 0:
    raise ValueError("quantidade: deve ser não negativa")

# [] é válida. Se houver elementos, todos já devem ter sido verificados como str.
etiquetas_validadas: list[str] = []
for etiqueta in etiquetas:
    if not isinstance(etiqueta, str):
        raise ValueError("etiquetas: cada elemento deve ser str")
    etiquetas_validadas.append(etiqueta)

Atenção

Sem conversões silenciosas

Não use int(valor) para fazer "12" passar como quantidade neste contrato. Essa conversão mudaria a política de aceitação: aqui, a entrada deve já conter um inteiro válido, e não apenas um texto conversível.

Verifique a decisão do contrato

Nome composto apenas de espaços

A entrada nome = " " deve ser aceita porque seu tipo é str.

Quantidade no limite

Qual valor de quantidade, já confirmado como int e não booleano, deve ser aceito pela regra deste caso?

Passo 6 de 8

Construir o resultado tipado

Integre todas as verificações em uma função que separa a entrada externa de um Produto construído com valores validados.

Da entrada externa ao novo registro

O retorno só existe após todas as verificações

A função validadora tem dois desfechos: ela interrompe o processamento com ValueError ao encontrar uma entrada inválida ou retorna um Produto construído no fim. Não devolva o dicionário recebido: monte um novo registro com nome, quantidade e uma nova lista de etiquetas já validadas.

Separação entre entrada e saída

A saída é criada a partir de componentes já estreitados, e não por uma afirmação sobre o contêiner externo.

Diagrama mostrando um dicionário de entrada não confiável passando por verificações e gerando um novo registro de produto com uma lista independente.

O novo dicionário e a nova lista formam uma fronteira: campos extras e valores não verificados não atravessam.

Implementação integrada

produto.py

python
import json
from typing import TypedDict


class Produto(TypedDict):
    nome: str
    quantidade: int
    etiquetas: list[str]


def validar_produto(entrada: object) -> Produto:
    if not isinstance(entrada, dict):
        raise ValueError("raiz: esperado um objeto JSON")

    for campo in ("nome", "quantidade", "etiquetas"):
        if campo not in entrada:
            raise ValueError(f"{campo}: campo obrigatório ausente")

    nome_bruto: object = entrada["nome"]
    quantidade_bruta: object = entrada["quantidade"]
    etiquetas_brutas: object = entrada["etiquetas"]

    if not isinstance(nome_bruto, str):
        raise ValueError("nome: esperado str")
    nome = nome_bruto.strip()
    if not nome:
        raise ValueError("nome: não pode ficar vazio")

    if isinstance(quantidade_bruta, bool) or not isinstance(quantidade_bruta, int):
        raise ValueError("quantidade: esperado int que não seja bool")
    if quantidade_bruta < 0:
        raise ValueError("quantidade: não pode ser negativa")

    if not isinstance(etiquetas_brutas, list):
        raise ValueError("etiquetas: esperado list")
    etiquetas_validadas: list[str] = []
    for indice, etiqueta_bruta in enumerate(etiquetas_brutas):
        etiqueta: object = etiqueta_bruta
        if not isinstance(etiqueta, str):
            raise ValueError(f"etiquetas[{indice}]: esperado str")
        etiquetas_validadas.append(etiqueta)

    produto: Produto = {
        "nome": nome,
        "quantidade": quantidade_bruta,
        "etiquetas": etiquetas_validadas,
    }
    return produto


texto_json = '{"nome": "  Caderno  ", "quantidade": 0, "etiquetas": ["papel"]}'
dado_externo: object = json.loads(texto_json)
produto = validar_produto(dado_externo)

print(produto)
print(produto["nome"].upper())

Por que a construção é segura para o verificador?

Cada valor já tem o tipo necessário

No literal atribuído a produto: Produto, nome só chegou ali depois de ser confirmado como str e normalizado. quantidade_bruta só continua após a verificação de int e a exclusão de bool. Já etiquetas_validadas é uma list[str] nova, preenchida somente depois da inspeção de cada elemento.

A anotação no ponto de construção permite que o mypy confira as chaves e os tipos dos valores. Isso é diferente de usar cast(Produto, entrada), que apenas afirmaria um contrato sem criar nem verificar nada.

Dica

Mutável não significa desprotegido

O consumidor pode usar produto["nome"] como str sem repetir a validação. Porém, Produto ainda é um dicionário comum: código posterior pode alterá-lo. A garantia obtida vale para o valor construído e retornado naquele momento.

Verifique a fronteira construída

Complete o campo do retorno

No literal de produto: Produto, complete o valor para preservar a separação da lista externa:

"etiquetas": ____

Explique a garantia do retorno

Por que a função retorna um Produto confiável para o código consumidor sem copiar o dicionário bruto nem usar cast?

Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8

Combinar mypy com testes de rejeição

Use análise estática e testes de execução como evidências complementares para uma fronteira que transforma dados externos em Produto.

Duas evidências, duas perguntas

O que cada ferramenta verifica

A fronteira já retorna Produto apenas depois de validar a entrada. Agora, verifique-a de duas formas:

  • mypy --strict analisa o código: compatibilidade entre argumentos, campos do TypedDict e retorno declarado.
  • pytest executa exemplos: confirma que dados externos inválidos geram ValueError e que limites válidos são aceitos.

Uma anotação int não expressa que o número deve ser não negativo. Por isso, mypy não rejeita uma quantidade negativa apenas pelo seu valor; esse comportamento precisa de testes em tempo de execução.

Cobertura complementar

Observe onde cada evidência atua no fluxo de entrada.

Diagrama mostrando JSON passando por desserialização, validação estrutural e validação semântica até Produto. Mypy aponta para o código anotado e pytest aponta para entradas válidas e inválidas executadas.

mypy examina a consistência dos contratos no código; pytest exercita valores reais que atravessam a fronteira.

Associe a evidência ao que ela pode revelar

Faça as associações mais adequadas.

Toque em um item e depois no par correspondente.

Módulo e consumidor sob análise estática

Crie os arquivos locais

Em uma pasta vazia, crie produto.py com a fronteira e consumidor.py com um uso válido. O parâmetro da fronteira permanece object: a desserialização não justifica afirmar uma estrutura antes da validação.

produto.py

python
from typing import TypedDict


class Produto(TypedDict):
    nome: str
    quantidade: int
    etiquetas: list[str]


def validar_produto(entrada: object) -> Produto:
    if not isinstance(entrada, dict):
        raise ValueError("raiz: esperado um objeto JSON")

    for campo in ("nome", "quantidade", "etiquetas"):
        if campo not in entrada:
            raise ValueError(f"{campo}: campo obrigatório ausente")

    nome_bruto: object = entrada["nome"]
    quantidade_bruta: object = entrada["quantidade"]
    etiquetas_brutas: object = entrada["etiquetas"]

    if not isinstance(nome_bruto, str):
        raise ValueError("nome: esperado str")
    nome = nome_bruto.strip()
    if not nome:
        raise ValueError("nome: não pode ficar vazio")

    if isinstance(quantidade_bruta, bool) or not isinstance(quantidade_bruta, int):
        raise ValueError("quantidade: esperado int, sem bool")
    if quantidade_bruta < 0:
        raise ValueError("quantidade: não pode ser negativa")

    if not isinstance(etiquetas_brutas, list):
        raise ValueError("etiquetas: esperado list")
    etiquetas: list[str] = []
    for indice, etiqueta_bruta in enumerate(etiquetas_brutas):
        if not isinstance(etiqueta_bruta, str):
            raise ValueError(f"etiquetas[{indice}]: esperado str")
        etiquetas.append(etiqueta_bruta)

    return {
        "nome": nome,
        "quantidade": quantidade_bruta,
        "etiquetas": etiquetas,
    }

consumidor.py

python
import json

from produto import Produto, validar_produto


def carregar(texto: str) -> Produto:
    externo: object = json.loads(texto)
    return validar_produto(externo)


produto = carregar('{"nome": " Caneta ", "quantidade": 0, "etiquetas": []}')
print(produto["nome"].upper())
print(produto["quantidade"] + 1)

Dica

Comando estático

Com mypy instalado no ambiente, execute:

python -m mypy --strict produto.py consumidor.py

O resultado esperado é sucesso sem erros. Isso sustenta que o consumidor recebe um Produto compatível; não demonstra que toda entrada externa será válida.

Teste os limites da fronteira

Casos que devem falhar e casos que devem passar

Crie test_produto.py. Os testes abaixo cobrem falhas estruturais, erros de tipo e regras semânticas. Também verificam dois limites aceitos: quantidade zero e lista vazia de etiquetas.

test_produto.py

python
import json

import pytest

from produto import validar_produto


@pytest.mark.parametrize(
    "entrada",
    [
        [],
        {"quantidade": 1, "etiquetas": []},
        {"nome": None, "quantidade": 1, "etiquetas": []},
        {"nome": "A", "quantidade": "1", "etiquetas": []},
        {"nome": "A", "quantidade": 1, "etiquetas": ["nova", 2]},
    ],
)
def test_rejeita_estrutura_ou_tipos_invalidos(entrada: object) -> None:
    with pytest.raises(ValueError):
        validar_produto(entrada)


@pytest.mark.parametrize(
    "entrada",
    [
        {"nome": "   ", "quantidade": 1, "etiquetas": []},
        {"nome": "A", "quantidade": -1, "etiquetas": []},
        {"nome": "A", "quantidade": True, "etiquetas": []},
    ],
)
def test_rejeita_regras_semanticas(entrada: object) -> None:
    with pytest.raises(ValueError):
        validar_produto(entrada)


def test_aceita_limites_e_normaliza_nome() -> None:
    produto = validar_produto(
        {"nome": " Caneta ", "quantidade": 0, "etiquetas": []}
    )

    assert produto == {
        "nome": "Caneta",
        "quantidade": 0,
        "etiquetas": [],
    }


def test_json_invalido_falha_antes_da_validacao() -> None:
    with pytest.raises(json.JSONDecodeError):
        json.loads('{"nome": "Caneta",}')

Atenção

Duas origens de falha

json.loads rejeita texto que não é JSON sintaticamente válido, como uma vírgula final indevida. Já validar_produto recebe um objeto desserializado e rejeita dados que violam o contrato, como {"quantidade": -1} ou uma raiz em lista.

Não capture JSONDecodeError como se fosse uma falha de regra do Produto: são etapas diferentes.

Dica

Execute os testes

Na mesma pasta, execute:

python -m pytest -q

O resultado esperado é que todos os testes passem. Uma suíte verde é evidência para os casos escolhidos, não uma prova de ausência de defeitos fora deles.

Interprete o resultado

Relate suas evidências

Depois de executar os comandos no seu computador, relate o resultado de mypy e de pytest. Inclua um caso semanticamente inválido que mypy não rejeitaria apenas por analisar o tipo.

Escreva pelo menos 80 caracteres (0/80).

Resumo

Evidências complementares

  • Use mypy em modo estrito para verificar se o módulo, o TypedDict e seus consumidores respeitam os contratos estáticos.
  • Use pytest para executar entradas externas inválidas e confirmar falhas previsíveis com ValueError.
  • Teste separadamente estrutura, tipos, elementos internos e regras semânticas.
  • JSON malformado falha na desserialização; JSON bem-formado pode falhar depois, na validação do Produto.
  • Mypy não demonstra regras baseadas em valores, e testes aprovados cobrem somente os casos executados.

Passo 8 de 8

Aplicar e revisar a fronteira completa

Aplique a validação de um Produto, verifique-a localmente e consolide as garantias obtidas na fronteira de entrada.

O fluxo que você concluiu

Da entrada ao contrato

A fronteira recebe dados que podem ter qualquer forma. O fluxo completo é: desserializar o JSON, guardar o resultado como object, verificar a estrutura, estreitar cada campo, aplicar regras semânticas e criar um novo Produto.

O consumidor recebe somente esse novo registro quando todas as etapas terminam sem erro. Caso contrário, a função encerra com ValueError previsível.

Camadas da fronteira

Cada camada elimina uma classe diferente de entrada inadequada antes da construção do retorno.

Diagrama em camadas mostrando dados externos passando por desserialização, contenção como object, validação estrutural, validação de tipos, regras semânticas e construção de Produto.

O contrato de saída só existe depois de estrutura, tipos e regras terem sido verificados.

Desafio local: complete a regra ausente

Sua tarefa

No seu computador, crie um arquivo chamado produto.py com o código abaixo. Complete o trecho marcado por TODO: depois de normalizar o nome, rejeite um nome vazio com ValueError, sem mudar o TypedDict nem usar cast.

Depois, crie test_produto.py com os testes apresentados. Eles incluem uma entrada válida e uma entrada estruturalmente válida, mas semanticamente inválida.

produto.py

python
import json
from typing import TypedDict


class Produto(TypedDict):
    nome: str
    quantidade: int
    etiquetas: list[str]


def validar_produto(entrada: object) -> Produto:
    if not isinstance(entrada, dict):
        raise ValueError("raiz: esperado um objeto")

    registro: dict[object, object] = entrada
    for campo in ("nome", "quantidade", "etiquetas"):
        if campo not in registro:
            raise ValueError(f"{campo}: campo obrigatório ausente")

    nome_bruto: object = registro["nome"]
    quantidade_bruta: object = registro["quantidade"]
    etiquetas_brutas: object = registro["etiquetas"]

    if not isinstance(nome_bruto, str):
        raise ValueError("nome: esperado str")
    if not isinstance(quantidade_bruta, int) or isinstance(quantidade_bruta, bool):
        raise ValueError("quantidade: esperado int, exceto bool")
    if not isinstance(etiquetas_brutas, list):
        raise ValueError("etiquetas: esperado list")

    etiquetas: list[str] = []
    for indice, etiqueta_bruta in enumerate(etiquetas_brutas):
        etiqueta: object = etiqueta_bruta
        if not isinstance(etiqueta, str):
            raise ValueError(f"etiquetas[{indice}]: esperado str")
        etiquetas.append(etiqueta)

    nome = nome_bruto.strip()
    # TODO: rejeite nome == "" com ValueError.

    if quantidade_bruta < 0:
        raise ValueError("quantidade: deve ser não negativa")

    return {
        "nome": nome,
        "quantidade": quantidade_bruta,
        "etiquetas": etiquetas,
    }


def carregar_produto(texto: str) -> Produto:
    bruto: object = json.loads(texto)
    return validar_produto(bruto)

test_produto.py

python
import pytest

from produto import carregar_produto


def test_aceita_e_normaliza_um_produto_valido() -> None:
    produto = carregar_produto(
        '{"nome": "  Café  ", "quantidade": 0, "etiquetas": []}'
    )

    assert produto == {
        "nome": "Café",
        "quantidade": 0,
        "etiquetas": [],
    }


def test_rejeita_nome_vazio_apos_normalizacao() -> None:
    with pytest.raises(ValueError, match="nome"):
        carregar_produto(
            '{"nome": "   ", "quantidade": 2, "etiquetas": ["bebida"]}'
        )

Evidências da correção

Execute e observe

Com o ambiente que contém mypy e pytest, execute:

python -m mypy --strict produto.py test_produto.py

python -m pytest -q

O mypy deve confirmar a consistência das anotações analisadas. O pytest deve aprovar o caso válido — inclusive quantidade zero e etiquetas vazias — e confirmar que o nome formado apenas por espaços é rejeitado.

Relate sua verificação

Relate: (1) a condição que você adicionou; (2) o resultado da entrada válida; (3) a rejeição observada para o nome vazio; e (4) o resultado de mypy e pytest. Por que trocar essa condição por cast(Produto, ...) não preservaria o mesmo comportamento?

Escreva pelo menos 180 caracteres (0/180).

Checklist final da fronteira

Resumo

O que a fronteira garante

Use este checklist ao receber dados externos para um registro tipado.

  • A entrada desserializada foi contida como object, sem afirmar antecipadamente sua estrutura.
  • A raiz, as chaves obrigatórias e os tipos dos campos foram verificados antes do uso específico de cada valor.
  • As regras do domínio foram aplicadas após o estreitamento: nome não vazio, quantidade não negativa e bool rejeitado como quantidade.
  • O retorno é um novo Produto construído com nome normalizado e uma nova lista de etiquetas verificadas; campos extras não são transferidos.
  • cast apenas altera a visão estática do verificador: não inspeciona, não converte e não rejeita dados inválidos.
  • mypy oferece evidência sobre compatibilidade estática; testes oferecem evidência sobre casos executados. Nenhum dos dois, isoladamente, prova todas as regras e todos os comportamentos possíveis.

Fronteira concluída

Parabéns! Você concluiu: Converter dados externos em valores validados e tipados

Muito bem! Você pode agora projetar uma fronteira explícita: dados sem garantias entram como object, falhas previsíveis interrompem o fluxo e somente componentes verificados formam o retorno tipado.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso