Developers Published Updated

Generate a BIP39 Seed Phrase in Python: Code and Test Vectors

#bip39#seed phrase#mnemonic#wallet#recovery
Generate a BIP39 Seed Phrase in Python: Code and Test Vectors

To generate a BIP39 seed phrase in Python, draw 128 to 256 random bits from the secrets module, append the first bits of their SHA-256 hash as a checksum, split the result into 11-bit numbers and look each one up in the official 2,048-word list. This guide builds a complete BIP39 implementation in Python using only the standard library: it generates a mnemonic from secure randomness, validates the checksum of an existing phrase, derives the 512-bit seed and turns that seed into a BIP32 master key. The code below passes all 24 English test vectors published for BIP39, and the last section shows how to run that check yourself. Use it to learn and to test; a phrase that will hold real funds belongs in a hardware wallet or well-established wallet software.

  • Randomness: secrets.token_bytes(32) gives 256 bits, which become 24 words; secrets.token_bytes(16) gives 12.
  • Checksum: the first ENT/32 bits of the entropy’s SHA-256 hash, 4 to 8 bits depending on length.
  • Words: each 11-bit group is an index from 0 to 2047 into english.txt, whose SHA-256 the script verifies.
  • Seed: PBKDF2-HMAC-SHA512 with 2,048 iterations and the salt "mnemonic" plus an optional passphrase.
  • Proof: the official test vectors, run at the end, must all pass.

How BIP39 Turns Randomness into Words

BIP39 starts with entropy, a random number of 128 to 256 bits, in multiples of 32. It appends a checksum made of the first ENT/32 bits of the entropy’s SHA-256 hash, splits the result into 11-bit groups and uses each group as an index into a list of 2,048 words. A separate step turns the words into a seed: PBKDF2 with HMAC-SHA512, 2,048 iterations, the phrase as the password and "mnemonic" plus an optional passphrase as the salt, both normalized to Unicode NFKD.

Entropy (bits)Checksum (bits)Total bitsWords
128413212
160516515
192619818
224723121
256826424

A worked example: for 128 zero bits, the SHA-256 hash of the entropy begins with the byte 0x37 (binary 0011 0111), so the 4-bit checksum is 0011. The first eleven 11-bit groups are all zeros, index 0 in the list, abandon. The twelfth is seven zero bits followed by 0011, which is 3, the word about. That is why the best-known test phrase is abandon eleven times followed by about.

The checksum makes most mistyped phrases invalid, while all the security comes from the entropy: 128 bits for 12 words, 256 bits for 24. To watch these steps on a live example, bit by bit, open our BIP39 visualizer.

Before You Start

You need Python 3.9 or newer; check with python3 --version, and get an installer from python.org if needed. Then download the official English wordlist, english.txt, from the bitcoin/bips repository as a raw file and confirm that it hasn’t been altered:

curl -O https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt
shasum -a 256 english.txt     # macOS; use sha256sum on Linux
# 2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda  english.txt

A modified list would produce phrases no other wallet can restore, so the script checks this hash as well.

The Code

Save this as bip39_demo.py next to english.txt:

import hashlib
import hmac
import secrets
import unicodedata
from pathlib import Path

# SHA-256 of bip-0039/english.txt in the bitcoin/bips repository
WORDLIST_SHA256 = "2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda"


def load_wordlist(path: str = "english.txt") -> list[str]:
    data = Path(path).read_bytes()
    if hashlib.sha256(data).hexdigest() != WORDLIST_SHA256:
        raise ValueError("english.txt is not the official BIP39 English wordlist")
    words = data.decode("utf-8").split()
    assert len(words) == 2048
    return words


def entropy_to_mnemonic(entropy: bytes, wordlist: list[str]) -> str:
    if len(entropy) not in (16, 20, 24, 28, 32):
        raise ValueError("entropy must be 128, 160, 192, 224 or 256 bits")
    ent_bits = len(entropy) * 8
    cs_bits = ent_bits // 32                      # 4 to 8 checksum bits
    checksum = hashlib.sha256(entropy).digest()[0] >> (8 - cs_bits)
    value = (int.from_bytes(entropy, "big") << cs_bits) | checksum
    n_words = (ent_bits + cs_bits) // 11
    indexes = [(value >> (11 * i)) & 0x7FF for i in reversed(range(n_words))]
    return " ".join(wordlist[i] for i in indexes)


