odu-core
The 256 Odù Ifá as a canonical byte mapping, with Yorùbá orthography intact.
An Odù figure is two legs of four lines each, and every line carries either one mark or two. That is four bits per leg, eight bits per figure, and exactly 256 figures — a bijection with the byte that needs no padding and loses nothing.
from odu_core import from_byte, encode, decode
from_byte(255).name # 'Èjì Ogbè'
from_byte(0).name # 'Ọ̀yẹ̀kú Méjì'
from_byte(44).name # 'Òtúrúpọ̀n Ìrosùn'
decode(encode(b"hello")) == b"hello" # True
The data is the project
data/principal_odu.json holds the 16 principal Odù and is the single source of
truth. Everything else — all 256 figures, every byte value, every ordering — is
derived from it. data/odu_256.json is the generated artifact other languages
and surfaces import, so nothing re-derives the mapping for itself.
Regenerate after any change to the canonical 16:
python3 scripts/generate.py
Conventions
Four choices fully determine the mapping. Change any one and every byte value means something different, so they are recorded in the data file rather than buried in code:
| Choice | This library |
|---|---|
| Single mark | 1 |
| Double mark | 0 |
| Line order | top to bottom, top line most significant |
| Leg order | right leg is the high nibble |
| Seniority | southwestern Yorùbá (Bascom's predominant order) |
There is no universal digital standard for any of these. Interoperability with
anyone else's work depends on stating them explicitly, which is why
spec_version() exists — encoded data, generated art, and mnemonics are only
meaningful against a known version.
Mnemonic phrases
Bytes become figures, with a checksum figure appended so that a mistyped or transposed figure is caught on decode instead of silently yielding different bytes.
from odu_core.mnemonic import to_phrase, from_phrase, format_phrase
phrase = to_phrase(b"heritage computing")
format_phrase(phrase, "display") # 'Ìwòrì Ọ̀bàrà · Ìwòrì Òfún · …'
from_phrase(phrase) # b'heritage computing'
From the command line:
odu encode --text "Ifá" --style display
odu random --bytes 32 --style numbered
odu decode "ika-odi iwori-iwori irosun-owonrin ose-okanran irosun-ofun"
odu show 44 # describe and draw one figure
odu table # the 16 principal Odù
odu spec # the bit conventions in use
Fingerprinting a file
Four figures you can read down a phone to check two people hold the same file. The last is a checksum, so a mistranscription is caught rather than accepted.
odu fingerprint render.mp4
odu fingerprint *.mp4 --json # one object per file, for other programs
cat render.mp4 | odu fingerprint - # reads standard input
odu fingerprint render.mp4 --check "otura-odi ika-irete oturupon-osa ofun-ogunda"
--check exits 0 on a match and 1 on a mismatch, which is what lets a
Makefile, a backup script or a CI job use this without importing anything:
if odu fingerprint backup.tar --check "$EXPECTED" -q; then
echo "backup intact"
fi
There is a browser version at /verify/ — drop a file on each side and compare them by eye or by ear. It hashes locally and uploads nothing.
This is a truncated SHA-256. At the default three bytes it is 24 bits, so it detects accidents — a truncated download, a file that rotted, the wrong take sent — and not tampering. Anyone who wants two files to share a fingerprint can arrange it in seconds. For that, compare the whole digest.
Slug form (ika-odi) is the canonical written form — ASCII, one token per
figure. Display form keeps full orthography and needs a separator, since figure
names are themselves two words. Both parse back.
A browser demo of the same thing lives in web/:
python3 scripts/build_web.py && python3 -m http.server -d web
Before using this for key material
The checksum detects accidental corruption. It is not authentication, it is not encryption, and it adds no entropy — a phrase reveals exactly the bytes it encodes to anyone holding it.
Each figure carries 8 bits where a BIP-39 word carries 11, so 24 figures are 192 bits against a 24-word BIP-39 phrase's 264. If you are encoding a wallet seed, use BIP-39 — it is specified, audited, and interoperable across wallets. This layer is built for memory, teaching, and art.
Building
make # everything, in dependency order
make test # Python (220) and TypeScript (29) suites
make check # validate content, type-check, report verification coverage
make clean # remove derived artifacts
Everything derives from data/principal_odu.json. The Makefile encodes the
order so it cannot be got wrong by running scripts out of sequence.
TypeScript
ts/ is a thin binding that reads data/odu_256.json rather than
re-deriving the mapping. Two implementations that each compute the table can
drift apart; one that computes and one that reads cannot.
import { fromByte, toPhrase, formatPhrase } from "@odu/core";
fromByte(255).name; // 'Èjì Ogbè'
formatPhrase(toPhrase(new TextEncoder().encode("Ifá")));
Its test suite asserts against ts/test/fixtures/parity.json, generated from
the Python implementation, so the two cannot silently disagree about a name, a
seniority rank, or a checksum.
Source files import with .ts extensions, so Node runs them directly with
native type stripping — no build step for a quick check:
node -e 'import("./ts/src/index.ts").then(m => console.log(m.fromByte(44).name))'
tsc rewrites those to .js on the way into dist/. It does not rewrite
them in declaration files, which would leave dist/*.d.ts pointing at a
./types.ts that isn't there and break every TypeScript consumer — so
npm run build runs scripts/fix-decl-extensions.mjs afterwards, and
test/dist.test.js fails the build if any .ts specifier survives. Build
through npm run build or make, never bare tsc.
Native stripping needs a Node built with Amaro. Distro packages often are not
(node -p 'process.config.variables.node_use_amaro' must print true); an
official nodejs.org or nvm build is.
Knowledge base
Sourced content keyed to the 256 figures.
kb/content/ is the corpus; kb/odu.db is a derived index. The database is
rebuilt from scratch on every ingest and is not version-controlled — whatever
holds the truth must be the thing that gets reviewed and diffed, and a binary
SQLite file fails that.
python3 scripts/ingest.py --check # validate content files, write nothing
python3 scripts/ingest.py # rebuild kb/odu.db
python3 scripts/build_kb_site.py # generate site/ — 256 permalinks + index
Nothing can be stored without a source. source_id is NOT NULL on every
content table, so an unattributed verse does not merely get flagged — it fails
to insert. A corpus that cannot say where a line came from has no value, and a
corpus of living sacred material that cannot say so is worse than empty.
The corpus ships empty, deliberately. No verse in this repository was generated. Filling it means sitting with the sources and entering records one at a time, which is the slow part of the project and the part that cannot be automated.
Publication is default-deny
The site generator reads only the publishable_* views. A record reaches the
public site only if all of these hold:
| Gate | Reason |
|---|---|
status = 'published' |
drafts stay private |
restricted = 0 |
some ẹsẹ Ifá are initiation-restricted |
| source permits reproduction | citable ≠ republishable |
| every contributor still consents | consent is withdrawable, and withdrawal propagates |
Forgetting any one of them hides the record rather than exposing it. Withdrawing a contributor's consent retracts their material from the next build while leaving citation-only records intact — there is an end-to-end test for exactly that.
A source under copyright is still useful: store a page_reference instead of
the text. Attempting to store reproduced text from such a source raises rather
than silently accepting it.
Seniority is not numeric order
Ogbè is the most senior Odù, but its leg is 1111 — byte 255, last numerically.
The two orderings are genuinely different and neither follows from the other by
arithmetic on the byte.
This is why the library indexes by bit pattern, not by rank. Bit patterns are structural and uncontested; seniority varies by lineage and region, so it lives as an attribute that can hold more than one tradition. Adding the Lucumí ordering means adding a field to the JSON, not rewriting the mapping.
from odu_core import by_seniority
by_seniority()[0].name # 'Èjì Ogbè' (byte 255)
Orthography
Yorùbá carries two independent diacritic systems: sub-dots that distinguish letters (ẹ, ọ, ṣ) and tone marks that carry pitch (à, á). Both are meaningful — stripping either produces a different word, not a cosmetic variant.
Canonical data keeps full diacritics and is NFC-normalized (tested). ASCII slugs exist alongside as identifiers only, and the conversion is deliberately one-way.
from odu_core import principal
principal("Ọ̀yẹ̀kú") is principal("oyeku") # True — both resolve
Status and verification
All 16 principal figures are verified — 16 of 16. Checked 2026-07-28
against Bascom, Ifa Divination (1969), Table 1 p. 4 and Table 3
column B p. 48, read via Internet Archive controlled lending. Every bit
pattern and every seniority rank matched; no corrections were needed.
odu verify # exits 0 only when all 16 are verified
python3 scripts/verify_odu.py --status # per-figure detail with citations
--against and --by are both required, so a verification without a source
and a name cannot be recorded. Tests assert that no figure claims verified
without a citation, a checker, and a date.
Seniority is southwestern Yoruba, and that is a choice
Bascom examined 86 lists from 61 sources. The order used here is the
predominant one — 42 of 86 lists, including 30 of 60 Yoruba lists, 5 of 6
Cuban, and the single Brazilian list. But twenty-one other rankings are
recorded (Bascom 1969, pp. 47–48), and Bascom's own study follows a
different one: the Ifẹ variant, kept in alternativeOrders in the canonical
JSON.
The two differ only in the order of two pairs, positions 5–8 and 11–14. Bit patterns are identical in both. This is exactly why the library indexes by bit pattern and treats rank as an attribute — the structure is settled, the ordering is not.
Structural invariants are still enforced by tests: the 16 legs cover every 4-bit value exactly once, and every consecutive seniority pair is a bitwise complement or bit-reversal.
Tests
python3 -m pytest tests/ -q
The mapping is small enough to test exhaustively, so every assertion about the 256 figures checks all 256 rather than sampling.
Authorship
Timeyin Bemi — author and maintainer.
Archived at Zenodo: 10.5281/zenodo.21743991 (this DOI always resolves to the latest version). Please cite Bascom (1969) alongside it — this dataset is a transcription, not a discovery.
The Yorùbá orthography in this dataset is his. That is a substantive contribution rather than a formality: the contracted names carry tone marks because a Yorùbá speaker determined that Bascom's parenthesised letters mark an optional vowel rather than a deleted one, and corrected an earlier reading that had it backwards. Tone is meaning in Yorùbá, and a dataset that dropped it would have been quietly wrong in a way no test could catch.
Built with Claude Code; individual commits carry co-author trailers. The verification against Bascom (1969) was performed through Internet Archive controlled lending on the maintainer's account.
If anything here misrepresents the tradition, that is a defect worth reporting
regardless of what the licence permits — see LICENSE-DATA.md.
License
MIT for the code and the structural mapping. Verse content added later needs separate terms — some published collections are copyrighted, and oral contributions need attribution and consent terms agreed with contributors.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file odu_core-1.0.0.tar.gz.
File metadata
- Download URL: odu_core-1.0.0.tar.gz
- Upload date:
- Size: 135.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d07c2a39480daff7fc896585c8ef902e4181cdbb3de41dc547480e007e7a6d6f
|
|
| MD5 |
da41e73dcc5279c7840f5d79e47df641
|
|
| BLAKE2b-256 |
7d4e7f6641577772f75f6bb76696311929ecd28db3f95586047c9c84bc5b3329
|
File details
Details for the file odu_core-1.0.0-py3-none-any.whl.
File metadata
- Download URL: odu_core-1.0.0-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5d7181c92dc197ffaa49e17349a7d48fb072e32239fc27bc602398ee0add372
|
|
| MD5 |
28f7c3d943ae45b5a36fd560dd4dd54d
|
|
| BLAKE2b-256 |
1316a6e1b90ba951fc03613e071cbacb142663f6413e0d228def4eb8e36847bc
|