Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Decodifica Base64 in Python: una guida completa

In qualche punto del tuo codice è appena atterrata una stringa di lettere che non assomiglia minimamente al testo: un lungo filare di lettere dalla A alla Z, qualche cifra, l'occasionale + o /, forse un - o un _, e magari uno o due segni = parcheggiati in coda. Dietro quella stringa potrebbe esserci il carico utile di un JWT respinto dal tuo gateway, un'immagine nascosta dentro una pagina di HTML, un file che qualcuno ti ha spedito come allegato .b64, o il blocco di un certificato dentro un ticket che ha attraversato tre sportelli di assistenza. Il tuo compito è restituire i byte originali, esattamente come erano. E Python è in gran forma per questo lavoro, perché l'intera cassetta degli attrezzi viaggia nella libreria standard da decenni: una riga di import base64 e sei pronto su ogni piattaforma, senza niente da installare e niente da configurare.

Mentre ti sistemi, un rapido richiamo, perché capita a tutti almeno una volta l'anno: Base64 riscrive ogni tre byte di dati come quattro caratteri tratti da un alfabeto di 64 caratteri, e quando l'ultimo gruppo di tre byte è incompleto, il riempimento = lo completa, così l'output arriva sempre a gruppi di quattro. È tutto qui il trucco. Non è compressione e non è segretezza, solo un modo per far sopravvivere il binario a canali che non accettano altro che testo. La home page di questo sito percorre il formato in tutta la sua profondità, alfabeto e calcoli del riempimento inclusi, quindi spenderemo le nostre energie dove il dolore sta davvero: sul lato Python della decodifica, e sul mantenere il risultato onesto.

Tre fatti plasmano tutto quello che segue, e vale la pena memorizzarli prima di leggere un'altra riga. Primo, il decoder ha due umori: un predefinito cortese e indulgente che scarta in silenzio tutto ciò che non riconosce, e una modalità rigorosa che rifiuta un input del genere in blocco. Secondo, il risultato di una decodifica è sempre un oggetto bytes, mai una stringa, e il momento in cui vuoi del vero testo da dentro è una decisione che devi prendere con consapevolezza. Terzo, esistono due alfabeti quasi identici, quello standard e quello URL-safe, e confonderli è uno dei modi preferiti per perdere dati senza che scatti nemmeno un errore. Questa guida ti fa passare oltre tutti e tre, così la prossima volta che un muro di caratteri senza senso atterra nel tuo terminale, potrai sorridere invece di strizzare gli occhi.

Il menu completo della decodifica

Apri il modulo base64 e troverai due generazioni di interfacce affiancate. Quella moderna, centrata su b64decode, converte gli oggetti bytes-like (e le semplici stringhe ASCII) di nuovo in byte, e parla entrambi i dialetti Base64 definiti in RFC 4648. Quella legacy è più vecchia e orientata ai file: lavora sugli oggetti file, conosce solo l'alfabeto standard, ed è stata costruita attorno alle righe a 76 caratteri con a capo che RFC 2045, lo standard MIME per la posta del 1996, pretendeva dall'output codificato. Incontrerai i nomi legacy in abbondante codice che è in giro da un po', quindi ecco la parte completa del menu dedicata alla decodifica:

Funzione Cosa fa Note
base64.b64decode(s, altchars=None, validate=False) il cavallo da lavoro: un blob Base64 di nuovo in byte grezzi accetta byte o stringa ASCII, restituisce sempre byte
base64.standard_b64decode(s) lo stesso lavoro, agganciato all'alfabeto standard comodo quando conosci il dialetto con certezza
base64.urlsafe_b64decode(s) legge l'alfabeto URL-safe con - e _ quello che legge i JWT
base64.decodebytes(s) decodifica una o più righe Base64 con a capo aggiunto in Python 3.1, la via amica del MIME, permissiva
base64.decode(input, output) sposta in streaming un file Base64 in un file grezzo legacy, legge riga per riga, permissivo
base64.b32decode(s, casefold=False) decodifica il cugino più piccolo, Base32 casefold accetta input in minuscolo
base64.b16decode(s, casefold=False) decodifica Base16, che è esadecimale puro fino a sei volte più veloce in Python 3.14
binascii.a2b_base64(s, strict_mode=False) la funzione a livello C che fa il lavoro vero una leva diretta sul rigore, con strict_mode da Python 3.11 in poi

