---
title: "Generate a BIP39 Seed Phrase in Python: Code and Test Vectors"
url: "https://bip39-phrase.com/seed-phrase-generation-using-python/"
language: "en"
published: "2025-02-28T00:00:00+00:00"
updated: "2026-10-16T00:00:00+00:00"
---

[ Developers ](/topics/developers/) Published February 28, 2025 Updated October 16, 2026 

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

![Generate a BIP39 Seed Phrase in Python: Code and Test Vectors](/wp-content/uploads/2025/02/seed-phrase-generation-using-python-hero.webp) 

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](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki), 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 bits | Words |
| -------------- | --------------- | ---------- | ----- |
| 128            | 4               | 132        | 12    |
| 160            | 5               | 165        | 15    |
| 192            | 6               | 198        | 18    |
| 224            | 7               | 231        | 21    |
| 256            | 8               | 264        | 24    |

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](/can-you-guess-a-seed-phrase-probability-and-real-risks/): 128 bits for 12 words, 256 bits for 24\. To watch these steps on a live example, bit by bit, open our [BIP39 visualizer](/how-bip39-works/).

## Before You Start

You need Python 3.9 or newer; check with `python3 --version`, and get an installer from [python.org](https://www.python.org/downloads/) if needed. Then download the official English wordlist, `english.txt`, from the [bitcoin/bips repository](https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt) as a raw file and confirm that it hasn’t been altered:

```bash
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`:

```python
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](/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](/bip39-passphrase/) creates a separate wallet.
- **`seed_to_xprv`** is the first step of [BIP32](/bip-32/): 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](https://github.com/trezor/python-mnemonic/blob/master/vectors.json); every entry uses the passphrase `TREZOR`. Three of the English entries:

| Entropy                          | Mnemonic                                                                    |
| -------------------------------- | --------------------------------------------------------------------------- |
| 00000000000000000000000000000000 | abandon × 11, then about                                                    |
| 7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f | legal winner thank year wave sausage worth useful legal winner thank yellow |
| ffffffffffffffffffffffffffffffff | zoo × 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:

```python
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](https://github.com/trezor/python-mnemonic) as its reference implementation, and it supports all the official wordlists. After `pip install mnemonic` (we tested version 0.21):

```python
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](/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](/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](/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](/topics/developers/).

[ Back to Blog ](/blog/)
