Skip to content

Comparison with tonal.js

tonal_py is a behavior-faithful port of tonal.js v6.4.3. This page calls out the places where they differ — useful when reading the upstream JS docs or porting code.

Identifier casing

The largest visible change. See Naming Conventions for the exact mapping.

Type model

JS uses plain object literals; we use frozen dataclasses (@dataclass(frozen=True, slots=True)). This means:

  • All result objects are immutable — you can't accidentally mutate a Note.
  • Equality is structural and free (Note.get("C4") == Note.get("C4")).
  • Type hints work: Note.get("C4") is statically typed as Note.
  • Serialization uses dataclasses.asdict() instead of JSON.stringify.

Sentinel encoding

JS NoNote has step: NaN, alt: NaN, midi: null, .... JSON.stringify produces null for both NaN and null, but JS distinguishes them (undefined keys get dropped, null is emitted).

In Python: - We use math.nan for fields JS marks NaN. - We use None for fields JS marks null. - For oct (which is undefined in JS for pitch classes — and JSON drops it), we use None in Python and document that it serializes as null, not as a missing key.

The test helper to_js_dict handles this asymmetry for oracle comparisons.

Mutable global state

ChordType and ScaleType use a module-level mutable dictionary that add() extends and remove_all() clears — same as JS. Tests that mutate must restore via the private _seed() function (see tests/test_chord_type.py for a try/finally pattern).

Tonal.js bugs preserved

These are JS quirks that tonal_py mirrors verbatim — typically with a comment in the source explaining what's happening and why we don't "fix" it.

chord interval-rotation reads single chars

When building inverted chord intervals, JS reads intervals[0][0] and intervals[0][1] — single characters. For two-digit interval numbers ("13M", "11A") this truncates. We preserve the truncation for output parity.

chord-detect note.length === 0

The detect function checks note.length === 0 to early-return on empty input. But note here is the imported function — .length is its arity, never 0. The check never fires. We add an explicit if not notes: return [] guard so Python doesn't crash on Chord.detect([]).

"Aug" tokenization

Chord.tokenize("Aug") is special-cased to mean the chord type "aug", not the note A with suffix "ug". Mirrored exactly.

TimeSignature crash on garbage

T.TimeSignature.get("garbage") throws a TypeError in JS (calls .split on undefined). Our port returns NO_TIME_SIGNATURE. Strictly speaking this is a divergence — but a crash isn't an API contract.

VoicingDictionary.lookup returns undefined

JS returns undefined for misses. We return None. Same effective behavior.

Interval coordinates are 2-element, not 3

Although IntervalCoordinates is typed as [Fifths, Octaves, Direction] in TypeScript, the implementation only emits 2 elements. JS array destructuring under-fills with undefined; Python tuple unpacking would raise. We slice to expected length where needed.

Random number generation

JS's Math.random is replaced by Python's random.random as the default. Functions that take an optional rnd callable (shuffle, RhythmPattern.random, RhythmPattern.probability) accept any Callable[[], float].

>>> from tonal_py import RhythmPattern
>>> seq = iter([0.9, 0.1, 0.9, 0.1])
>>> RhythmPattern.random(4, 0.5, rnd=lambda: next(seq))
[1, 0, 1, 0]

This is the recommended way to make randomness deterministic in tests.

What's NOT included

  • Browser bundle. tonal_py is server/library only. No browser/tonal.min.js equivalent.
  • Performance parity with V8. Python is slower for raw arithmetic; we don't optimize beyond module-level dict caches.
  • Streaming / MIDI playback. Tonal itself doesn't ship that, neither do we. Pair tonal_py with mido or python-rtmidi if you need it.
  • Async APIs. Everything is synchronous (matches JS).

Going the other way

If you write tonal_py code and want to port it to JavaScript, the rules just reverse: snake_casecamelCase, set_numsetNum, frozen dataclasses → plain objects, Noneundefined for optional fields.