To decode Base64 in Python, import the built-in base64 module and call base64.b64decode(). It returns bytes, so call .decode("utf-8") on the result when the original data was text. For URL-safe input (with - and _), use base64.urlsafe_b64decode() instead.
import base64
data = base64.b64decode("SGVsbG8sIHdvcmxkIQ==")
print(data) # b'Hello, world!'
print(data.decode("utf-8")) # Hello, world!
The rest of this guide covers the inputs that break it: missing padding, URL-safe strings, binary files and junk characters. I ran every example below in Python 3.14.4, and I note where Python 3.15 behaves differently.
How to decode Base64 in Python step by step
Decoding Base64 in Python takes two steps: turn the Base64 text into bytes, then turn those bytes into whatever they really are.
- Import the module with
import base64. It ships with Python, so there's nothing to install. - Pass the Base64 value to
base64.b64decode(). According to the Python base64 docs, it accepts a bytes-like object or an ASCII string and returns the decodedbytes. - If the original was text, call
.decode("utf-8")on the bytes. - If the original was a file, such as an image or a PDF, write the bytes to disk instead.
Base64 works in fixed blocks. RFC 4648 describes it as turning 24-bit groups of input into 4 encoded characters, padded at the end with =. That's why the length of valid Base64 is a multiple of 4, and why the padding errors below happen.
Decoding Base64 to a string vs to a file
The b64decode() result is raw bytes, and you should only call .decode("utf-8") when you know those bytes are text. Binary data will throw an error if you try.
The string iVBORw0KGgo= is the start of a PNG file. Decoding it works fine:
raw = base64.b64decode("iVBORw0KGgo=")
print(raw) # b'\x89PNG\r\n\x1a\n'
Calling raw.decode("utf-8") on it fails with UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte. That error doesn't mean the Base64 was bad. It means you decoded an image as text. Save it instead:
with open("image.png", "wb") as f:
f.write(base64.b64decode(encoded_png))
Strip the data URI prefix first
Images copied from HTML or CSS often start with a prefix like data:image/png;base64,. As the bobbyhadz guide to the padding error points out, you have to remove it before decoding. Split on the first comma:
encoded = value.split(",", 1)[1] if value.startswith("data:") else value
Fixing "binascii.Error: Incorrect padding"
binascii.Error: Incorrect padding means the Base64 input length isn't a multiple of 4, almost always because the trailing = signs were removed. The Python docs say b64decode() raises binascii.Error when the input is incorrectly padded.
RFC 4648 allows skipping padding when the data length is known some other way. JWTs and URL tokens take that option.
s = "SGVsbG8sIHdvcmxkIQ" # padding stripped
base64.b64decode(s) # binascii.Error: Incorrect padding
base64.b64decode(s + "=" * (-len(s) % 4)) # b'Hello, world!'
The -len(s) % 4 trick adds exactly the number of = signs needed: zero, one or two. Extra padding is also tolerated, since b64decode() ignores surplus = at the end.
The catch: re-padding hides a truncated value. If someone copied only part of a token, padding it can still decode into bytes, just the wrong ones. When the decoded result looks like garbage, check that you have the full string before blaming the decoder.
URL-safe Base64 and the silent wrong answer
URL-safe Base64 swaps two characters: RFC 4648 replaces + and / with - and _ so the value can sit in a URL or filename. Python's urlsafe_b64decode() uses that alphabet.
The trap is that plain b64decode() doesn't always complain about URL-safe input. With the default validate=False, the docs say characters outside the normal alphabet are discarded before the padding check. In my test, that produced a wrong answer with no error at all:
base64.b64decode("-_-_") # b'' (no error, data lost)
base64.urlsafe_b64decode("-_-_") # b'\xfb\xff\xbf'
Sometimes you do get an error, just a confusing one. base64.b64decode("-_8=") raised Invalid base64-encoded string: number of data characters (1) cannot be 1 more than a multiple of 4. The real problem was the alphabet, not the length.
If the value came from a JWT, a URL parameter or anything labelled Base64URL, use urlsafe_b64decode(). A JWT payload decoded this way is usually JSON, so the next step is json.loads(). If that part is new to you, what JSON format is covers the basics.
What changed in Python 3.15
Python 3.15 makes padding optional for URL-safe decoding and adds stricter options to b64decode(). According to python.org, Python 3.15 was first released on 2026-10-09, so many tutorials still describe the older behaviour.
The What's New in Python 3.15 page and the module docs list these changes:
urlsafe_b64decode()gained apaddedparameter, and padding of input is no longer required by default. On Python 3.14.4,urlsafe_b64decode("-_8")still raisedIncorrect paddingfor me.b64decode()gainedpadded,ignorecharsandcanonicalparameters.canonical=Truerejects encodings with non-zero padding bits.- Accepting
+and/together with an alternative alphabet is now deprecated.
If your code has to run on both versions, keep the -len(s) % 4 padding step. It's harmless on 3.15 and required on earlier releases.
Validating Base64 input with validate=True
validate=True makes b64decode() raise binascii.Error on any character outside the Base64 alphabet instead of quietly dropping it. RFC 4648 says implementations must reject such characters unless a specification says otherwise, so the strict mode is the one that matches the standard.
base64.b64decode("SGVs bG8=") # b'Hello'
base64.b64decode("SGVs bG8=", validate=True) # binascii.Error: Only base64 data is allowed
My take: use validate=True for anything that comes from a user or another system. The default is forgiving with line breaks and spaces, which helps with copy-pasted values, but it is also how -_-_ turned into an empty result above. I'd rather get an exception than a wrong file.
If the input is wrapped across lines, like a PEM block or an email attachment, either strip the whitespace first or use the default mode on purpose. In my test, b64decode("SGVs\nbG8=") returned b'Hello' with the newline discarded.
Quick reference: errors and their causes
Most Base64 errors in Python come down to one of five causes:
| What you see | Likely cause | Fix |
|---|---|---|
Incorrect padding |
Trailing = stripped or value cut off |
Add "=" * (-len(s) % 4), check the full value |
number of data characters (1) cannot be 1 more than a multiple of 4 |
URL-safe input given to b64decode(), or a truncated value |
Try urlsafe_b64decode() |
Only base64 data is allowed |
Spaces, newlines or a prefix with validate=True |
Strip whitespace and any data: prefix |
UnicodeDecodeError |
The decoded bytes aren't text | Write the bytes to a file |
| Wrong or empty output, no error | Non-alphabet characters silently dropped | Use the right decoder and validate=True |
For a one-off check without writing a script, the Base64 decoder on this site decodes text, URL-safe values, Data URIs and PEM blocks in the browser. For anything repeatable, use the code.
FAQ
How do I convert Base64 to a string in Python?
Call base64.b64decode() on the Base64 text to get bytes, then call .decode('utf-8') on those bytes. Only do the second step when the original data was text.
Why do I get binascii.Error: Incorrect padding?
The input length isn't a multiple of 4 because the trailing = signs were stripped or the string was cut off. Add '=' * (-len(s) % 4) to the end, or check that you copied the whole value.
What is the difference between b64decode and urlsafe_b64decode?
urlsafe_b64decode expects - and _ where standard Base64 uses + and /. Use it for JWTs, URL parameters and anything labelled Base64URL.
Do I need to import anything to decode Base64 in Python?
Only the base64 module, which ships with Python. There is nothing to install with pip.