Tutto ciò che segue si costruisce sulla prima riga. Un fatto merita di essere saputo prima di scendere più in profondità: nella documentazione ufficiale il modulo vive sotto "Gestione dei dati di internet", proprio accanto a binascii, e quella collocazione non è un caso. b64decode è un sottile involucro che traduce l'alfabeto (quando passi altchars) e poi lascia al binascii.a2b_base64 di livello C il lavoro pesante. È per questo che la funzione è veloce, e per questo i suoi messaggi di errore hanno il sapore netto e disincantato della C.

Il cavallo da lavoro: b64decode

Ecco l'intero contratto, corto abbastanza da tenerlo in testa. La funzione accetta un oggetto bytes-like o una stringa ASCII, un'opzionale sostituzione a due caratteri dell'alfabeto e un flag di validazione. Restituisce un oggetto bytes. In caso di errore solleva binascii.Error, che è una sottoclasse di ValueError, per il caso in cui ti serva catturare tutta una famiglia di eccezioni in un colpo:

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

Quella riga finale è la singola riga più importante di questo articolo. Il risultato è byte, non una stringa, e Python ti tiene la mano esattamente fino a dove dovrebbe: stampare l'oggetto ti mostra la rappresentazione b'...', e provare a incollarlo a una stringa solleva un TypeError. Il momento in cui vuoi del testo vero e proprio è una decisione tua, e la sezione sulla codifica dei caratteri sotto copre quando quella decisione è facile e quando è una trappola.

L'argomento opzionale altchars sostituisce il + e il / dell'alfabeto standard con una diversa coppia di caratteri. È esattamente la manopola che produce il dialetto URL-safe, ed è così che urlsafe_b64decode si costruisce sopra b64decode. Raramente sarà altchars a raggiungere la tua mano, ma è bene sapere che il meccanismo c'è. Per tutto il resto, la funzione semplicemente fa il lavoro, veloce, in C.

Permissivo di default, rigoroso su richiesta

Di default, b64decode è uno scartatore educato. Ogni carattere non presente nell'alfabeto di 64 caratteri (e non presente nel tuo altchars) viene scartato in silenzio prima che la decodifica cominci, e tutto ciò che sopravvive viene decodificato. Nessun avviso, nessuna notifica, nessun valore di ritorno da controllare, solo un risultato. Quella tolleranza ha un nobile antenato: la sezione 6.8 di RFC 2045 dice ai decoder che "tutti gli a capo e gli altri caratteri non presenti nella Tabella 1 devono essere ignorati", perché lo SMTP storicamente spezzava le righe lunghe e spargeva caratteri vaganti lungo il cammino. Un carico utile che ha attraversato un client di posta, un'app di chat o una copia da un PDF verrà spesso decodificato senza nessuna preparazione, e questo è un vero superpotere.

La stessa gentilezza è anche la ragione per cui il decoder di default è inutile come validatore. La sezione 12 di RFC 4648 spiega il rischio nel dettaglio: ignorare i caratteri fuori alfabeto invece di rifiutare l'intera codifica apre un canale nascosto che può essere usato per perdere informazioni, e può rompere i controlli di uguaglianza tra stringhe, perché due input diversi possono decodificarsi negli stessi byte. Per qualsiasi cosa non codificata da te, passa validate=True e tratta l'eccezione come la risposta. Ecco il rapporto dei danni, ogni riga riproducibile su qualsiasi Python moderno:

In ingresso Permissivo (default) validate=True
Zm9vYmFy (un carico utile pulito) b'foobar' b'foobar'
Zm9v\r\nYmFy (a capo in mezzo) b'foobar' binascii.Error
Zm9v YmFy (spazi in più) b'foobar' binascii.Error
Zm9v!YmFy (un punto esclamativo vagante) b'foobar' binascii.Error
junkZm9vYmFy (una parola davanti al carico utile) b'\x8e\xe9\xe4foobar' b'\x8e\xe9\xe4foobar'
Zm9v=YmFy (un riempimento in mezzo) b'foobar' binascii.Error
=Zm9v (riempimento in testa) b'foo' binascii.Error
==== (quattro riempimenti, nessun dato) b'' binascii.Error
(input vuoto) b'' b''

