Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Decodificação Base64 em Python: um guia completo

Em algum lugar do seu código, acabou de cair uma string de letras que não parece texto nenhum: uma longa sequência de A até Z, alguns dígitos, o ocasional + ou /, talvez um - ou _, e quem sabe um ou dois sinais de = estacionados no final. Atrás dessa string pode estar o payload de um JWT que o seu gateway rejeitou, uma imagem escondida dentro de uma página de HTML, um arquivo que alguém te enviou por e-mail como um anexo .b64, ou um bloco de certificado em um chamado que já passou por três mesas de suporte. O seu trabalho é devolver os bytes originais, exatamente como eram. E o Python está de ótimo humor para esse trabalho, porque a caixa de ferramentas inteira embarca na biblioteca padrão há décadas: uma linha de import base64 e você está pronto em qualquer plataforma, sem instalar nada e sem configurar nada.

Uma breve revisão enquanto você se instala, porque todo mundo precisa disso uma vez por ano: o Base64 reescreve cada grupo de três bytes de dados como quatro caracteres tirados de um alfabeto de 64 caracteres, e quando o grupo final de três bytes fica incompleto, o padding de = completa o grupo para que a saída sempre venha em grupos de quatro. Esse é o truque inteiro. Não é compressão e não é sigilo, apenas uma forma de deixar o binário sobreviver a canais que aceitam só texto. A página inicial deste site percorre o formato em profundidade completa, incluindo o alfabeto e a matemática do padding, então vamos gastar a nossa energia onde a dor realmente mora: no lado Python da decodificação, e em manter o resultado honesto.

Três fatos moldam tudo que vem a seguir, e valem a pena memorizar antes de você ler mais uma linha. Primeiro, o decodificador tem dois humores: um padrão educado e permissivo que descarta em silêncio qualquer coisa que não reconhece, e um modo estrito que rejeita essa entrada de cara. Segundo, o resultado de uma decodificação é sempre um objeto bytes, nunca uma string, e o momento em que você quer texto de verdade saindo dele é uma decisão que você tem que tomar de propósito. Terceiro, existem dois alfabetos que parecem quase idênticos, o padrão e o URL-safe, e confundir os dois é um jeito favorito de perder dados sem nenhum erro sequer. Este guia te conduz por cima dos três, para que da próxima vez que uma parede de caracteres sem sentido cair no seu terminal, você esteja sorrindo em vez de franzindo o cenho.

O cardápio completo de decodificação

Abrir o módulo base64 e você encontra duas gerações de interface sentadas lado a lado. A moderna, centrada no b64decode, converte objetos bytes-like (e strings ASCII simples) de volta em bytes, e ela fala os dois dialetos de Base64 definidos na RFC 4648. A legada é mais antiga e orientada a arquivos: ela trabalha com objetos de arquivo, conhece apenas o alfabeto padrão, e foi construída em torno das linhas envoltas de 76 caracteres que a RFC 2045, o padrão de e-mail MIME de 1996, exigia da saída codificada. Você vai encontrar os nomes legados em bastante código que está por aí há um tempo, então aqui está o lado completo de decodificação do cardápio:

Função O que Ela Faz Notas
base64.b64decode(s, altchars=None, validate=False) o cavalo de batalha: um bloco de Base64 de volta a bytes crus aceita bytes ou uma string ASCII, sempre retorna bytes
base64.standard_b64decode(s) o mesmo trabalho, travado no alfabeto padrão útil quando você conhece o dialeto com certeza
base64.urlsafe_b64decode(s) lê o alfabeto URL-safe com - e _ a que lê JWTs
base64.decodebytes(s) decodifica uma ou mais linhas envoltas de Base64 adicionado no Python 3.1, o caminho amigável ao MIME, permissivo
base64.decode(input, output) faz o stream de um arquivo Base64 para um arquivo cru legado, lê linha por linha, permissivo
base64.b32decode(s, casefold=False) decodifica o primo menor, o Base32 casefold aceita entrada em minúsculas
base64.b16decode(s, casefold=False) decodifica o Base16, que é hexadecimal puro até seis vezes mais rápido no Python 3.14
binascii.a2b_base64(s, strict_mode=False) a função de nível C que faz o trabalho de verdade um controle direto sobre o rigor, com strict_mode desde o Python 3.11

Tudo abaixo se constrói sobre a primeira linha. Um fato vale a pena saber antes de você ir mais fundo: na documentação oficial o módulo mora sob "Tratamento de dados da Internet", logo ao lado do binascii, e esse posicionamento não é um acaso. O b64decode é um wrapper fino que traduz o alfabeto (quando você passa altchars) e depois deixa o binascii.a2b_base64 de nível C fazer o trabalho pesado. É por isso que a função é rápida, e é por isso que as mensagens de erro dela têm o sabor crocante e desapegado do C.

