Voltar ao Blog Gestão Empresarial

IRRF: por que ROUND_HALF_UP é a única forma certa de calcular em Python

Banker's rounding do Python round() erra cálculo de IRRF. Veja por que ROUND_HALF_UP do Decimal é obrigatório, com 414 linhas de testes do Odoo brasileiro.

Luis Felipe Miléo

Luis Felipe Miléo

· 5 min de leitura

A função round() do Python tem um comportamento que pega muita gente de surpresa: round(0.5) == 0 e round(1.5) == 2. Não é bug. É banker’s rounding (arredondamento bancário, ou ROUND_HALF_EVEN), o padrão IEEE 754. Em folha de pagamento brasileira, isso está errado — e o erro custa centavos por colaborador, milhares de reais por ano em folhas grandes, e infinitos problemas de auditoria. Este post explica por que a folha CLT da KMEE no Odoo usa decimal.ROUND_HALF_UP em todos os cálculos de IRRF, com 414 linhas de testes provando o comportamento correto.

O bug clássico

>>> round(2.675, 2)
2.67

Espera, o quê? Cinco arredonda para cima. Resposta deveria ser 2.68.

São dois problemas combinados:

  1. float é binário. 2.675 em IEEE 754 não é exatamente 2.675. É 2.67499999999999982236431605997495353221893310546875. Quando o Python arredonda, o número já não é mais 2.675.
  2. round() usa banker’s rounding. Mesmo com tipos exatos, round(0.5) arredonda para 0 (vizinho par mais próximo), round(1.5) arredonda para 2 (vizinho par mais próximo). Em uma distribuição grande de números, isso evita viés estatístico — útil em estatística, contraintuitivo em folha.

A folha brasileira segue regra explícita: 5 sempre arredonda para cima. Quem implementa folha e usa round() do Python entrega valor errado.

A regra do IRRF

O IRRF mensal de empregado CLT segue tabela progressiva semelhante ao INSS, com cinco faixas e parcela a deduzir:

Base de cálculo (2026)AlíquotaParcela a deduzir
até R$ 2.428,80isento
2.428,81 a 2.826,657,5%182,16
2.826,66 a 3.751,0515%394,16
3.751,06 a 4.664,6822,5%675,49
acima de 4.664,6827,5%908,73

A base de cálculo é o salário bruto menos INSS retido, menos R$ 189,59 por dependente, menos pensão alimentícia. O imposto é base × alíquota - parcela.

Esses números, multiplicados, geram dízimas. R$ 3.500 × 15% = R$ 525,00 — fácil. Mas R$ 3.124,33 × 15% = R$ 468,6495 — precisa arredondar. Para cima ou para baixo?

A IN RFB 2.060/2021 diz: arredondamento para a segunda casa decimal, com 5 arredondando para cima.

A solução com Decimal

from decimal import Decimal, ROUND_HALF_UP

DEDUCAO_DEPENDENTE = Decimal("189.59")

IRRF_FAIXAS_2026 = [
    (Decimal("2428.80"),  Decimal("0.000"), Decimal("0.00")),
    (Decimal("2826.65"),  Decimal("0.075"), Decimal("182.16")),
    (Decimal("3751.05"),  Decimal("0.15"),  Decimal("394.16")),
    (Decimal("4664.68"),  Decimal("0.225"), Decimal("675.49")),
    (Decimal("999999.99"), Decimal("0.275"), Decimal("908.73")),
]

def calc_irrf(salario_bruto: Decimal, inss: Decimal, dependentes: int = 0,
              pensao: Decimal = Decimal("0")) -> Decimal:
    base = salario_bruto - inss - (DEDUCAO_DEPENDENTE * dependentes) - pensao
    if base <= Decimal("0"):
        return Decimal("0.00")
    for limite, aliquota, deducao in IRRF_FAIXAS_2026:
        if base <= limite:
            imposto = (base * aliquota - deducao)
            if imposto < Decimal("0"):
                return Decimal("0.00")
            return imposto.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return Decimal("0.00")

Tudo Decimal. Tudo ROUND_HALF_UP. Sem float em momento algum.

Os 414 testes

O arquivo tests/test_calc_irrf.py no PR kmee/kmee-odoo-addons#277 tem 414 linhas. Algumas amostras:

def test_irrf_isento():
    assert calc_irrf(salario=Decimal("2000"), inss=Decimal("150")) == Decimal("0.00")

def test_irrf_limite_exato_faixa_1():
    # base 2428.80 -> isento
    assert calc_irrf(Decimal("2620.00"), Decimal("191.20")) == Decimal("0.00")

def test_irrf_um_centavo_acima_isencao():
    # base 2428.81 * 0.075 - 182.16 = 0.00
    assert calc_irrf(Decimal("2620.01"), Decimal("191.20")) == Decimal("0.00")

def test_irrf_arredondamento_meio_centavo_para_cima():
    # base que gera 0.005 deve arredondar para 0.01
    # exemplo: 525.005 -> 525.01 (nao 525.00 como round() faria)
    base = Decimal("525.005")
    assert base.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) == Decimal("525.01")

def test_irrf_diferenca_round_vs_decimal():
    # Reproducao do bug: round() retorna 525.00, Decimal+HALF_UP retorna 525.01
    assert round(525.005, 2) == 525.0  # Python comportamento padrao
    assert Decimal("525.005").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) == Decimal("525.01")

E mais quase 400 linhas cobrindo: dependentes (de zero a oito), pensão alimentícia, faixa por faixa, limites das faixas, salários acima de R$ 100k (regra continua válida), regressões de bugs reportados em produção, e testes contra valores oficiais da Receita Federal.

Impacto em folha real

Numa folha de 100 colaboradores com salários médios entre R$ 5.000 e R$ 15.000, o erro de round() Python aparece em cerca de 30-40% dos cálculos mensais (sempre que a terceira casa decimal é exatamente 5). A diferença média é de R$ 0,01 por cálculo. Multiplicando: 35 colaboradores × R$ 0,01 × 12 meses = R$ 4,20 por ano de imposto a menor (ou maior). Parece pouco — até a auditoria da Receita pedir os memoriais de cálculo e perceber que o sistema não bate com a IN.

Em folhas de 1.000+ colaboradores, o problema escala. E em órgãos públicos, onde o memorial de cálculo é auditado a cada folha, o erro vira processo administrativo.

Conclusão

A folha CLT do Odoo da KMEE, doada à OCA via PR #277, usa exclusivamente decimal.Decimal e ROUND_HALF_UP. Os 414 asserts do test_calc_irrf.py rodam em segundos e blindam contra regressões. Se você está implementando ou auditando folha em Python, a regra é simples: float e round() ficam fora de qualquer cálculo monetário. Decimal com ROUND_HALF_UP é a única forma certa.

Veja mais sobre folha de pagamento Odoo ou compare Odoo vs sistemas proprietários.

#folha #esocial

Compartilhar

Sobre o autor

Luis Felipe Miléo

Luis Felipe Miléo

Desenvolvedor Odoo · KMEE

Desenvolvedor especializado em localização fiscal e projetos open source no ecossistema Odoo/OCA, com foco em integrações para o mercado latino-americano.

Ver perfil no LinkedIn