Python para Notas Fiscais: automatize XML de NF-e sem planilha manual

Aprenda a usar Python para ler XML de NF-e, extrair dados fiscais, validar totais, gerar CSV e montar uma automação segura para financeiro, compras e contabilidade.

9 min de leitura Equipe Python Brasil

Toda empresa brasileira que compra, vende, importa, presta serviço ou controla estoque convive com notas fiscais. Mesmo quando o ERP existe, ainda é comum alguém baixar XML de NF-e, abrir planilhas, copiar CNPJ, conferir valor total, separar CFOP, enviar arquivo para contabilidade e responder perguntas do financeiro. Esse trabalho repetitivo é um ótimo caso de uso para Python porque mistura leitura de arquivos, validação de dados, relatórios e integração entre áreas.

Este guia mostra como criar uma automação prática para ler XML de notas fiscais eletrônicas, extrair campos importantes, validar totais básicos e gerar uma planilha CSV para conferência. O objetivo não é substituir o contador, o ERP ou a legislação fiscal. O objetivo é reduzir retrabalho, evitar erro manual e criar um projeto de portfólio com valor real para financeiro, compras, operações, backoffice e contabilidade.

Se você está começando em automação, veja também Python para automação de planilhas, Python e Excel com openpyxl, ETL com Python em 2026 e projetos de portfólio Python. Para integrações mais robustas, combine este fluxo com mensageria usando Kafka ou RabbitMQ e observabilidade com OpenTelemetry.

Por que automatizar XML de NF-e com Python

O XML da NF-e é um documento estruturado. Isso significa que ele já vem com campos previsíveis: emitente, destinatário, itens, impostos, totais, chave de acesso, data de emissão e natureza da operação. Quando alguém transforma isso manualmente em planilha, está repetindo um trabalho que o computador faz melhor.

Casos comuns de automação:

  • consolidar todas as notas recebidas em um mês;
  • conferir se o valor total da nota bate com o pedido de compra;
  • separar notas por fornecedor, CNPJ, CFOP ou centro de custo;
  • localizar notas sem XML arquivado;
  • preparar arquivo de apoio para conciliação financeira;
  • identificar produtos mais comprados por período;
  • gerar relatórios simples para compras, estoque e contabilidade;
  • criar alertas quando uma nota vier de fornecedor desconhecido.

Em empresas pequenas, isso pode economizar horas por semana. Em empresas maiores, pode virar uma camada de validação antes dos dados entrarem no ERP, data warehouse ou dashboard.

O que você deve e não deve automatizar

Python ajuda a ler, organizar e conferir dados. Mas nota fiscal envolve regras tributárias, prazos, créditos, obrigações acessórias e decisões contábeis. Não trate uma automação caseira como parecer fiscal.

Use Python para:

  • extrair campos do XML;
  • validar estrutura mínima;
  • comparar valores informados;
  • detectar duplicidade por chave de acesso;
  • preparar relatórios;
  • registrar evidências de conferência;
  • integrar sistemas internos.

Evite automatizar sozinho:

  • decisão de crédito tributário;
  • interpretação de enquadramento fiscal;
  • alteração de dados oficiais;
  • envio para órgãos públicos sem entender certificados e regras;
  • cálculo fiscal complexo sem validação contábil.

A automação mais segura é aquela que organiza o trabalho e deixa decisões sensíveis para pessoas responsáveis.

Estrutura do projeto

Um projeto simples pode começar assim:

nfe-python/
  pyproject.toml
  README.md
  data/
    entrada/
      nota-exemplo.xml
    saida/
  src/
    nfe_parser.py
    relatorio.py
  tests/
    test_nfe_parser.py

Para rodar localmente:

python -m venv .venv
source .venv/bin/activate
pip install pytest

A biblioteca padrão do Python já possui xml.etree.ElementTree, suficiente para muitos casos de leitura. Em projetos maiores, você pode usar lxml para recursos avançados, validação por schema e melhor performance. Começar com a biblioteca padrão reduz dependências e facilita demonstrar o raciocínio.

Lendo um XML de NF-e

O XML da NF-e usa namespaces. Se você tentar buscar tags sem considerar isso, muitas consultas retornam vazio. Uma função pequena resolve boa parte do problema:

from pathlib import Path
import xml.etree.ElementTree as ET

NAMESPACE_NFE = {"nfe": "http://www.portalfiscal.inf.br/nfe"}


def texto_no(elemento: ET.Element, caminho: str, padrao: str = "") -> str:
    encontrado = elemento.find(caminho, NAMESPACE_NFE)
    if encontrado is None or encontrado.text is None:
        return padrao
    return encontrado.text.strip()


def carregar_xml(caminho: str | Path) -> ET.Element:
    arvore = ET.parse(caminho)
    return arvore.getroot()

Agora podemos extrair dados principais da nota. Em muitos arquivos, as informações ficam dentro de NFe/infNFe. Em alguns casos, o XML vem envelopado como nfeProc, então é melhor procurar infNFe em qualquer nível.