Guarda la colonna permissiva fare il suo lavoro in silenzio. La riga che sorprende per prima è quella con la parola in testa: tutte e quattro le lettere di junk finiscono per stare nell'alfabeto Base64, quindi la "spazzatura" si decodifica come tre byte veri e viene incollata al tuo carico utile con aria composta. La modalità rigorosa non è una salvatrice in questa riga, perché l'input è davvero un Base64 valido; sono le altre righe a venire rifiutate in blocco, e i rifiuti hanno una forma unica: un binascii.Error con uno di un pugno di messaggi memorabili:

  • Incorrect padding - la lunghezza non è un multiplo di quattro dopo lo scarto, o un gruppo finale è troppo corto. Una stringa come Zm9vYmE senza riempimenti arriva qui.
  • Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4 - l'input è corto di esattamente un carattere rispetto al gruppo successivo. È l'impronta digitale classica di un carico utile troncato o copiato-incollato.
  • Only base64 data is allowed - un carattere fuori alfabeto è sopravvissuto fino alla modalità rigorosa, e un singolo a capo ne conta come uno.
  • Excess padding not allowed - riempimenti in mezzo alla stringa, o più riempimenti di quanti ne consenta il gruppo finale.
  • Leading padding not allowed - la stringa comincia con =.
  • E uno di un'altra famiglia: ValueError: string argument should contain only ASCII characters, quello che ottieni quando passi una stringa con lettere non ASCII. Le stringhe sono accettate, ma solo quelle ASCII.

Dietro le quinte, validate=True non è affatto un percorso di codice a parte. Il modulo inoltra il flag a binascii.a2b_base64 come suo parametro strict_mode, il controllo rigoroso aggiunto a binascii in Python 3.11. Ti dà una presa diretta quando vuoi il rigore senza passare dal livello base64:

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

Una stranezza da fissare prima di fidarti ciecamente della modalità rigorosa: rifiuta anche un singolo a capo in coda, quindi un blocco avvolto in stile MIME è un lavoro per la via permissiva o per decodebytes, non per validate=True. Tieni la via rigorosa per i dati che ti aspetti perfettamente puliti, come un token appena coniato, uscito di fresco dal tuo codice.

base64url, l'alfabeto che sta dentro gli URL

L'alfabeto standard ha due caratteri che gli URL odiano. Il segno + viene letto come spazio da qualsiasi decoder di moduli, e il segno / è riservato ai separatori di percorso. La sezione 5 di RFC 4648 definisce il dialetto cugino, in cui + diventa - e / diventa _, e il riempimento viene buttato via ogni volta che la lunghezza dei dati è nota dal contesto. Lo RFC dà perfino a questa variante un nome proprio, base64url, e insiste sul fatto che non va chiamato semplicemente "base64". Lo incontrerai più spesso dentro i JSON Web Token, dove ogni parte del token è base64url senza riempimento, e fa anche capolino nei token OAuth e nei parametri cursore delle API.

Python ne fornisce una funzione dedicata, urlsafe_b64decode. Traduce trattini e linee basse di nuovo in più e barre prima di decodificare, ma non rifà il riempimento al posto tuo. L'input senza riempimento è il caso normale per i JWT, quindi la riga di aritmetica viene prima, ed è la stessa che librerie come PyJWT usano sotto il cofano:

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

L'espressione "=" * (-len(segment) % 4) ha l'aspetto di un trucco, ma è l'intero lavoro: produce zero, uno o due riempimenti, mai tre, così una stringa già riempita passa inalterata. Il modulo negativo è ciò che la fa funzionare con stringhe di ogni lunghezza, ed è la riga di aritmetica Base64 che ogni sviluppatore Python finisce per digitare almeno una volta.

Ora la confusione pericolosa, perché i due alfabeti si somigliano abbastanza da far sbagliare. Passa una stringa base64url attraverso il decoder standard, e i trattini e le linee basse semplicemente non esistono nell'alfabeto standard, così il decoder permissivo li inghiotte e decodifica ciò che resta. Per alcuni carichi utili è una sequenza di byte corrotta, per altri è proprio niente:

import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - ogni carattere è stato scartato in silenzio
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'

La direzione inversa è indulgente, ed è questo a far sì che la confusione passi inosservata: urlsafe_b64decode prima traduce il suo alfabeto e poi decodifica in modo permissivo, quindi accetta di buon grado anche una stringa dell'alfabeto standard con dentro + e /. La lezione non è improvvisare. La lezione è scegliere una funzione per dialetto e non cambiarla più, come si farebbe con una valuta straniera: spendi yen dove gli yen sono validi, non alla cassa di cambio sbagliata.

