Python port of the tonal npm music theory library
Project description
tonal_py
A Python port of the tonal JavaScript music theory library (v6.4.3). Behavior and data tables are translated exactly; identifiers follow Python conventions (snake_case functions, PascalCase namespaces).
If you've used tonal from JavaScript, the API will feel familiar:
// JS
import { Note, Chord, Scale } from "tonal";
Note.midi("C4"); // 60
Chord.get("Cmaj7").notes; // ["C", "E", "G", "B"]
Scale.get("C major").notes; // ["C", "D", "E", "F", "G", "A", "B"]
# Python
from tonal_py import Note, Chord, Scale
Note.midi("C4") # 60
Chord.get("Cmaj7").notes # ('C', 'E', 'G', 'B')
Scale.get("C major").notes # ('C', 'D', 'E', 'F', 'G', 'A', 'B')
Installation
Requires Python 3.13+. The project uses uv for dependency management.
uv add tonal-py # in a uv project
# or
pip install tonal-py # plain pip
For local development:
git clone https://github.com/heyaphra/tonal_py
cd tonal_py
uv sync
uv run pytest -q
Quick start
Notes
from tonal_py import Note
Note.midi("C4") # 60
Note.from_midi(60) # 'C4'
Note.freq("A4") # 440.0
Note.transpose("C4", "M3") # 'E4'
Note.simplify("C##") # 'D'
Note.enharmonic("Db") # 'C#'
Note.names() # ['C', 'D', 'E', 'F', 'G', 'A', 'B']
Intervals
from tonal_py import Interval
Interval.semitones("P5") # 7
Interval.invert("3m") # '6M'
Interval.simplify("9M") # '2M'
Interval.add("3m", "5P") # '7m'
Interval.from_semitones(7) # '5P'
Interval.distance("C4", "G4") # '5P'
Chords
from tonal_py import Chord
cmaj7 = Chord.get("Cmaj7")
cmaj7.notes # ('C', 'E', 'G', 'B')
cmaj7.quality # 'Major'
cmaj7.tonic # 'C'
Chord.transpose("Cmaj7", "M3") # 'Emaj7'
Chord.detect(["C", "E", "G"]) # includes 'CM'
Scales
from tonal_py import Scale
c_major = Scale.get("C major")
c_major.notes # ('C', 'D', 'E', 'F', 'G', 'A', 'B')
c_major.tonic # 'C'
c_major.type # 'major'
Scale.scale_chords("C major") # chords compatible with C major
Modes
from tonal_py import Mode
Mode.get("dorian").triad # 'm'
Mode.notes("dorian", "D") # ['D', 'E', 'F', 'G', 'A', 'B', 'C']
Mode.triads("ionian", "C") # ['C', 'Dm', 'Em', 'F', 'G', 'Am', 'Bdim']
Keys
from tonal_py import Key
c_major = Key.major_key("C")
c_major.chords # ('Cmaj7', 'Dm7', 'Em7', 'Fmaj7', 'G7', 'Am7', 'Bm7b5')
c_major.minor_relative # 'A'
a_minor = Key.minor_key("A")
a_minor.relative_major # 'C'
Roman numerals & progressions
from tonal_py import RomanNumeral, Progression
RomanNumeral.get("V").interval # '5P'
Progression.from_roman_numerals("C", ["I", "IIm7", "V7"]) # ['C', 'Dm7', 'G7']
Progression.to_roman_numerals("C", ["C", "Dm7", "G7"]) # ['I', 'IIm7', 'V7']
Voicings
from tonal_py import Voicing
Voicing.get("Dm7", ["C3", "C5"]) # ['F3', 'A3', 'C4', 'E4']
Rhythm patterns
from tonal_py import RhythmPattern
RhythmPattern.euclid(8, 3) # [1, 0, 0, 1, 0, 0, 1, 0]
RhythmPattern.binary(13) # [1, 1, 0, 1]
MIDI & ranges
from tonal_py import Midi, Range
Midi.midi_to_freq(69) # 440.0
Midi.freq_to_midi(440) # 69
Range.numeric(["C4", "E4"]) # [60, 61, 62, 63, 64]
Range.chromatic(["C4", "E4"]) # ['C4', 'Db4', 'D4', 'Eb4', 'E4']
Available namespaces
Every public namespace from the JS package is exposed as a PascalCase module:
| Namespace | What it does |
|---|---|
AbcNotation |
Convert between ABC and scientific pitch notation |
Array |
Note-array helpers (sorted, unique) |
Chord |
Parse/build chords from symbols |
ChordType |
The 106-entry chord-type dictionary |
Collection |
Generic list helpers (range, rotate, shuffle…) |
Core |
Low-level pitch/interval/distance primitives |
DurationValue |
Note durations (whole, half, dotted…) |
Interval |
Parse/build intervals |
Key |
Major/minor key analysis with chord sets |
Midi |
MIDI ↔ note ↔ frequency conversion |
Mode |
The 7 diatonic modes |
Note |
Parse/build notes |
Pcset |
Pitch-class sets, subsets, modes |
Progression |
Roman-numeral ↔ chord-symbol progressions |
Range |
Numeric and chromatic note ranges |
RhythmPattern |
Euclidean & binary rhythm generation |
RomanNumeral |
Roman-numeral parsing |
Scale |
Build/detect scales from notes |
ScaleType |
The 92-entry scale-type dictionary |
TimeSignature |
Parse time signatures (incl. additive 3+2+3/8) |
VoiceLeading |
Pick voicings to minimize voice movement |
Voicing |
Generate chord voicings within a range |
VoicingDictionary |
Lookup tables of voicings |
The dataclass return types are also re-exported at the top level for type hints:
from tonal_py import Pitch, NO_NOTE, NO_INTERVAL, MajorKey, MinorKey
Naming conventions
- Functions are
snake_case:Note.from_midi,Pcset.is_subset_of. JS'sNote.fromMidibecomesNote.from_midi. - Dataclass fields are
snake_case:set_num,key_signature,root_degree. - Namespaces are
PascalCase, matching the JS exports. - Constants are
UPPER_SNAKE_CASE:CHORDS,SCALES,MODES. - No camelCase aliases — one obvious name per function.
Verification
The port is verified against a JS oracle (tests/fixtures/js_outputs.json) regenerated directly from the original tonal npm package. Every chord type (106), scale type (92), mode, and key is checked row-by-row against the JS output:
uv run pytest -q # 514 tests
uv run pytest tests/test_chord_type.py -v # one module
node tests/fixtures/regen_oracle.mjs > tests/fixtures/js_outputs.json # refresh oracle
A handful of upstream JS quirks are intentionally preserved bit-for-bit (and commented in the source), notably:
- The chord interval-rotation that truncates two-digit numerals during inversions.
- The
Augchord-symbol tokenization special case. - The
Chord.detect([])empty-input path (JS would crash; we add the obvious guard).
Documentation
Full API docs are built with MkDocs Material:
uv sync --group docs
uv run mkdocs serve
A hosted version lives at https://heyaphra.github.io/tonal_py/.
Status
All seven planned phases are complete; the package mirrors the full surface of tonal v6.4.3. See CLAUDE.md for the per-phase build checklist and lessons-learned notes.
Credits
This is a translation of tonaljs/tonal by @danigb and contributors, distributed under the MIT license. All music-theory data tables, parsing logic, and edge-case behavior originate there; only the language differs.
Project details
Release history Release notifications | RSS feed
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 tonal_py-0.1.0.tar.gz.
File metadata
- Download URL: tonal_py-0.1.0.tar.gz
- Upload date:
- Size: 59.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.20 {"installer":{"name":"uv","version":"0.9.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6726a7e084b2317c7a1c2b344989196a8cfef01ad00cbc9240ff1157765ab893
|
|
| MD5 |
ace64360c0f4c24d6922930d8561fa44
|
|
| BLAKE2b-256 |
aacd86c878013ce8d1670e80e222d0695fbc2110ff68793f924dadb962b5af80
|
File details
Details for the file tonal_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tonal_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 75.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.20 {"installer":{"name":"uv","version":"0.9.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97c4f56745b24ed3a37ff543ba3ab9d6779d046ce2f393a5a5f98ae1e1efe9e0
|
|
| MD5 |
8f5321bc3e54f22f618a91f71d7076cd
|
|
| BLAKE2b-256 |
33c24ced2003457fd5a60c797514f2f8bcd49be4dc9ae87a47a5fb4196f01c93
|