def encontrar_infnfe(raiz: ET.Element) -> ET.Element:
    infnfe = raiz.find(".//nfe:infNFe", NAMESPACE_NFE)
    if infnfe is None:
        raise ValueError("XML não parece conter uma NF-e válida: infNFe ausente")
    return infnfe


def extrair_resumo_nfe(caminho: str | Path) -> dict[str, str]:
    raiz = carregar_xml(caminho)
    infnfe = encontrar_infnfe(raiz)

    ide = infnfe.find("nfe:ide", NAMESPACE_NFE)
    emit = infnfe.find("nfe:emit", NAMESPACE_NFE)
    dest = infnfe.find("nfe:dest", NAMESPACE_NFE)
    total = infnfe.find("nfe:total/nfe:ICMSTot", NAMESPACE_NFE)

    if ide is None or emit is None or total is None:
        raise ValueError("XML sem blocos obrigatórios para o resumo")

    chave = infnfe.attrib.get("Id", "").replace("NFe", "")

    return {
        "arquivo": str(caminho),
        "chave": chave,
        "numero": texto_no(ide, "nfe:nNF"),
        "serie": texto_no(ide, "nfe:serie"),
        "data_emissao": texto_no(ide, "nfe:dhEmi") or texto_no(ide, "nfe:dEmi"),
        "cnpj_emitente": texto_no(emit, "nfe:CNPJ"),
        "emitente": texto_no(emit, "nfe:xNome"),
        "cnpj_destinatario": texto_no(dest, "nfe:CNPJ") if dest is not None else "",
        "destinatario": texto_no(dest, "nfe:xNome") if dest is not None else "",
        "valor_produtos": texto_no(total, "nfe:vProd", "0.00"),
        "valor_nota": texto_no(total, "nfe:vNF", "0.00"),
    }

Esse código já resolve uma dor real: transformar dezenas ou centenas de XMLs em linhas estruturadas.

Extraindo itens da nota

Para compras, estoque e análise de fornecedores, o cabeçalho da nota não basta. Você provavelmente precisa dos itens: código, descrição, NCM, CFOP, quantidade e valor.

def extrair_itens_nfe(caminho: str | Path) -> list[dict[str, str]]:
    raiz = carregar_xml(caminho)
    infnfe = encontrar_infnfe(raiz)
    resumo = extrair_resumo_nfe(caminho)
    linhas = []

    for det in infnfe.findall("nfe:det", NAMESPACE_NFE):
        produto = det.find("nfe:prod", NAMESPACE_NFE)
        if produto is None:
            continue

        linhas.append({
            "chave": resumo["chave"],
            "numero": resumo["numero"],
            "emitente": resumo["emitente"],
            "codigo_produto": texto_no(produto, "nfe:cProd"),
            "descricao": texto_no(produto, "nfe:xProd"),
            "ncm": texto_no(produto, "nfe:NCM"),
            "cfop": texto_no(produto, "nfe:CFOP"),
            "unidade": texto_no(produto, "nfe:uCom"),
            "quantidade": texto_no(produto, "nfe:qCom", "0"),
            "valor_unitario": texto_no(produto, "nfe:vUnCom", "0.00"),
            "valor_total_item": texto_no(produto, "nfe:vProd", "0.00"),
        })

    return linhas

Com isso, você consegue responder perguntas como “quanto compramos deste fornecedor?”, “quais CFOPs apareceram este mês?” ou “qual item está com descrição diferente no cadastro?”.

Gerando CSV para o financeiro

CSV continua sendo útil porque abre no Excel, LibreOffice, Google Sheets, Power BI e ferramentas de BI. Também é simples de versionar em um pipeline.

import csv
from pathlib import Path


def gerar_csv_resumo(pasta_xml: str | Path, saida_csv: str | Path) -> None:
    pasta = Path(pasta_xml)
    arquivos = sorted(pasta.glob("*.xml"))
    linhas = [extrair_resumo_nfe(arquivo) for arquivo in arquivos]

    if not linhas:
        raise ValueError("Nenhum XML encontrado para processar")

    campos = list(linhas[0].keys())

    with Path(saida_csv).open("w", newline="", encoding="utf-8") as arquivo:
        escritor = csv.DictWriter(arquivo, fieldnames=campos)
        escritor.writeheader()
        escritor.writerows(linhas)

Uso:

gerar_csv_resumo("data/entrada", "data/saida/resumo-nfe.csv")

Para itens, basta trocar a função de extração e juntar listas:

def gerar_csv_itens(pasta_xml: str | Path, saida_csv: str | Path) -> None:
    linhas = []
    for arquivo in sorted(Path(pasta_xml).glob("*.xml")):
        linhas.extend(extrair_itens_nfe(arquivo))

    if not linhas:
        raise ValueError("Nenhum item encontrado nos XMLs")

    with Path(saida_csv).open("w", newline="", encoding="utf-8") as arquivo:
        escritor = csv.DictWriter(arquivo, fieldnames=list(linhas[0].keys()))
        escritor.writeheader()
        escritor.writerows(linhas)

Validações úteis para evitar erro manual