L'output è byte: la conversazione sulla codifica dei caratteri

Ecco la frase che risolve metà delle domande sulla codifica dei caratteri che la gente porta a Base64: b64decode decodifica byte, non decodifica testo. Non c'è un argomento per la codifica, non c'è conversione, e nulla nell'input dice a Python cosa dovrebbero significare i byte. Il significato è una cosa che devi fornire tu dal contesto, e quel contesto è quasi sempre una di tre cose: un header che lo dice, un contratto API che lo dice, o un numero magico nascosto nei byte stessi.

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

La stessa idea con l'etichetta sbagliata è un fallimento assordante, ed è una fortuna. I byte che non sono UTF-8 valido rifiutano di diventare stringa, e l'eccezione ti dice esattamente quale byte è stato il colpevole:

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

Tre regole pratiche tengono questa sezione lontana dallo spavento. Una: se i dati decodificati sono JSON, non devi decodificare a mano nemmeno per un istante, perché json.loads accetta i byte direttamente da Python 3.6 e rileva da solo UTF-8, UTF-16 e UTF-32. Due: il binario non è testo, quindi una codifica "rilevata" per un PNG è un colpo di fortuna, non un fatto. Controlla i byte, non l'etichetta. Tre: se il mittente ti ha detto la codifica, credi al mittente, perché un header content-type o un documento API battono qualsiasi rilevatore, ogni singola volta.

Dove il Base64 decodificato fa capolino nel codice Python

Dopo un po' cominci a riconoscere le forme. Ecco la guida da campo dei luoghi in cui il Base64 decodificato spunta in un'applicazione Python, e la ricetta in una riga per ciascuno. Le sezioni che seguono danno il trattamento completo a quelli più comuni:

Dove lo trovi Cosa è Come si legge
Un JWT le parti header, carico utile e firma (RFC 7519) spezza sul punto, urlsafe_b64decode con la correzione del riempimento
Un header Authorization credenziali HTTP Basic, user:pass (RFC 7617) tolgi il prefisso Basic, decodifica, spezza al primo due punti
Un URI data: media inline dentro HTML o CSS (RFC 2397) taglia alla prima virgola, decodifica il resto
Un allegato di posta un corpo con Content-Transfer-Encoding: base64 (RFC 2045) get_payload(decode=True) sulla parte del messaggio
Un valore di header di posta una parola codificata =?charset?b?...?= (RFC 2047) lascia che il pacchetto email lo decodifichi per te
Un file PEM una chiave o un certificato corazzati (RFC 7468) butta via le righe dell'armatura, decodifica il corpo in DER
Un campo di un'API JSON binario contrabbandato sotto forma di stringa decodifica, poi tratta il risultato come byte, non come testo
Una colonna TEXT o una variabile d'ambiente binario o JSON immagazzinato in un luogo solo testo decodifica, poi analizza o scrivi, con la codifica su cui siete d'accordo

Leggere un JSON Web Token

Un JWT è tre pezzi base64url uniti da punti: un header, un carico utile e una firma. I primi due sono JSON puro, quindi darci un'occhiata dentro è una riga ciascuno, usando la correzione del riempimento della sezione precedente:

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'}

Una nota sul campo di applicazione, perché conta: ispezionare un token in questo modo è uno strumento di debug, non un meccanismo di autenticazione. Il fatto che il carico utile sia leggibile non significa che sia autentico. Un attaccante può falsificare i primi due segmenti senza mai conoscere il tuo segreto. Per una verifica vera, consegna il token a PyJWT (pip install pyjwt), che controlla la firma e rifiuta di decodificare senza un elenco esplicito di algoritmi:

import jwt
# Una chiave sotto i 32 byte riceve l'InsecureKeyLengthWarning di PyJWT (PyJWT 2.11+), un brontolio legittimo per una chiave da demo.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}

Con una chiave sbagliata ottieni un'eccezione invece di un dizionario, che è esattamente il comportamento che vuoi nel codice di produzione. E se il token arriva con un timestamp scaduto, PyJWT solleva anche per quello, così non devi ricordarti tu i nomi dei claim.

Aprire un Data URI

I Data URI incorporano i media direttamente dentro HTML o CSS, così il browser non deve sparare una seconda richiesta: data:, il tipo di media, la parola base64, una virgola e i byte codificati. Il taglio è alla prima virgola, fine della storia, e tutto ciò che viene dopo è un semplice carico utile dell'alfabeto standard:

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'