O cavalo de batalha: b64decode

Aqui está o contrato inteiro, curto o suficiente para caber na sua cabeça. A função recebe um objeto bytes-like ou uma string ASCII, uma troca opcional de alfabeto de dois caracteres, e uma flag de validação. Ela devolve um objeto bytes. Em caso de falha ela lança binascii.Error, que é uma subclasse de ValueError, caso você precise algum dia capturar uma família de exceções de uma vez:

import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>

Essa última linha é a linha única mais importante deste artigo. O resultado é bytes, não uma string, e o Python segura a sua mão exatamente até onde deveria: imprimir o objeto mostra a representação b'...' para você, e tentar colá-lo a uma string lança um TypeError. No momento em que você quer texto de verdade, a decisão é sua de tomar, e a seção de charset abaixo cobre quando essa decisão é fácil e quando é uma armadilha.

O argumento opcional altchars troca o + e o / do alfabeto padrão por um par diferente de caracteres. Esse é precisamente o botão que produz o dialeto URL-safe, e é assim que o urlsafe_b64decode é construído em cima do b64decode. Você raramente vai recorrer ao altchars sozinho, mas é bom saber que o mecanismo está ali. Para tudo o resto, a função simplesmente faz o trabalho, rápido, em C.

Permissivo por padrão, estrito sob pedido

Por padrão, o b64decode é um esquecediço educado. Qualquer caractere que não esteja no alfabeto de 64 caracteres (e não esteja no seu altchars) é jogado fora em silêncio antes da decodificação começar, e o que sobrevive é decodificado. Sem aviso, sem notificação, sem valor de retorno para checar, só um resultado. Essa tolerância tem um ancestral nobre: a seção 6.8 da RFC 2045 diz aos decodificadores que "todos os saltos de linha ou outros caracteres não encontrados na Tabela 1 devem ser ignorados", porque o SMTP historicamente envolvia linhas longas e espalhava caracteres soltos pelo caminho. Um payload que cruzou um cliente de e-mail, um app de chat, ou uma cópia de um PDF muitas vezes é decodificado sem nenhuma preparação, e isso é um superpoder de verdade.

A mesma gentileza é também a razão pela qual o decodificador padrão é inútil como validador. A seção 12 da RFC 4648 descreve o risco: ignorar caracteres fora do alfabeto em vez de rejeitar a codificação inteira abre um canal clandestino que pode ser usado para vazar informação, e pode quebrar verificações de igualdade de strings, porque duas entradas diferentes podem decodificar para os mesmos bytes. Para qualquer coisa que você não codificou você mesmo, passe validate=True e trate a exceção como a resposta. Aqui está o relatório de danos, toda linha reproduzível em qualquer Python moderno:

O que Entra Permissivo (padrão) validate=True
Zm9vYmFy (um payload limpo) b'foobar' b'foobar'
Zm9v\r\nYmFy (quebra de linha no meio) b'foobar' binascii.Error
Zm9v YmFy (espaços extras) b'foobar' binascii.Error
Zm9v!YmFy (um ponto de exclamação solto) b'foobar' binascii.Error
junkZm9vYmFy (uma palavra à frente do payload) b'\x8e\xe9\xe4foobar' b'\x8e\xe9\xe4foobar'
Zm9v=YmFy (um pad no meio) b'foobar' binascii.Error
=Zm9v (padding na frente) b'foo' binascii.Error
==== (quatro pads, sem dados) b'' binascii.Error
(entrada vazia) b'' b''

Assista à coluna permissiva fazer o seu trabalho em silêncio. A linha que surpreende as pessoas primeiro é a que tem uma palavra na frente: as quatro letras de junk por acaso ficam no alfabeto Base64, então o "lixo" é decodificado como três bytes de verdade e é colado ao seu payload com a cara lavada. O modo estrito não é nenhum salvador nesta linha, porque a entrada realmente é Base64 válido; são as outras linhas que são recusadas de chofre, e as recusas têm exatamente uma forma: um binascii.Error carregando uma de um punhado de mensagens memoráveis:

  • Incorrect padding - o comprimento não é um múltiplo de quatro após o descarte, ou um grupo final é curto demais. Uma string como Zm9vYmE sem nenhum pad termina aqui.
  • Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4 - a entrada tem exatamente um caractere a menos que o próximo grupo. Essa é a impressão digital clássica de um payload truncado ou copiado e colado.
  • Only base64 data is allowed - um caractere fora do alfabeto sobreviveu até o modo estrito, e um único salto de linha conta como um.
  • Excess padding not allowed - pads no meio da string, ou mais pads do que o grupo final permite.
  • Leading padding not allowed - a string começa com =.
  • E uma de uma família diferente: ValueError: string argument should contain only ASCII characters, que você recebe quando passa uma string com letras não-ASCII dentro. Strings são aceitas, mas só as ASCII.