A automação começa a gerar valor quando não apenas copia dados, mas também aponta inconsistências. Algumas validações simples:

  • chave de acesso duplicada;
  • XML sem CNPJ do emitente;
  • nota com valor total zero;
  • fornecedor fora de uma lista aprovada;
  • data de emissão fora do período esperado;
  • diferença entre soma dos itens e total de produtos;
  • CFOP inesperado para uma rotina de compra.

Exemplo de validação de duplicidade:

def encontrar_chaves_duplicadas(resumos: list[dict[str, str]]) -> set[str]:
    vistas = set()
    duplicadas = set()

    for resumo in resumos:
        chave = resumo["chave"]
        if chave in vistas:
            duplicadas.add(chave)
        vistas.add(chave)

    return duplicadas

Exemplo de validação de valor:

from decimal import Decimal


def dinheiro(valor: str) -> Decimal:
    return Decimal(valor.replace(",", "."))


def validar_valor_nota(resumo: dict[str, str]) -> list[str]:
    erros = []
    valor_nota = dinheiro(resumo["valor_nota"])

    if valor_nota <= 0:
        erros.append(f"Nota {resumo['numero']} está com valor total inválido")

    if not resumo["cnpj_emitente"]:
        erros.append(f"Nota {resumo['numero']} não tem CNPJ de emitente")

    return erros

Use Decimal, não float, para valores financeiros. float é ótimo para muitas contas, mas pode gerar pequenas diferenças binárias que atrapalham conferências contábeis.

Como transformar em projeto de portfólio

Um projeto de NF-e chama atenção porque mostra que você entende problema de negócio, não apenas sintaxe. Para deixá-lo apresentável no GitHub, inclua:

  1. Dados fictícios: nunca publique XML real de cliente, fornecedor ou empresa.
  2. README com cenário: explique que o projeto ajuda financeiro/compras a consolidar XMLs.
  3. Comandos de uso: mostre como rodar a extração e onde o CSV aparece.
  4. Testes automatizados: valide XML mínimo, erro de arquivo inválido e duplicidade.
  5. Exemplo de saída: inclua um CSV pequeno com dados inventados.
  6. Limites claros: deixe explícito que a automação não substitui análise fiscal.

Um teste simples com pytest:

from src.nfe_parser import extrair_resumo_nfe


def test_extrai_numero_e_emitente_do_xml_exemplo():
    resumo = extrair_resumo_nfe("data/entrada/nota-exemplo.xml")

    assert resumo["numero"] == "12345"
    assert resumo["emitente"] == "Fornecedor Exemplo Ltda"
    assert resumo["valor_nota"] == "199.90"

Esse tipo de teste demonstra cuidado profissional. Em uma vaga de dados, backoffice, RevOps ou automação, isso comunica maturidade melhor do que um script solto.

Evoluindo para produção

Quando a automação sair do computador de uma pessoa e virar rotina da empresa, pense em arquitetura:

  • salvar histórico em PostgreSQL;
  • mover arquivos processados para uma pasta de arquivamento;
  • registrar logs com quantidade de notas, erros e tempo de execução;
  • enviar alerta quando uma validação falhar;
  • rodar por agendamento no cron, Airflow ou GitHub Actions interno;
  • usar fila para processar muitos XMLs;
  • expor uma API FastAPI para upload controlado;
  • integrar com ERP, data lake ou ferramenta de BI.

Também vale cuidar de LGPD e segurança. XMLs podem conter CNPJ, endereço, produtos, valores e relações comerciais. Controle acesso, evite anexar dados sensíveis em tickets públicos e não misture XML real em repositório de estudo.

Para times que usam outras linguagens no backend, Python pode ficar responsável pela camada de extração e validação, enquanto serviços em Go cuidam de APIs de alta concorrência ou workers de longa duração. O importante é escolher a ferramenta pelo problema, não por moda.

Checklist de uma boa automação de NF-e

Antes de considerar o projeto pronto, confira:

  • processa múltiplos XMLs de uma pasta;
  • ignora ou reporta arquivos inválidos sem derrubar tudo;
  • gera CSV de resumo;
  • gera CSV de itens;
  • detecta chave duplicada;
  • usa Decimal para valores;
  • tem testes com XML fictício;
  • documenta limites fiscais e contábeis;
  • não expõe dados reais;
  • explica como evoluir para banco, API ou dashboard.

Conclusão

Automatizar XML de NF-e com Python é um projeto pequeno o bastante para começar em poucos dias e útil o bastante para gerar impacto real. Ele conecta programação com processos de negócio: financeiro, compras, estoque, contabilidade e operações. Essa combinação é valiosa no mercado brasileiro porque muitas empresas ainda dependem de planilhas manuais para tarefas críticas.

Comece lendo uma pasta de XMLs, gere um CSV confiável, adicione validações e escreva testes. Depois, evolua para banco de dados, dashboard, API ou integração com ERP. O diferencial não está em escrever o código mais sofisticado, mas em entregar uma automação que reduz erro, economiza tempo e respeita os limites de segurança e responsabilidade fiscal.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português