I 8 byte della firma PNG in testa al risultato sono un controllo poco costoso e di buon umore per accertare che hai decodificato la cosa giusta. Due trabocchetti meritano una menzione. Se l'URI viene da una pagina presa con uno scraper o da un messaggio di chat, prima togli le entità HTML e gli spazi bianchi vaganti, perché il decoder permissivo perdonerà un sacco di spazzatura e ti consegnerà un'immagine corrotta invece di un errore. E se stai decodificando in blocco input di cui non ti fidi, passa validate=True: un data URI che non supera la validazione rigorosa è un data URI che non è mai stato ben formato, e non vuoi scriverlo a disco sull'istinto.

Screpolare l'header di Authorization

L'autenticazione Basic (RFC 7617) è il meccanismo più vecchio di HTTP, e ancora tiene in piedi un numero sorprendente di integrazioni API, webhook e pipeline CI. Il client invia le proprie credenziali come user:pass, codificate in base64, dietro la parola 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

Notare il partition, perché è il dettaglio che ti salverà più tardi: la password può contenere due punti, l'ID utente no, e solo il primo due punti è il separatore. Una nota onesta, perché l'RFC stesso è secco su questo: base64 non è crittografia. RFC 4648 dice che la codifica in base "nasconde visivamente informazioni altrimenti facilmente riconoscibili, come le password, ma non fornisce alcuna segretezza computazionale". Un header Basic può essere decodificato da chiunque veda il traffico, quindi trattalo come una comodità per le connessioni protette da TLS, non come un confine di sicurezza. Quando sei tu a inviare l'header, requests lo costruisce per te con auth=("jane", "pa:ss"), che vale la pena usare quando la libreria è già nel tuo stack.

La posta elettronica, la cliente originale

Base64 è stato standardizzato nel 1993 per esattamente un lavoro: far sopravvivere il binario alla posta elettronica. RFC 2045, lo standard MIME, ha definito la codifica del corpo Content-Transfer-Encoding: base64, ed è ancora il modo predefinito con cui gli allegati viaggiano attraverso internet. Il pacchetto email di Python fa l'intero lavoro per te: analizza gli header, decodifica le parole codificate =?utf-8?b?...?= che RFC 2047 nasconde nei campi degli header, e decodifica in base64 i corpi quando glielo chiedi:

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'

La chiamata get_payload(decode=True) legge l'header Content-Transfer-Encoding e decodifica in base64 il corpo per te, srotolando lungo il cammino le righe da 76 caratteri. L'argomento policy=policy.default seleziona l'interfaccia moderna da Python 3.6 in poi, quando la nuova API email basata su policy ha smesso di essere provvisoria, e ti dà valori di header decodificati fin da subito. L'analizzatore legacy funziona ancora, ma alla fine finisci per decodificare le parole codificate a mano. Scendi fino a decodebytes solo quando stai analizzando un frammento nudo che non è un messaggio completo, come un blocco che qualcuno ha incollato in un ticket. Per i messaggi multipart, itera con iter_attachments() e dai a ogni parte lo stesso trattamento in una riga.

L'armatura PEM e il pacchetto cryptography

Un file PEM è una riga di header, un po' di Base64 avvolto e una riga di footer, e nient'altro. L'armatura è decorativa. Il Base64 è la storia intera, perché si decodifica nella struttura DER grezza che sta sotto. Il pacchetto cryptography (pip install cryptography) può caricare il risultato direttamente, ed è per questo che è lo strumento standard per qualsiasi cosa riguardi certificati e chiavi:

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

Nella maggior parte del codice di produzione non si toglie mai a mano l'armatura e non si decodifica: load_pem_x509_certificate accetta i byte corazzati e gestisce il passaggio Base64 per te sotto il cofano. La via manuale guadagna il suo prezzo quando i byte DER sono già nelle tue mani (una colonna del database, un file di configurazione, un buffer di byte da un protocollo), o quando il blocco arriva avvolto in una stringa e vuoi vedere cosa c'è dentro prima di fidarti. Le chiavi funzionano allo stesso modo, con load_der_private_key ad aspettare dall'altra parte della stessa decodifica.

File, numeri magici e l'abitudine del .b64

