Functional Weave
Code in Python

encoding.base64

Bytes to base64 and base64url text and back (RFC 4648), with strict decoding, identically in every language.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 46 tests, run in TypeScript, Python and Rust.base64Encode 12 · base64Decode 14 · base64UrlEncode 9 · base64UrlDecode 11

What it does

Base64 from RFC 4648: `base64Encode` / `base64Decode` use the standard alphabet (`+`, `/`) with `=` padding, and `base64UrlEncode` / `base64UrlDecode` use the URL- and filename-safe alphabet (`-`, `_`) of section 5, without padding, which is the form JWTs (RFC 7515 section 2) and most URL tokens use. The four ship as one group because the two alphabets share one codec; install only what you call with `only=`.

Bytes are lists of integers 0 to 255, as everywhere in the registry (see `encoding.hex`). Python callers can pass a `bytes` value directly.

The functions

A group: 4 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.

  1. base64_encode (bytes: int[]) -> string
  2. base64_decode (text: string) -> int[]
  3. base64_url_encode (bytes: int[]) -> string
  4. base64_url_decode (text: string) -> int[]

Once installed, your code imports each one from the group's module.

base64_encode throws on bad input 12 tests

def base64_encode(bytes: Sequence[int]) -> str
bytesint[]each an integer from 0 to 255
returnsstringstandard alphabet (+ and /), padded with = to a multiple of 4

For example

  • base64_encode() → RFC 4648 section 10: the empty string
  • base64_encode(102) → Zg== RFC 4648 section 10: "f", two padding characters
  • base64_encode(102, 111) → Zm8= RFC 4648 section 10: "fo", one padding character
from fune.encoding.base64 import base64_encode  # encoding.base64@^1
impl/python/base64_encode.py · 29 lines · open · raw
from typing import Sequence

_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


def base64_encode(bytes: Sequence[int]) -> str:
    """Bytes as standard base64 (RFC 4648 section 4), padded with "=".

    A Python ``bytes`` value is accepted as is; a list must hold integers
    0-255, and anything else is refused rather than masked to a byte.
    """
    if isinstance(bytes, (str, dict)) or not hasattr(bytes, "__len__"):
        raise TypeError("bytes must be a list of integers from 0 to 255")
    for b in bytes:
        if type(b) is not int or b < 0 or b > 255:
            raise ValueError("bytes must be a list of integers from 0 to 255")
    out = []
    n_full = len(bytes) - len(bytes) % 3
    for i in range(0, n_full, 3):
        n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
        out.append(_ALPHABET[(n >> 18) & 63] + _ALPHABET[(n >> 12) & 63] + _ALPHABET[(n >> 6) & 63] + _ALPHABET[n & 63])
    rest = len(bytes) - n_full
    if rest == 1:
        n = bytes[n_full] << 16
        out.append(_ALPHABET[(n >> 18) & 63] + _ALPHABET[(n >> 12) & 63] + "==")
    elif rest == 2:
        n = (bytes[n_full] << 16) | (bytes[n_full + 1] << 8)
        out.append(_ALPHABET[(n >> 18) & 63] + _ALPHABET[(n >> 12) & 63] + _ALPHABET[(n >> 6) & 63] + "=")
    return "".join(out)

base64_decode throws on bad input 14 tests

def base64_decode(text: str) -> List[int]
textstringstandard alphabet, padded; no whitespace or line breaks
returnsint[]

For example

  • base64_decode() → the empty string
  • base64_decode(Zg==) → 102 RFC 4648 section 10: "Zg==" is "f"
  • base64_decode(Zm8=) → 102, 111 RFC 4648 section 10: "Zm8=" is "fo"
from fune.encoding.base64 import base64_decode  # encoding.base64@^1
impl/python/base64_decode.py · 59 lines · open · raw
from typing import List


