Skip to main content

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's Note.fromMidi becomes Note.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 Aug chord-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tonal_py-0.1.0.tar.gz (59.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tonal_py-0.1.0-py3-none-any.whl (75.8 kB view details)

Uploaded Python 3

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

Hashes for tonal_py-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6726a7e084b2317c7a1c2b344989196a8cfef01ad00cbc9240ff1157765ab893
MD5 ace64360c0f4c24d6922930d8561fa44
BLAKE2b-256 aacd86c878013ce8d1670e80e222d0695fbc2110ff68793f924dadb962b5af80

See more details on using hashes here.

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

Hashes for tonal_py-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 97c4f56745b24ed3a37ff543ba3ab9d6779d046ce2f393a5a5f98ae1e1efe9e0
MD5 8f5321bc3e54f22f618a91f71d7076cd
BLAKE2b-256 33c24ced2003457fd5a60c797514f2f8bcd49be4dc9ae87a47a5fb4196f01c93

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page