La decodifica è solo metà del lavoro. I byte di solito vogliono diventare un file. Lo schema è leggi, decodifica, controlla, scrivi, e il controllo conta, perché un carico utile rotto altrimenti produrrebbe un file sbagliato in silenzio, e lo scopriresti settimane dopo:

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)

Per conversioni rapide una tantum, la funzione legacy da file a file fa l'intero viaggio in una singola chiamata, righe avvolte incluse:

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

Dunque, cosa hai appena decodificato? I primi byte di quasi tutti i formati comuni sono una firma fissa, e dato che Base64 è deterministico, anche la firma codificata è fissa. Vedere uno di questi prefissi è come riconoscere una targa da lontano:

Con cui comincia il Base64 Probabilmente è
iVBORw0KGgo un'immagine PNG
/9j/ un'immagine JPEG
R0lGODlh un'immagine GIF
JVBERi0 un documento PDF
UEsDBA== un archivio ZIP
UklGRg== un contenitore RIFF (WAV, WEBP, AVI)
LS0tLS1CRUdJTg== un blocco corazzato in ASCII ("-----BEGIN ...")

E fai il calcolo delle dimensioni mentre il file si scrive, perché è il numero che sorprende la gente quando il disco si riempie: la codifica gonfia i dati di circa un terzo, quindi un file da 300 KB viaggia come circa 400 KB di testo Base64, e il file che ricavi decodificando è della dimensione più piccola, originale. Il tuo disco, e la tua memoria se leggi l'intero file in un colpo, dovrebbero mettere in bilancio la differenza.

Database, file di configurazione e variabili d'ambiente

Base64 è amato per contrabbandare il binario (o il JSON) attraverso archiviazioni che accettano solo testo: colonne TEXT, valori dentro file .ini, variabili d'ambiente in una pipeline di distribuzione. La ricetta di decodifica è la stessa dei file, senza il disco:

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

Due note per questo angolo della casa. Quando il valore memorizzato è JSON, salta il passaggio intermedio .decode("utf-8") e lascia che json.loads prenda i byte direttamente, dato che lo fa da Python 3.6. E un avvertimento onesto, perché è qui che vive il malinteso più costoso di tutto l'articolo: il Base64 in una variabile d'ambiente o in un file di configurazione è uno scudo contro l'umano che dà un'occhiata al file, non contro quello che lo legge. Se il valore è davvero sensibile, cifralo prima (il pacchetto cryptography include Fernet esattamente per questo) e solo allora metti in Base64 il testo cifrato se il tuo archivio richiede testo.

Quando il carico utile arriva a pezzi

La libreria standard non ha un decoder Base64 incrementale: non esiste la coppia aggiungi-e-termina, quindi i dati in streaming richiedono un po' di contabilità fatta da te. L'aritmetica è semplice e rigorosa allo stesso tempo. Quattro caratteri codificati fanno tre byte, quindi puoi decodificare solo gruppi completi di quattro caratteri, e devi portare il resto nel blocco successivo:

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)

Alimentalo con un buffer di socket, un file letto a pezzi da 64 KB, o un generatore di righe con gli a capo rimossi, e l'output è identico a decodificare tutto in un colpo. Se il tuo input è garantito pulito e non avvolto, mantieni il rigore decodificando ogni gruppo completo con validate=True, e ricorda che il resto finale potrebbe aver bisogno della correzione del riempimento, ed è per questo che l'ausiliario la aggiunge prima dell'ultima decodifica. Questa è la stessa logica di confine che i codificatori usano dall'altra parte, solo con quattro caratteri invece di tre byte.

Dalla riga di comando

Il modulo base64 fa anche da piccolo strumento da riga di comando, comodo quando il carico utile sta nel tuo terminale invece che nel tuo codice. La codifica è il predefinito. -d (o il suo gemello -u) decodifica:

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

Legge da stdin quando non gli dai un file, o dal file che nomini, e sotto il cofano è l'interfaccia legacy da file a file, quindi l'output arriva avvolto a 76 caratteri, con un a capo finale in ogni riga. Se vuoi incollare un carico utile in una sessione con il rigore al massimo, la versione in una riga del decoder è una bella abitudine:

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

Nove modi per bruciarti

Ogni bug di decodifica Base64 in Python è uno di questi. Tieni la lista in un posto dove la trovi in preda al panico, perché ha rovinato più pomeriggi di qualsiasi altro singolo documento che leggerai quest'anno. I primi tre vengono con il codice, perché una volta vista la rovina sono più facili da ricordare:

Il riempimento mancante. Il crash più comune in assoluto, di solito perché una parte di un JWT o un valore di un'API è arrivato senza i suoi riempimenti:

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'

La stringa troncata. Quando l'errore dice che il numero di caratteri dati "cannot be 1 more than a multiple of 4", il carico utile è stato tagliato in transito, o una copia-incolla ha perso un carattere in fondo. Nessuna quantità di riempimenti ripara una stringa la cui lunghezza è uno modulo quattro. I dati semplicemente non ci sono, e la risposta onesta è chiedere il carico utile di nuovo.

La spazzatura silenziosa. La modalità permissiva decodifica quello che sopravvive, e le parole inglesi comuni sono piene di lettere dell'alfabeto Base64, quindi una parola vagante davanti al carico utile diventa byte veri incollati ai tuoi dati:

import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - tre byte di pura finzione, poi la verità

Gli altri sei non hanno bisogno di codice del tutto:

  • Hai decodificato una stringa base64url con il decoder standard. I trattini e le linee basse non stanno nell'alfabeto standard, quindi sono scomparsi in silenzio e il carico utile è uscito corrotto, o vuoto. Usa urlsafe_b64decode con la correzione del riempimento.
  • Hai dimenticato che il risultato è byte. Incollarlo a una stringa solleva un TypeError, e spingerlo in una risposta JSON serializza la rappresentazione b'...'. Chiama .decode(encoding) al confine, con deliberazione, con la codifica che intendi davvero.
  • Hai passato una stringa non ASCII. Il decoder accetta stringhe, ma solo ASCII. Qualsiasi altra cosa è un ValueError. Se il tuo carico utile è uscito da un file di testo letto con la codifica sbagliata, correggi la lettura, non la decodifica.
  • Hai decodificato due volte. I dati erano già stati decodificati a monte, o era Base64 di Base64, e il secondo passaggio ha trasformato la tua password in sei byte che nessun umano rileggerà mai più.
  • Hai usato la modalità rigorosa su dati avvolti. Un singolo a capo basta a far lanciare validate=True, quindi i blocchi MIME e i corpi PEM appartengono agli strumenti permissivi, non a quello rigoroso.
  • Hai fidato di un riempimento in mezzo. In modalità permissiva un = ovunque nella stringa viene scartato in silenzio, quindi un carico utile corrotto con un riempimento fuori posto può decodificarsi nella risposta "giusta". Solo la modalità rigorosa se ne accorge, e se ne accorge rifiutando.

Se il tuo lavoro è fare il portiere, ecco un piccolo ausiliario che mette i due umori a lavorare insieme: prima il rigore, poi la correzione del riempimento, e un fallimento rumoroso quando nessuno dei due aiuta:

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'

Nota che l'ausiliario continua a fidarsi dell'alfabeto che gli dicono di fidarsi. Se il tuo input potrebbe essere base64url, alimentalo invece con urlsafe_b64decode. La validazione è un contratto, e il contratto dice in quale dialetto stanno i dati.

Trent'anni di un modulo silenzioso

Il modulo è nella libreria standard da un quarto di secolo, e per la maggior parte del tempo è rimasto fermo. Quando si è mosso, i movimenti sono stati piccoli ma veri, e spiegano qualche storia del tipo "funziona sul mio computer" che fluttua nei vecchi forum:

  • 1995 - Jack Jansen riscrisse base64.py per delegare il lavoro vero al modulo binascii di livello C. Il commento è ancora nel file, e la delega è ancora vera oggi.
  • 2003, spedito in Python 2.4 - Barry Warsaw aggiunse il supporto completo a RFC 3548: le famiglie b16, b32 e b64, più le varianti standard_* e urlsafe_* che usi oggi.
  • Python 3.1 - encodestring e decodestring furono deprecati a favore di encodebytes e decodebytes, i nomi che sono rimasti.
  • Python 3.3 - le funzioni di decodifica cominciarono ad accettare stringhe ASCII, chiudendo l'era in cui ogni decodifica cominciava con un letterale di byte.
  • Python 3.4 - qualsiasi oggetto bytes-like (memoryview comprese) è accettato ovunque, e i cugini Base85, a85 e b85, entrarono nel modulo.
  • Python 3.9 - i da tempo deprecati encodestring e decodestring furono infine rimossi. I vecchi tutorial che li chiamano hanno bisogno di un cambio di nome di una parola.
  • Python 3.10 - b32hexencode e b32hexdecode arrivarono con l'alfabeto esadecimale esteso, quello che mantiene i dati codificati ordinabili in ordine lessicografico.
  • Python 3.11 - binascii.a2b_base64 guadagnò strict_mode, su cui validate=True si appoggia sotto il cofano.
  • Python 3.13 - z85encode e z85decode portarono il dialetto Z85 di ZeroMQ nella libreria standard, e il venerabile modulo uu fu rimosso sotto PEP 594, con una nota mirata a usare base64 al suo posto.
  • Python 3.14 - b16decode è diventato fino a sei volte più veloce: la sua validazione ora corre su bytes.translate invece di un'espressione regolare, e il modulo non carica più re affatto. Anche il suo tempo di caricamento è finito nella lista dei moduli migliorati.