def _sextet(ch: str) -> int:
    # Character ranges rather than str.isalnum(), which is true for letters
    # and digits of every script.
    if "A" <= ch <= "Z":
        return ord(ch) - 65
    if "a" <= ch <= "z":
        return ord(ch) - 71
    if "0" <= ch <= "9":
        return ord(ch) + 4
    if ch == "+":
        return 62
    if ch == "/":
        return 63
    return -1


def base64_decode(text: str) -> List[int]:
    """Standard, padded base64 back to bytes, strictly.

    No whitespace, no characters outside the alphabet, padding to a multiple
    of four, and zero bits after the last byte, so every byte string has
    exactly one spelling. The standard library's decoder skips or accepts
    some of these, which is why this is written out.
    """
    if not isinstance(text, str):
        raise TypeError("base64 text must be a string")
    values = []
    padding = 0
    for ch in text:
        if ch == "=":
            padding += 1
            continue
        v = _sextet(ch)
        if v < 0 or padding > 0:
            raise ValueError("base64 text may only contain A-Z, a-z, 0-9, + and /, with = padding at the end")
        values.append(v)
    if len(text) % 4 != 0:
        raise ValueError("base64 text must be a multiple of 4 characters long, received %d" % len(text))
    if padding > 2:
        raise ValueError("base64 text has too much = padding")
    out = []
    n_full = len(values) - len(values) % 4
    for i in range(0, n_full, 4):
        n = (values[i] << 18) | (values[i + 1] << 12) | (values[i + 2] << 6) | values[i + 3]
        out.extend(((n >> 16) & 255, (n >> 8) & 255, n & 255))
    rest = len(values) - n_full
    if rest == 2:
        if values[n_full + 1] & 15:
            raise ValueError("base64 text has non-zero bits after its last byte")
        out.append(((values[n_full] << 2) | (values[n_full + 1] >> 4)) & 255)
    elif rest == 3:
        if values[n_full + 2] & 3:
            raise ValueError("base64 text has non-zero bits after its last byte")
        n = (values[n_full] << 18) | (values[n_full + 1] << 12) | (values[n_full + 2] << 6)
        out.extend(((n >> 16) & 255, (n >> 8) & 255))
    return out

base64_url_encode throws on bad input 9 tests

def base64_url_encode(bytes: Sequence[int]) -> str
bytesint[]each an integer from 0 to 255
returnsstringURL-safe alphabet (- and _), no padding, as JWTs use

For example

  • base64_url_encode() → the empty string
  • base64_url_encode(102) → Zg "f" without its padding
  • base64_url_encode(102, 111) → Zm8 "fo" without its padding
from fune.encoding.base64 import base64_url_encode  # encoding.base64@^1
impl/python/base64_url_encode.py · 8 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Sequence

from .encoding_base64_base64_encode import base64_encode  ← base64Encode, another function of this group · built into the same file, even by a slim install


def base64_url_encode(bytes: Sequence[int]) -> str:
    """Bytes as base64url (RFC 4648 section 5) without padding, as JWTs use it."""
    return base64_encode(bytes).rstrip("=").replace("+", "-").replace("/", "_")

base64_url_decode throws on bad input 11 tests

def base64_url_decode(text: str) -> List[int]
textstringURL-safe alphabet, with or without correct = padding
returnsint[]

For example

  • base64_url_decode() → the empty string
  • base64_url_decode(Zg) → 102 unpadded, as a JWT writes it
  • base64_url_decode(Zg==) → 102 correct padding is accepted too
from fune.encoding.base64 import base64_url_decode  # encoding.base64@^1
impl/python/base64_url_decode.py · 29 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import List

from .encoding_base64_base64_decode import base64_decode  ← base64Decode, another function of this group · built into the same file, even by a slim install