Atrás das cortinas, validate=True não é um caminho de código separado nem um pouco. O módulo repassa a flag para o binascii.a2b_base64 como seu parâmetro strict_mode, a verificação estrita que foi adicionada ao binascii no Python 3.11. Isso te dá um controle direto quando você quer rigor sem passar pela camada base64:

import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'

Uma peculiaridade para fixar antes de você confiar no modo estrito às cegas: ele rejeita até um único salto de linha no final, então um bloco envolto em MIME é trabalho do caminho permissivo ou do decodebytes, não do validate=True. Guarde o caminho estrito para dados que você espera perfeitamente limpos, como um token recém-cunhado saindo direto do seu próprio código.

base64url, o alfabeto que cabe em URLs

O alfabeto padrão tem dois caracteres que URLs odeiam. O sinal + é lido como um espaço por qualquer decodificador de formulário, e o sinal / é reservado para separadores de caminho. A seção 5 da RFC 4648 define o dialeto primo, onde + vira - e / vira _, e o padding é descartado sempre que o comprimento dos dados é conhecido pelo contexto. A RFC até dá à variante um nome próprio, base64url, e insiste que ela não deveria ser chamada só de "base64". Você vai encontrá-la mais frequentemente dentro de JSON Web Tokens, onde cada parte do token é base64url sem padding, e ela também aparece em tokens OAuth e em parâmetros de cursor de API.

O Python embarca uma função dedicada para isso, o urlsafe_b64decode. Ele traduz os traços e sublinhados de volta em mais e barras e então decodifica, mas ele não vai re-fazer o padding para você. Entrada sem padding é o caso normal para JWTs, então a linha de aritmética vem primeiro, e é a mesma que bibliotecas como PyJWT usam por baixo dos panos:

import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'

A expressão "=" * (-len(segment) % 4) parece um truque, mas é o trabalho inteiro: ela produz zero, um, ou dois pads e nunca três, então uma string já com padding passa por ela intacta. O módulo negativo é o que faz ela funcionar para strings de qualquer comprimento, e é a única linha de aritmética de Base64 que todo desenvolvedor Python acaba digitando pelo menos uma vez.

Agora a confusão perigosa, porque os dois alfabetos parecem próximos o suficiente para confundir. Passe uma string base64url pelo decodificador padrão e os traços e sublinhados simplesmente não estão no alfabeto padrão, então o decodificador permissivo engole-os e decodifica o que restar. Para alguns payloads isso é um stream de bytes corrompido; para outros é nada absoluto:

import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - cada caractere foi silenciosamente descartado
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'

A direção reversa é permissiva, o que faz a confusão passar despercebida: o urlsafe_b64decode traduz o alfabeto dele primeiro e então decodifica de forma permissiva, então ele aceita de bom grado uma string do alfabeto padrão com + e / dentro. A lição não é improvisar. É escolher uma função por dialeto e ficar nela, do jeito que você faria com uma moeda estrangeira: gaste o iene onde o iene é válido, não na casa de câmbio errada.

A saída é bytes: a conversa sobre charset

Aqui está a frase que resolve metade das questões de charset que as pessoas trazem ao Base64: o b64decode decodifica bytes; ele não decodifica texto. Não há argumento de charset, não há conversão, e nada na entrada diz ao Python o que os bytes deveriam significar. O significado é algo que você deve fornecer a partir do contexto, e esse contexto quase sempre é um destes três: um cabeçalho que diz isso, um contrato de API que diz isso, ou um número mágico escondido nos próprios bytes.

import base64
raw = base64.b64decode("w6l0w6k=")
print(raw)
# b'\xc3\xa9t\xc3\xa9'
print(raw.decode("utf-8"))
# été

A mesma ideia com o rótulo errado é uma falha barulhenta, o que é um alívio. Bytes que não são UTF-8 válido se recusam a virar string, e a exceção te diz exatamente qual byte ofendeu:

import base64
raw = base64.b64decode("/w==")
try:
  raw.decode("utf-8")
except UnicodeDecodeError as caught:
  print(caught)
# 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

Três regras gerais mantêm esta seção de virar um show de horrores. Uma: quando os dados decodificados são JSON, você não precisa decodificar manualmente nem um pouco, porque json.loads aceita bytes diretamente desde o Python 3.6 e detecta UTF-8, UTF-16, e UTF-32 por conta própria. Duas: binário não é texto, então um charset "detectado" para um PNG é um chute de sorte em vez de um fato; confira os bytes em vez do rótulo. Três: se o remetente te disse o charset, acredite no remetente, porque um cabeçalho content-type ou um documento de API supera qualquer detector, todo santo dia.

Onde o Base64 decodificado aparece em código Python