def generate_mnemonic(wordlist: list[str], strength: int = 256) -> str:
    return entropy_to_mnemonic(secrets.token_bytes(strength // 8), wordlist)


def mnemonic_to_entropy(mnemonic: str, wordlist: list[str]) -> bytes:
    words = unicodedata.normalize("NFKD", mnemonic).split()
    if len(words) not in (12, 15, 18, 21, 24):
        raise ValueError("a BIP39 mnemonic has 12, 15, 18, 21 or 24 words")
    index = {word: i for i, word in enumerate(wordlist)}
    value = 0
    for word in words:
        if word not in index:
            raise ValueError(f"not in the BIP39 wordlist: {word!r}")
        value = (value << 11) | index[word]
    cs_bits = len(words) * 11 // 33
    entropy = (value >> cs_bits).to_bytes(len(words) * 4 // 3, "big")
    expected = hashlib.sha256(entropy).digest()[0] >> (8 - cs_bits)
    if value & ((1 << cs_bits) - 1) != expected:
        raise ValueError("checksum mismatch: a word is wrong or out of order")
    return entropy


def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes:
    words = " ".join(unicodedata.normalize("NFKD", mnemonic).split())
    salt = "mnemonic" + unicodedata.normalize("NFKD", passphrase)
    return hashlib.pbkdf2_hmac("sha512", words.encode("utf-8"),
                               salt.encode("utf-8"), 2048)


B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def seed_to_xprv(seed: bytes) -> str:
    """BIP32 master private key, serialized for Bitcoin mainnet."""
    digest = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
    key, chain_code = digest[:32], digest[32:]
    payload = (bytes.fromhex("0488ADE4") + bytes(9)   # version, depth, parent, index
               + chain_code + b"\x00" + key)
    payload += hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
    n, out = int.from_bytes(payload, "big"), ""
    while n:
        n, r = divmod(n, 58)
        out = B58[r] + out
    return out


if __name__ == "__main__":
    wordlist = load_wordlist()
    mnemonic = generate_mnemonic(wordlist)       # 24 words
    mnemonic_to_entropy(mnemonic, wordlist)      # raises ValueError if anything is off
    seed = mnemonic_to_seed(mnemonic, passphrase="")
    print(mnemonic)
    print("seed:", seed.hex())

What each function does:

  • entropy_to_mnemonic computes the checksum, appends it to the entropy and reads the bits back 11 at a time as word indexes.
  • generate_mnemonic draws the entropy from secrets.token_bytes, which uses the operating system’s secure random generator. Python’s documentation warns that the random module must not be used for security purposes.
  • mnemonic_to_entropy reverses the process and raises an error for an unknown word, a wrong word count or a failed checksum, the same test our BIP39 checksum checker runs in the browser. About 1 in 16 random 12-word sequences, and 1 in 256 random 24-word ones, still passes by chance, so a valid checksum doesn’t prove that the words are the ones you wrote down.
  • mnemonic_to_seed applies PBKDF2 exactly as BIP39 specifies. Any passphrase gives a valid but different seed, which is how a BIP39 passphrase creates a separate wallet.
  • seed_to_xprv is the first step of BIP32: HMAC-SHA512 keyed with "Bitcoin seed", serialized as an xprv string. Every account and address key is derived from it.

Check It Against the Official Test Vectors

The BIP39 test vectors live in vectors.json in the python-mnemonic repository; every entry uses the passphrase TREZOR. Three of the English entries:

EntropyMnemonic
00000000000000000000000000000000abandon × 11, then about
7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7flegal winner thank year wave sausage worth useful legal winner thank yellow
ffffffffffffffffffffffffffffffffzoo × 11, then wrong

Download the raw file next to the script (curl -O https://raw.githubusercontent.com/trezor/python-mnemonic/master/vectors.json) and run:

import json
import bip39_demo as b

wordlist = b.load_wordlist("english.txt")
with open("vectors.json") as f:
    vectors = json.load(f)["english"]

for entropy_hex, mnemonic, seed_hex, xprv in vectors:
    entropy = bytes.fromhex(entropy_hex)
    assert b.entropy_to_mnemonic(entropy, wordlist) == mnemonic
    assert b.mnemonic_to_entropy(mnemonic, wordlist) == entropy
    seed = b.mnemonic_to_seed(mnemonic, passphrase="TREZOR")
    assert seed.hex() == seed_hex
    assert b.seed_to_xprv(seed) == xprv

print(f"all {len(vectors)} English test vectors pass")

On Python 3.14 it prints all 24 English test vectors pass. For the first vector, the seed begins c55257c360c07c72.

Using the Reference Library Instead

BIP39 names python-mnemonic as its reference implementation, and it supports all the official wordlists. After pip install mnemonic (we tested version 0.21):

from mnemonic import Mnemonic

mnemo = Mnemonic("english")
words = mnemo.generate(strength=256)          # 24 words
assert mnemo.check(words)                     # wordlist and checksum
seed = mnemo.to_seed(words, passphrase="")    # 64-byte seed
xprv = Mnemonic.to_hd_master_key(seed)        # BIP32 master key

It agrees with the code above on every English test vector.

Security Rules for Real Use

  • Generate offline. Run the script on a computer that is not connected to the network, and don’t fund a phrase that has been printed on an everyday machine.
  • Keep phrases out of logs, notebooks and the clipboard. Jupyter saves cell output in the notebook file, and terminal scrollback or log files can keep what you print.
  • Never pick the words yourself. Human choices have far less entropy than secrets (why you should never create your own seed phrase).
  • Store the result on paper or metal, as described in how to store a seed phrase.

Frequently Asked Questions

Why use secrets instead of random?

random is a deterministic generator designed for modelling and simulation, and Python’s documentation says it should not be used for security. secrets draws from the operating system’s cryptographically secure source.

Will a phrase from this script work in my wallet?

Yes, if the wallet supports BIP39: the words and the seed follow the standard exactly. Which addresses you see then depends on the wallet’s derivation path and on whether you add a passphrase.

Can I use the other BIP39 languages?

The algorithm is the same, but you need the matching wordlist from the bitcoin/bips repository, and the wordlist notes require Japanese phrases to be shown with ideographic spaces (U+3000) between words. The mnemonic library handles these details for all official lists. More code and reference material for developers is collected in our developer guides.