def base64_url_decode(text: str) -> List[int]:
    """base64url text back to bytes, with or without correct padding."""
    if not isinstance(text, str):
        raise TypeError("base64url text must be a string")
    body = []
    padding = 0
    for ch in text:
        if ch == "=":
            padding += 1
            continue
        ok = ("A" <= ch <= "Z") or ("a" <= ch <= "z") or ("0" <= ch <= "9") or ch == "-" or ch == "_"
        if not ok or padding > 0:
            raise ValueError(
                "base64url text may only contain A-Z, a-z, 0-9, - and _, with optional = padding at the end"
            )
        body.append("+" if ch == "-" else "/" if ch == "_" else ch)
    if len(body) % 4 == 1:
        raise ValueError(
            "base64url text cannot be %d characters long; no number of bytes encodes to that" % len(body)
        )
    needed = (4 - len(body) % 4) % 4
    if padding != 0 and padding != needed:
        raise ValueError("base64url text has the wrong amount of = padding")
    return base64_decode("".join(body) + "=" * needed)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add encoding.base64

That builds the whole group. To build only what you call, and whatever it uses inside the group:

fune add encoding.base64 --only base64Encode
Download for Python encoding.base64-1.0.0-python.fune · 18,514 bytes sha256 58e9c16c1f6c3615f3f6afb9ee61f3f8886ab25bf92843c8c4bfdf62686cb8a7

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./encoding.base64-1.0.0-python.fune, or fetch it from a terminal with fune pull encoding.base64@1.0.0:python.

The whole function, every language, is one file too: encoding.base64-1.0.0.fune, 31,225 bytes, sha256 9dc42678ee52da6f5471c4c64d22da5ce75f681e734cea0ad5dd0ce6a188c403. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before encoding.base64.base64Encode
# fune: before encoding.base64.base64Decode
# fune: before encoding.base64.base64UrlEncode
# fune: before encoding.base64.base64UrlDecode

after — your function gets the result and the arguments, and returns the final result.

# fune: after encoding.base64.base64Encode
# fune: after encoding.base64.base64Decode
# fune: after encoding.base64.base64UrlEncode
# fune: after encoding.base64.base64UrlDecode

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show encoding.base64 --steps.

# fune: step encoding.base64.<fn> after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

base64Encode 12 tests

CaseArgumentsExpected
RFC 4648 section 10: the empty string →
RFC 4648 section 10: "f", two padding characters 102 → Zg==
RFC 4648 section 10: "fo", one padding character 102, 111 → Zm8=
RFC 4648 section 10: "foo", no padding 102, 111, 111 → Zm9v
RFC 4648 section 10: "foob" 102, 111, 111, 98 → Zm9vYg==
RFC 4648 section 10: "fooba" 102, 111, 111, 98, 97 → Zm9vYmE=
RFC 4648 section 10: "foobar" 102, 111, 111, 98, 97, 114 → Zm9vYmFy
high bytes use the + and / characters of the standard alphabet 251, 255, 191 → +/+/
zero bytes are A, not dropped 0, 0, 0, 0 → AAAAAA==
256 is not a byte 256 → error: bytes must be a list of integers from 0 to 255
Show the other 2 tests
CaseArgumentsExpected
a fraction is not a byte 1.5 → error: bytes must be a list of integers from 0 to 255
a string inside the list is not a byte f → error: bytes must be a list of integers from 0 to 255

base64Decode 14 tests

CaseArgumentsExpected
the empty string →
RFC 4648 section 10: "Zg==" is "f" Zg== → 102
RFC 4648 section 10: "Zm8=" is "fo" Zm8= → 102, 111
RFC 4648 section 10: "Zm9vYg==" is "foob" Zm9vYg== → 102, 111, 111, 98
RFC 4648 section 10: "Zm9vYmFy" is "foobar" Zm9vYmFy → 102, 111, 111, 98, 97, 114
the + and / characters +/+/ → 251, 255, 191
missing padding is refused in standard base64 Zg → error: base64 text must be a multiple of 4 characters long, received 2
non-zero bits after the last byte, which a lenient decoder reads as "f" Zh== → error: base64 text has non-zero bits after its last byte
non-zero bits after the last of two bytes Zm9= → error: base64 text has non-zero bits after its last byte
three padding characters Z=== → error: base64 text has too much = padding
Show the other 4 tests
CaseArgumentsExpected
padding in the middle Zg==Zg== → error: base64 text may only contain A-Z, a-z, 0-9, + and /, with = padding at the end
a trailing newline is not skipped Zm9v → error: base64 text may only contain A-Z, a-z, 0-9, + and /, with = padding at the end
base64url characters in standard text -_-_ → error: base64 text may only contain A-Z, a-z, 0-9, + and /, with = padding at the end
a non-ASCII letter Zm9é → error: base64 text may only contain A-Z, a-z, 0-9, + and /, with = padding at the end