Depois de um tempo você começa a reconhecer as formas. Aqui está o guia de campo para os lugares onde o Base64 decodificado aparece em um aplicativo Python, e a receita de uma linha para cada. As seções que seguem dão o tratamento completo aos mais comuns:

Onde Você Acha O Que É Como Ler
Um JWT partes de cabeçalho, payload e assinatura (RFC 7519) separe no ponto, urlsafe_b64decode com a correção de padding
Um cabeçalho Authorization credenciais HTTP Basic, user:pass (RFC 7617) remova o prefixo Basic, decodifique, separe no primeiro dois-pontos
Um URI data: mídia inline em HTML ou CSS (RFC 2397) corte na primeira vírgula, decodifique o resto
Um anexo de e-mail um corpo Content-Transfer-Encoding: base64 (RFC 2045) get_payload(decode=True) na parte da mensagem
Um valor de cabeçalho de e-mail uma palavra codificada =?charset?b?...?= (RFC 2047) deixe o pacote email decodificar para você
Um arquivo PEM uma chave ou certificado blindado (RFC 7468) descarte as linhas da blindagem, decodifique o corpo para DER
Um campo de API JSON binário contrabandeado como string decodifique, então trate o resultado como bytes, não texto
Uma coluna TEXT ou variável de ambiente binário ou JSON armazenado em um lugar só de texto decodifique, então parseie ou escreva, com o charset em que vocês combinaram

Lendo um JSON Web Token

Um JWT é três pedaços base64url unidos por pontos: um cabeçalho, um payload, e uma assinatura. Os dois primeiros são JSON puro, então dar uma espiada neles é uma linha cada, usando a correção de padding da seção acima:

import base64
import json
def read_part(segment):
  padded = segment + "=" * (-len(segment) % 4)
  return base64.urlsafe_b64decode(padded)
token = ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
         "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
         "8Rmup2hf8jZvoBgoCRqRWlBFNtvUYmA0eR7YKellPMs")
head, body, _signature = token.split(".")
print(json.loads(read_part(head)))
# {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(read_part(body)))
# {'sub': '1234567890', 'name': 'John Doe'}

Uma nota de escopo, porque isso importa: inspecionar um token deste jeito é uma ferramenta de depuração, não um mecanismo de autenticação. O payload ser legível não significa que ele seja autêntico; um atacante pode forjar os dois primeiros segmentos sem nunca saber o seu segredo. Para verificação de verdade, entregue o token ao PyJWT (pip install pyjwt), que verifica a assinatura e se recusa a decodificar sem uma lista explícita de algoritmos:

import jwt
# Uma chave de menos de 32 bytes provoca a InsecureKeyLengthWarning do PyJWT (PyJWT 2.11+), uma reclamação justa para uma chave de demonstração.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}

Com uma chave errada você recebe uma exceção em vez de um dicionário, que é exatamente o comportamento que você quer em código de produção. E se o token chegou com uma marca de tempo expirada, o PyJWT lança por isso também, então você nunca precisa lembrar os nomes das claims sozinho.

Abrindo um Data URI

Os Data URIs embutem mídia diretamente dentro de HTML ou CSS para que o navegador não dispare um segundo pedido: data:, o tipo de mídia, a palavra base64, uma vírgula, e os bytes codificados. O corte é na primeira vírgula, ponto final, e tudo depois dela é um payload comum do alfabeto padrão:

import base64
uri = ("data:image/png;base64,"
       "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"
       "AAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
mime, payload = uri.split(",", 1)
data = base64.b64decode(payload)
print(mime)
# data:image/png;base64
print(data[:8])
# b'\x89PNG\r\n\x1a\n'

A assinatura PNG de oito bytes na frente do resultado é uma checagem barata e agradável de que você decodificou a coisa certa. Duas armadilhas merecem menção. Se o URI veio de uma página raspada ou de uma mensagem de chat, remova entidades HTML e espaços em branco soltos primeiro, porque o decodificador permissivo perdoa bastante lixo e te entrega uma imagem corrompida em vez de um erro. E se você está decodificando entrada não confiável em massa, passe validate=True: um data URI que falha na validação estrita é um data URI que nunca foi bem-formado, e você não quer escrevê-lo em disco por um palpite.

Rachando o Cabeçalho Authorization

A autenticação Basic (RFC 7617) é o esquema mais antigo do HTTP, e ela ancora ainda uma quantidade surpreendente de integrações de API, webhooks, e pipelines de CI. O cliente envia as suas credenciais como user:pass, codificado em base64, atrás da palavra Basic:

import base64
header = "Basic amFuZTpwYTpzcw=="
decoded = base64.b64decode(header[len("Basic "):]).decode("utf-8")
user, _, password = decoded.partition(":")
print(user, password)
# jane pa:ss

Observe o partition, porque é o detalhe que te salva depois: a senha pode conter dois-pontos, o ID de usuário não pode, e só o primeiro dois-pontos é o separador. Uma nota honesta, porque a própria RFC é direta sobre isso: base64 não é criptografia. A RFC 4648 diz que a codificação base "oculta visualmente informação que de outro modo seria facilmente reconhecida, como senhas, mas não fornece qualquer confidencialidade computacional". Um cabeçalho Basic pode ser decodificado por qualquer um que veja o tráfego, então trate-o como uma conveniência para conexões protegidas por TLS, não como uma fronteira de segurança. Quando você é quem envia o cabeçalho, o requests o constrói para você com auth=("jane", "pa:ss"), o que vale a pena usar sempre que a biblioteca já está no seu stack.

E-mail, o cliente original

O Base64 foi padronizado em 1993 para exatamente um trabalho: fazer o binário sobreviver ao e-mail. A RFC 2045, o padrão MIME, definiu a codificação de corpo Content-Transfer-Encoding: base64, e ela ainda é a forma padrão de anexos viajarem pela internet. O pacote email do Python faz o trabalho inteiro para você: ele faz o parse dos cabeçalhos, ele decodifica as palavras codificadas =?utf-8?b?...?= que a RFC 2047 esconde em campos de cabeçalho, e ele decodifica corpos em base64 quando você pede:

import email
from email import policy
raw = (b"Subject: =?utf-8?b?w6l0w6k=?=\r\n"
       b"From: sender@example.com\r\n"
       b"To: reader@example.com\r\n"
       b"Content-Transfer-Encoding: base64\r\n"
       b"\r\n"
       b"w6l0w6kgbWFpbA==\r\n")
msg = email.message_from_bytes(raw, policy=policy.default)
print(msg["Subject"])
# été
print(msg.get_payload(decode=True))
# b'\xc3\xa9t\xc3\xa9 mail'

A chamada get_payload(decode=True) lê o cabeçalho Content-Transfer-Encoding e decodifica o corpo em base64 para você, desarmando as linhas de 76 caracteres pelo caminho. O argumento policy=policy.default seleciona a interface moderna a partir do Python 3.6, quando a nova API de e-mail baseada em policy deixou de ser provisória, o que te dá valores de cabeçalho decodificados direto da caixa; o parser legado ainda funciona, mas você acaba decodificando as palavras codificadas à mão. Você só desce até o decodebytes quando está fazendo o parse de um trecho solto que não é uma mensagem completa, como um bloco que alguém colou em um chamado. Para mensagens multipart, itere com iter_attachments() e dê a cada parte o mesmo tratamento de uma linha.

Blindagem PEM e o pacote cryptography

Um arquivo PEM é uma linha de cabeçalho, algum Base64 quebrado, e uma linha de rodapé, e nada mais. A blindagem é decorativa; o Base64 é a história inteira, porque ele decodifica para a estrutura DER crua embaixo. O pacote cryptography (pip install cryptography) pode carregar o resultado diretamente, e é por isso que ele é a ferramenta padrão para qualquer coisa envolvendo certificados e chaves:

import base64
from cryptography import x509
pem = b"""-----BEGIN CERTIFICATE-----
MIIBGzCBwaADAgECAgEBMAoGCCqGSM49BAMCMBcxFTATBgNVBAMMDGV4YW1wbGUu
dGVzdDAeFw0yNjA4MjkxNzIxMzZaFw0yNjA4MzAxNzIxMzZaMBcxFTATBgNVBAMM
DGV4YW1wbGUudGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPvNHjdF4b1n
SkBDT6UWtG2k8ICe45eL3kSkVfuhriev1uO9PBLMP50HWnrLbCXtl3lhWaVibctl
QbWRG4xqGLcwCgYIKoZIzj0EAwIDSQAwRgIhAKdFm5GLecg2fF7qUhSmKGtgNFaL
qVyKtDXK07N6GZd/AiEAtRXemnYqDMz77o9+VpM/NsNEwDi0yaVB+tKGLbdKJb0=
-----END CERTIFICATE-----
"""
body = b"".join(pem.splitlines()[1:-1])
der = base64.b64decode(body)
cert = x509.load_der_x509_certificate(der)
print(cert.subject.rfc4514_string())
# CN=example.test

Na maioria do código de produção você nunca faz a blindagem-e-decodificação à mão: o load_pem_x509_certificate aceita os bytes blindados e cuida da etapa de Base64 por baixo dos panos. O caminho manual se paga quando os bytes DER já estão nas suas mãos (uma coluna de banco de dados, um arquivo de configuração, um buffer de bytes de um protocolo), ou quando o bloco chegou embrulhado em uma string e você quer ver o que tem dentro antes de confiar nele. Chaves funcionam do mesmo jeito, com o load_der_private_key esperando do outro lado da mesma decodificação.

Arquivos, números mágicos e o hábito do .b64

Decodificar é só metade do trabalho; os bytes geralmente querem um arquivo. O padrão é ler, decodificar, checar, escrever, e a checagem importa porque um payload quebrado de outro modo produziria um arquivo silenciosamente errado que você só descobriria semanas depois:

import base64
import binascii
with open("payload.b64", "rb") as handle:
  encoded = handle.read()
try:
  data = base64.b64decode(encoded, validate=True)
except binascii.Error:
  data = base64.b64decode(encoded)
with open("payload.bin", "wb") as out:
  out.write(data)

Para conversões rápidas de uma vez, a função legada de arquivo-para-arquivo faz a viagem inteira em uma única chamada, linhas envoltas e tudo:

import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
  base64.decode(src, dst)

O que você acabou de decodificar, afinal? Os primeiros bytes de quase todos os formatos comuns são uma assinatura fixa, e como o Base64 é determinístico, a assinatura codificada também é fixa. Ver um desses prefixos é como reconhecer uma placa de carro à distância:

O Base64 Começa Com Provavelmente É
iVBORw0KGgo uma imagem PNG
/9j/ uma imagem JPEG
R0lGODlh uma imagem GIF
JVBERi0 um documento PDF
UEsDBA== um arquivo ZIP
UklGRg== um contêiner RIFF (WAV, WEBP, AVI)
LS0tLS1CRUdJTg== um bloco blindado em ASCII ("-----BEGIN ...")

E faça a matemática de tamanho enquanto o arquivo se escreve, porque é o número que surpreende as pessoas quando o disco enche: codificar infla os dados em mais ou menos um terço, então um arquivo de 300 KB viaja como uns 400 KB de texto Base64, e o arquivo que você decodifica de volta é do tamanho menor, o original. O seu disco, e a sua memória se você ler o arquivo inteiro de uma vez, devem reservar espaço para a diferença.

Bancos de dados, arquivos de configuração e variáveis de ambiente

O Base64 é um favorito para contrabandear binário (ou JSON) por armazenamentos que só aceitam texto: uma coluna TEXT, um valor em um arquivo .ini, uma variável de ambiente em um pipeline de deploy. A receita de decodificação é a mesma que para arquivos, menos o disco:

import base64
import json
stored = "eyJyb2xlIjogImFkbWluIiwicHJvamVjdCI6Im15c2l0ZSJ9"
payload = json.loads(base64.b64decode(stored))
print(payload)
# {'role': 'admin', 'project': 'mysite'}

Duas notas para esse canto da casa. Quando o valor armazenado é JSON, pule a etapa intermediária de .decode("utf-8") e deixe o json.loads pegar os bytes diretamente, já que ele faz isso desde o Python 3.6. E um aviso honesto, porque é aqui que mora o mal-entendido mais caro do artigo inteiro: Base64 em uma variável de ambiente ou em um arquivo de configuração é um escudo contra o humano que dá uma olhada no arquivo, não contra o que o lê. Se o valor for de verdade sensível, criptografe primeiro (o pacote cryptography embarca o Fernet exatamente para isso) e só então codifique o texto cifrado em Base64 se o seu armazenamento exigir texto.

Quando o payload chega em pedaços

A biblioteca padrão não tem um decodificador incremental de Base64: não existe um par update-e-finish, então fazer stream de dados precisa de um pouco de contabilidade sua. A aritmética é simples e estrita ao mesmo tempo. Quatro caracteres codificados fazem três bytes, então você só pode decodificar grupos completos de quatro caracteres, e deve levar o resto para o próximo chunk:

import base64
def chunked_decode(chunks):
  out = []
  leftover = b""
  for chunk in chunks:
    buffer = leftover + chunk
    whole = len(buffer) // 4 * 4
    if whole:
      out.append(base64.b64decode(buffer[:whole]))
    leftover = buffer[whole:]
  if leftover:
    out.append(base64.b64decode(leftover + b"=" * (-len(leftover) % 4)))
  return b"".join(out)

Alimente-o com um buffer de socket, um arquivo lido em pedaços de 64 KB, ou um gerador de linhas com as quebras removidas, e a saída é idêntica à de decodificar o todo de uma vez. Se a sua entrada é garantida limpa e sem quebra de linha, mantenha o rigor decodificando cada grupo completo com validate=True, e lembre-se de que o resto final pode precisar da correção de padding, que é por que o helper a adiciona antes da última decodificação. Essa é a mesma lógica de costura que os codificadores usam do outro lado, só que com quatro caracteres em vez de três bytes.

Da linha de comando

O módulo base64 também faz o papel de uma ferramenta minúscula de linha de comando, o que é útil quando o payload está no seu terminal em vez do seu código. Codificar é o padrão; -d (ou o seu par, -u) decodifica:

echo -n "hello world" | python3 -m base64
aGVsbG8gd29ybGQ=
echo -n "aGVsbG8gd29ybGQ=" | python3 -m base64 -d
hello world

Ele lê do stdin quando você não dá nenhum arquivo, ou do arquivo que você nomeia, e por baixo dos panos é a interface legada de arquivo-para-arquivo, então a saída vem envolta a 76 caracteres com um salto de linha no final de cada linha. Para colar um payload em uma sessão com o rigor ligado, a versão de uma linha do decodificador é um bom hábito:

import base64
import sys
print(base64.b64decode(sys.stdin.read(), validate=True))

Nove jeitos de se queimar

Cada bug de decodificação de Base64 em Python é um destes. Guarde a lista em algum lugar onde você a encontre no meio de um pânico, porque ela pegou mais tardes do que qualquer outro documento único que você vai ler este ano. Os três primeiros vêm com código, porque ficam mais fáceis de lembrar depois que você já viu a catástrofe:

O padding ausente. A queda mais comum de todas, geralmente porque uma parte de JWT ou um valor de API chegou sem os seus pads:

import base64
import binascii
segment = "Zm9vYmE"
try:
  base64.urlsafe_b64decode(segment)
except binascii.Error as caught:
  print(caught)
# Incorrect padding
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'

A string truncada. Quando o erro diz que o número de caracteres de dados "cannot be 1 more than a multiple of 4", o payload foi cortado em trânsito, ou um copiar-colar perdeu um caractere no final. Nenhuma quantidade de padding arruma uma string cujo comprimento é um módulo quatro; os dados simplesmente não estão ali, e a resposta honesta é pedir o payload de novo.

O lixo silencioso. O modo permissivo decodifica o que sobreviver, e palavras comuns em inglês estão cheias de letras do alfabeto Base64, então uma palavra solta à frente do payload vira bytes de verdade colados aos seus dados:

import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - três bytes de pura ficção, depois a verdade

Os outros seis não precisam de código nenhum:

  • Você decodificou uma string base64url com o decodificador padrão. Os traços e sublinhados não estão no alfabeto padrão, então desapareceram em silêncio e o payload saiu corrompido, ou vazio. Use urlsafe_b64decode com a correção de padding.
  • Você esqueceu que o resultado é bytes. Colá-lo a uma string lança um TypeError, e empurrá-lo para uma resposta JSON serializa a representação b'...'. Chame .decode(encoding) na fronteira, de propósito, com a codificação que você realmente quer dizer.
  • Você passou uma string não-ASCII. O decodificador aceita strings, mas só as ASCII; qualquer outra é um ValueError. Se o seu payload saiu de um arquivo de texto que foi lido com a codificação errada, corrija a leitura, não a decodificação.
  • Você decodificou duas vezes. Os dados já tinham sido decodificados a montante, ou era Base64 de Base64, e a segunda passada transformou a sua senha em seis bytes que nenhum humano vai ler de novo.
  • Você usou o modo estrito em dados com quebra de linha. Um único salto de linha é o suficiente para fazer o validate=True lançar, então blocos MIME e corpos PEM pertencem às ferramentas permissivas, não à estrita.
  • Você confiou em um pad no meio. No modo permissivo um = em qualquer lugar da string é descartado em silêncio, então um payload corrompido com um pad mal colocado pode decodificar para a resposta "certa". Só o modo estrito percebe, e percebe se recusando.

Se o seu trabalho é ser o porteiro, aqui está um pequeno helper que põe os dois humores para trabalhar juntos: estrito primeiro, correção de padding em segundo, e uma falha barulhenta quando nenhum ajuda:

import base64
import binascii
def safe_decode(text):
  candidate = text.strip()
  try:
    return base64.b64decode(candidate, validate=True)
  except binascii.Error:
    padded = candidate + "=" * (-len(candidate) % 4)
    return base64.b64decode(padded, validate=True)
print(safe_decode("Zm9vYmE"))
# b'fooba'
print(safe_decode("Zm9vYmFy"))
# b'foobar'

Note que o helper ainda confia no alfabeto em que ele é mandado confiar. Se a sua entrada pode ser base64url, alimente-o com urlsafe_b64decode em vez disso. A validação é um contrato, e o contrato diz em qual dialeto os dados estão.

Três décadas de um módulo quieto

O módulo está na biblioteca padrão há um quarto de século, e a maior parte do tempo ficou parado. Quando ele se moveu, os movimentos foram pequenos mas reais, e eles explicam algumas histórias de "funciona na minha máquina" que flutuam por fóruns antigos:

  • 1995 - Jack Jansen reescreveu base64.py para delegar o trabalho de verdade ao módulo binascii de nível C. O comentário ainda está no arquivo, e a delegação ainda é verdadeira hoje.
  • 2003, embarcado no Python 2.4 - Barry Warsaw adicionou suporte completo à RFC 3548: as famílias b16, b32 e b64, mais as variantes standard_* e urlsafe_* que você usa hoje.
  • Python 3.1 - encodestring e decodestring foram descontinuados em favor de encodebytes e decodebytes, os nomes que ficaram.
  • Python 3.3 - as funções de decodificação começaram a aceitar strings ASCII, encerrando a era em que toda decodificação começava com um literal de bytes.
  • Python 3.4 - qualquer objeto bytes-like (incluindo memoryviews) é aceito em qualquer lugar, e os primos Base85, a85 e b85, entraram no módulo.
  • Python 3.9 - os encodestring e decodestring desde há muito descontinuados foram finalmente removidos. Tutoriais antigos que os chamam precisam de uma renomeação de uma palavra.
  • Python 3.10 - b32hexencode e b32hexdecode chegaram com o alfabeto hex estendido, o que mantém dados codificados ordenáveis em ordem lexicográfica.
  • Python 3.11 - binascii.a2b_base64 ganhou strict_mode, que é o que validate=True usa por baixo dos panos.
  • Python 3.13 - z85encode e z85decode trouxeram o dialeto Z85 do ZeroMQ para a biblioteca padrão, e o antigo módulo uu foi removido sob o PEP 594 com uma observação pontual de usar base64 no lugar.
  • Python 3.14 - b16decode ficou até seis vezes mais rápido: a validação dele agora roda em bytes.translate em vez de uma expressão regular, e o módulo não importa mais re de jeito nenhum. O tempo de importação dele também entrou na lista de módulos melhorados.

Nada disso muda o que as funções fazem, que é o luxo quieto de um módulo tão velho: código que decodificou Base64 em 2005 ainda o decodifica em 2026, na mesma linha, com o mesmo resultado.

Delícias das margens

O trabalho sério está feito, então aqui estão as pequenas delícias que o módulo esconde nas suas margens:

  • A documentação própria do módulo rodou a mesma demonstração por mais de uma década: entra b'data to be encoded', sai b'ZGF0YSB0byBiZSBlbmNvZGVk'. Se você leu a página de base64 de qualquer release do Python nos últimos vinte anos, você já encontrou esse par antes.
  • A palavra junk é uma string Base64 perfeitamente válida. As quatro letras estão no alfabeto, que é por que uma palavra solta no começo de um payload vira três bytes de ficção em vez de um erro, e é por que o modo permissivo ganha o seu apelido.
  • O urlsafe_b64decode é acidentalmente bilíngue. Ele traduz o alfabeto dele primeiro e então decodifica de forma permissiva, então ele também vai ler uma string do alfabeto padrão com + e / dentro. Uma função, dois dialetos, zero reclamações.
  • As mensagens de erro são um mini-lexicão estável que não se moveu desde a implementação em C: Incorrect padding, Only base64 data is allowed, Excess padding not allowed, Leading padding not allowed. Aprenda-as e você pode triar um payload quebrado sem rodar uma única linha de código.
  • A string vazia é a única entrada que não provoca reação alguma: entra b'', sai b'', nos dois humores. Nada entra, nada sai, sem alarme.
  • A docstring do módulo ainda cita a RFC 3548, a edição de 2003 da especificação. A RFC 4648 é o padrão vigente desde 2006, e o módulo a segue fielmente sem se incomodar de atualizar a frase.
  • O Python 2 não tinha parede de tipo no lado da decodificação: uma str simples entra, uma str simples sai. A grande reescrita de bytes de 2007 do desenvolvimento do Python 3 mudou isso, e é para os tutoriais antigos do Python 2 que a maioria das threads de "por que a minha decodificação está quebrada" ainda aponta.

Então aqui está a filosofia inteira em quatro regras. Passe validate=True para qualquer coisa que você não codificou você mesmo, e trate a exceção como uma resposta de verdade em vez de uma sugestão. Saiba em qual dialeto você está segurando, padrão, base64url, ou envolto em MIME, porque o decodificador não vai te dizer; ele só vai chutar descartando o que não couber. Trate o resultado como bytes até você ter provado que é texto, e então pergunte quem era dono do charset. E lembre-se de que o recurso mais amigável desta função, a disposição de decodificar coisas que não são exatamente Base64, é o mesmo recurso que a torna perigosa, então decida, em cada chamada, quanta confiança a entrada mereceu.

Se em algum momento você precisar ir na direção oposta, embrulhando bytes frescos de volta naquela fita amigável de letras para um token, um anexo, ou uma imagem inline, a história inteira do b64encode é coberta em detalhe no artigo relacionado de codificação Base64 no final desta página. As duas direções são imagens espelhadas, mas cada uma tem o seu próprio conjunto de surpresas, e você agora conhece esta de cor. Boa decodificação.

Última atualização: 2026-09-08

Artigo relacionado: Codificação Base64 em Python: um guia completo