Nessuna di queste cose cambia ciò che le funzioni fanno, che è il lusso silenzioso di un modulo così vecchio: il codice che decodificava Base64 nel 2005 lo decodifica ancora nel 2026, sulla stessa riga, con lo stesso risultato.

Delizie dai margini

Il lavoro serio è finito, quindi ecco le piccole delizie che il modulo nasconde nei suoi margini:

  • La documentazione del modulo stesso fa la stessa dimostrazione da oltre un decennio: entra b'data to be encoded', esce b'ZGF0YSB0byBiZSBlbmNvZGVk'. Se hai letto la pagina base64 di qualsiasi release Python negli ultimi vent'anni, hai già incontrato questa coppia.
  • La parola junk è una stringa Base64 perfettamente valida. Tutte e quattro le lettere stanno nell'alfabeto, ed è per questo che una parola vagante all'inizio di un carico utile diventa tre byte di finzione invece di un errore, ed è per questo che la modalità permissiva si è guadagnata il suo soprannome.
  • urlsafe_b64decode è bilingue per caso. Traduce il suo alfabeto prima e poi decodifica in modo permissivo, quindi legge anche una stringa dell'alfabeto standard con dentro + e /. Una funzione, due dialetti, zero lamenti.
  • I messaggi di errore sono un mini-lessico stabile che non si è mosso dalla implementazione C: Incorrect padding, Only base64 data is allowed, Excess padding not allowed, Leading padding not allowed. Imparali e puoi fare il triage di un carico utile rotto senza eseguire una singola riga di codice.
  • La stringa vuota è l'unico input che non ottiene nessuna reazione del tutto: b'' dentro, b'' fuori, in entrambi gli umori. Nulla dentro, nulla fuori, nessun allarme.
  • La docstring del modulo cita ancora RFC 3548, l'edizione 2003 della specifica. RFC 4648 è lo standard corrente dal 2006, e il modulo lo segue fedelmente senza preoccuparsi di aggiornare la frase.
  • Python 2 non aveva un muro di tipi sul lato decodifica: una str semplice dentro, una str semplice fuori. Il ribaltone sui byte del 2007 nello sviluppo di Python 3 ha cambiato le cose, e sono i vecchi tutorial Python 2 dove la maggior parte dei thread "perché la mia decodifica è rotta" ancora puntano.

Quindi ecco l'intera filosofia in quattro regole. Passa validate=True per qualsiasi cosa non codificata da te, e tratta l'eccezione come una risposta vera, non come un suggerimento. Sappi quale dialetto stai tenendo in mano, standard, base64url o MIME-avvolto, perché il decoder non te lo dirà. Si limiterà a indovinare buttando via quello che non entra. Tratta il risultato come byte finché non hai provato che è testo, e poi chiedi a chi appartiene la codifica dei caratteri. E ricorda che la caratteristica più amichevole di questa funzione, la disponibilità a decodificare cose che non sono proprio Base64, è la stessa caratteristica che la rende pericolosa, quindi decidi, a ogni chiamata, quanta fiducia l'input ha meritato.

Se in qualche punto ti serve andare nella direzione opposta, avvolgere di nuovo byte freschi in quella simpatica fascia di lettere per un token, un allegato o un'immagine inline, l'intera storia di b64encode è coperta in dettaglio nell'articolo correlato sulla codifica Base64 in fondo a questa pagina. Le due direzioni sono immagini speculari, ma ciascuna ha il suo set di sorprese, e questa ormai te la conosci a memoria. Buona decodifica.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Codifica Base64 in Python: una guida completa