base64UrlEncode 9 tests

CaseArgumentsExpected
the empty string →
"f" without its padding 102 → Zg
"fo" without its padding 102, 111 → Zm8
"foobar" needs no padding in either alphabet 102, 111, 111, 98, 97, 114 → Zm9vYmFy
- and _ replace + and / 251, 255, 191 → -_-_
- and _ with padding removed 251, 255 → -_8
RFC 7515 appendix A.1: the example JWS header 123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125 → eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
300 is not a byte 300 → error: bytes must be a list of integers from 0 to 255
a negative value is not a byte -5 → error: bytes must be a list of integers from 0 to 255

base64UrlDecode 11 tests

CaseArgumentsExpected
the empty string →
unpadded, as a JWT writes it Zg → 102
correct padding is accepted too Zg== → 102
- and _ -_-_ → 251, 255, 191
- and _ in an unpadded tail -_8 → 251, 255
RFC 7515 appendix A.1: the example JWS header eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 → 123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125
standard-alphabet characters are refused +/+/ → error: base64url text may only contain A-Z, a-z, 0-9, - and _, with optional = padding at the end
one character past a multiple of four encodes nothing Zm9vY → error: base64url text cannot be 5 characters long; no number of bytes encodes to that
padding that is present must be complete Zg= → error: base64url text has the wrong amount of = padding
non-zero bits after the last byte Zh → error: base64 text has non-zero bits after its last byte
Show the other 1 test
CaseArgumentsExpected
a space inside the text Zm9v YmFy → error: base64url text may only contain A-Z, a-z, 0-9, - and _, with optional = padding at the end

More from the author

**Decoding is strict.** Line breaks, spaces and characters outside the alphabet are errors, not skipped (RFC 4648 section 3.3 says to reject them unless a specification says otherwise). Standard base64 must be padded to a multiple of four characters. The unused bits in the last character must be zero: `"Zh=="` is refused even though a lenient decoder reads it as `"f"`, because accepting it would give one byte string several spellings, which matters when the text is compared or signed (section 3.5). The URL-safe decoder accepts text with or without padding, but padding that is present has to be right, and a length that no byte count produces (one character past a multiple of four) is an error.

Mixing alphabets is an error in both directions: `+` or `/` in base64url text, and `-` or `_` in standard base64 text. That is almost always a token pasted into the wrong field.

Source: RFC 4648, The Base16, Base32, and Base64 Data Encodings, sections 4, 5, 3.3, 3.5, and the test vectors in section 10 (https://www.rfc-editor.org/rfc/rfc4648). The base64url example of a JWS header is from RFC 7515 appendix A.1.

Files

PathBytes
README.md1,692
impl/python/base64_decode.py2,204
impl/python/base64_encode.py1,327
impl/python/base64_url_decode.py1,153
impl/python/base64_url_encode.py303
impl/rust/base64_decode.rs2,441
impl/rust/base64_encode.rs1,603
impl/rust/base64_url_decode.rs1,684
impl/rust/base64_url_encode.rs942
impl/typescript/base64_decode.ts2,059
impl/typescript/base64_encode.ts1,357
impl/typescript/base64_url_decode.ts1,322
impl/typescript/base64_url_encode.ts523
vectors.json6,687