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 asNote. - Serialization uses
dataclasses.asdict()instead ofJSON.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_pyis server/library only. Nobrowser/tonal.min.jsequivalent. - 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_pywithmidoorpython-rtmidiif 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_case → camelCase, set_num → setNum, frozen
dataclasses → plain objects, None → undefined for optional fields.