Skip to content

Chord

chord

Chord parsing, transposition, and detection.

The Chord namespace handles musical chords end-to-end: parse names like "Cmaj7" or "F#dim/A", get the constituent notes and intervals, find related chords or compatible scales, transpose, and detect chords from note lists.

Chord names follow the standard format <tonic><type>[/<bass>]:

  • Tonic: any note name (C, Bb, F#)
  • Type: any chord type from the ChordType dictionary by full name (maj7) or alias (Δ, ^7, M7)
  • Bass: optional slash-bass note (/A)
Example

from tonal_py import Chord Chord.get("Cmaj7").notes ('C', 'E', 'G', 'B') Chord.get("F#dim/A").bass 'A' Chord.detect(["C", "E", "G"]) ['CM', 'Em#5/C']

Source parity: @tonaljs/chord

tokenize_bass

tokenize_bass(note_str: str, chord_str: str) -> tuple[str, str, str]

Internal helper for tokenize: split off /bass if present.

Parameters:

Name Type Description Default
note_str str

The tonic portion already extracted.

required
chord_str str

The remaining chord string (potentially with /bass).

required

Returns:

Type Description
tuple[str, str, str]

(tonic, type, bass) — bass is "" if not present or unparseable.

Example

from tonal_py.chord import tokenize_bass tokenize_bass("C", "maj7") ('C', 'maj7', '') tokenize_bass("C", "maj7/E") ('C', 'maj7', 'E')

Source code in src/tonal_py/chord.py
def tokenize_bass(note_str: str, chord_str: str) -> tuple[str, str, str]:
    """Internal helper for `tokenize`: split off `/bass` if present.

    Args:
        note_str: The tonic portion already extracted.
        chord_str: The remaining chord string (potentially with `/bass`).

    Returns:
        `(tonic, type, bass)` — bass is `""` if not present or unparseable.

    Example:
        >>> from tonal_py.chord import tokenize_bass
        >>> tokenize_bass("C", "maj7")
        ('C', 'maj7', '')
        >>> tokenize_bass("C", "maj7/E")
        ('C', 'maj7', 'E')
    """
    parts = chord_str.split("/")
    if len(parts) == 1:
        return (note_str, parts[0], "")
    letter, acc, oct_str, suffix = _pitch_note.tokenize_note(parts[1])
    if letter != "" and oct_str == "" and suffix == "":
        return (note_str, parts[0], letter + acc)
    return (note_str, chord_str, "")

tokenize

tokenize(name: str) -> tuple[str, str, str]

Parse a chord name into (tonic, type, bass) components.

Special case: "Aug" is interpreted as the chord type "aug", not as the note A with suffix "ug". This mirrors a workaround in tonal.js.

Parameters:

Name Type Description Default
name str

A chord name string.

required

Returns:

Type Description
tuple[str, str, str]

(tonic, type, bass). Any component is "" when not present.

Example

from tonal_py import Chord Chord.tokenize("Cmaj7") ('C', 'maj7', '') Chord.tokenize("F#dim/A") ('F#', 'dim', 'A') Chord.tokenize("Aug") ('', 'aug', '')

Source code in src/tonal_py/chord.py
def tokenize(name: str) -> tuple[str, str, str]:
    """Parse a chord name into `(tonic, type, bass)` components.

    Special case: `"Aug"` is interpreted as the chord type `"aug"`, not as
    the note A with suffix `"ug"`. This mirrors a workaround in tonal.js.

    Args:
        name: A chord name string.

    Returns:
        `(tonic, type, bass)`. Any component is `""` when not present.

    Example:
        >>> from tonal_py import Chord
        >>> Chord.tokenize("Cmaj7")
        ('C', 'maj7', '')
        >>> Chord.tokenize("F#dim/A")
        ('F#', 'dim', 'A')
        >>> Chord.tokenize("Aug")
        ('', 'aug', '')
    """
    letter, acc, oct_str, type_ = _pitch_note.tokenize_note(name)
    if letter == "":
        return tokenize_bass("", name)
    if letter == "A" and type_ == "ug":
        return tokenize_bass("", "aug")
    return tokenize_bass(letter + acc, oct_str + type_)

get_chord

get_chord(type_name: str, optional_tonic: str = '', optional_bass: str = '') -> Chord

Build a Chord from explicit (type, tonic, bass) arguments.

Lower-level than get: skips the chord-string tokenizer and constructs directly from named parts. Useful when you already have parsed components.

Parameters:

Name Type Description Default
type_name str

A chord type name or alias (e.g. "maj7", "Δ").

required
optional_tonic str

The tonic note (e.g. "C"). Empty for a generic chord type without tonic.

''
optional_bass str

An optional slash-bass note (e.g. "E").

''

Returns:

Type Description
Chord

A Chord dataclass, or NO_CHORD if the type or tonic is invalid.

Example

from tonal_py import Chord Chord.get_chord("maj7", "C").notes ('C', 'E', 'G', 'B') Chord.get_chord("maj7", "C", "E").bass 'E'

Source code in src/tonal_py/chord.py
def get_chord(type_name: str, optional_tonic: str = "", optional_bass: str = "") -> Chord:
    """Build a `Chord` from explicit (type, tonic, bass) arguments.

    Lower-level than [`get`][tonal_py.chord.get]: skips the chord-string
    tokenizer and constructs directly from named parts. Useful when you
    already have parsed components.

    Args:
        type_name: A chord type name or alias (e.g. `"maj7"`, `"Δ"`).
        optional_tonic: The tonic note (e.g. `"C"`). Empty for a generic
            chord type without tonic.
        optional_bass: An optional slash-bass note (e.g. `"E"`).

    Returns:
        A `Chord` dataclass, or `NO_CHORD` if the type or tonic is invalid.

    Example:
        >>> from tonal_py import Chord
        >>> Chord.get_chord("maj7", "C").notes
        ('C', 'E', 'G', 'B')
        >>> Chord.get_chord("maj7", "C", "E").bass
        'E'
    """
    type_ = chord_type.get(type_name)
    tonic = _pitch_note.note(optional_tonic or "")
    bass = _pitch_note.note(optional_bass or "")
    if type_.empty or (optional_tonic and tonic.empty) or (optional_bass and bass.empty):
        return NO_CHORD
    bass_interval = _pitch_distance.distance(tonic.pc, bass.pc)
    try:
        bass_index = list(type_.intervals).index(bass_interval)
    except ValueError:
        bass_index = -1
    has_root = bass_index >= 0
    root = bass if has_root else _pitch_note.note("")
    root_degree = bass_index + 1 if bass_index != -1 else 0
    has_bass = bool(bass.pc) and bass.pc != tonic.pc
    intervals = list(type_.intervals)
    if has_root:
        # Rotate intervals so the root sits first; new intervals get +7 added.
        # NB: JS source has a bug for two-digit interval numbers — `intervals[0][0]`
        # only reads one char. We mirror that quirk for parity.
        for _ in range(1, root_degree):
            num_char = intervals[0][0]
            quality = intervals[0][1] if len(intervals[0]) > 1 else ""
            new_num = int(num_char) + 7
            intervals.append(f"{new_num}{quality}")
            intervals.pop(0)
    elif has_bass:
        # Slash chord with non-chord-tone bass: prepend the bass interval - 8P
        ivl = _interval_mod.subtract(_pitch_distance.distance(tonic.pc, bass.pc), "8P")
        if ivl:
            intervals.insert(0, ivl)
    notes_ = (
        ()
        if tonic.empty
        else tuple(_pitch_distance.transpose(tonic.pc, i) for i in intervals)
    )
    if type_name in type_.aliases:
        symbol_type = type_name
    else:
        symbol_type = type_.aliases[0] if type_.aliases else ""
    if has_root and root_degree > 1:
        slash_part = "/" + root.pc
    elif has_bass:
        slash_part = "/" + bass.pc
    else:
        slash_part = ""
    symbol = f"{'' if tonic.empty else tonic.pc}{symbol_type}{slash_part}"
    if has_root and root_degree > 1:
        name_slash = " over " + root.pc
    elif has_bass:
        name_slash = " over " + bass.pc
    else:
        name_slash = ""
    full_name = f"{tonic.pc + ' ' if optional_tonic else ''}{type_.name}{name_slash}"
    return Chord(
        empty=False,
        name=full_name,
        set_num=type_.set_num,
        chroma=type_.chroma,
        normalized=type_.normalized,
        intervals=tuple(intervals),
        quality=type_.quality,
        aliases=type_.aliases,
        symbol=symbol,
        root=root.pc,
        bass=bass.pc if has_bass else "",
        root_degree=root_degree,
        type=type_.name,
        tonic=tonic.pc if not tonic.empty else None,
        notes=notes_,
    )

get

get(src: Any) -> Chord

Get Chord properties from a chord name or pre-tokenized list.

Accepts a chord name string ("Cmaj7", "F#dim/A") or a 3-element list [tonic, type, bass]. Returns NO_CHORD for empty or invalid input.

Parameters:

Name Type Description Default
src Any

A chord name string or [tonic, type, bass] list.

required

Returns:

Type Description
Chord

A Chord dataclass.

Example

from tonal_py import Chord c = Chord.get("Cmaj7") c.notes ('C', 'E', 'G', 'B') c.quality 'Major' c.intervals ('1P', '3M', '5P', '7M') Chord.get("F#dim/A").bass 'A' Chord.get("").empty True

Source code in src/tonal_py/chord.py
def get(src: Any) -> Chord:
    """Get `Chord` properties from a chord name or pre-tokenized list.

    Accepts a chord name string (`"Cmaj7"`, `"F#dim/A"`) or a 3-element list
    `[tonic, type, bass]`. Returns `NO_CHORD` for empty or invalid input.

    Args:
        src: A chord name string or `[tonic, type, bass]` list.

    Returns:
        A `Chord` dataclass.

    Example:
        >>> from tonal_py import Chord
        >>> c = Chord.get("Cmaj7")
        >>> c.notes
        ('C', 'E', 'G', 'B')
        >>> c.quality
        'Major'
        >>> c.intervals
        ('1P', '3M', '5P', '7M')
        >>> Chord.get("F#dim/A").bass
        'A'
        >>> Chord.get("").empty
        True
    """
    if isinstance(src, list):
        type_name = src[1] if len(src) > 1 else ""
        tonic_arg = src[0] if len(src) > 0 else ""
        bass_arg = src[2] if len(src) > 2 else ""
        return get_chord(type_name or "", tonic_arg, bass_arg)
    if src == "":
        return NO_CHORD
    tonic, type_, bass = tokenize(src)
    chord = get_chord(type_, tonic, bass)
    if chord.empty:
        # JS calls `getChord(src)` here — passing the whole string as type_name.
        # If the original string itself is a chord-type name like "maj7", this
        # works; otherwise still returns NO_CHORD.
        return get_chord(src)
    return chord

transpose

transpose(chord_name: str, ivl: str) -> str

Transpose a chord name by an interval.

Both the tonic and the bass (if any) are transposed; the chord type is preserved.

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required
ivl str

An interval string.

required

Returns:

Type Description
str

The transposed chord name. Returns the input unchanged if the

str

chord has no parseable tonic.

Example

from tonal_py import Chord Chord.transpose("Cmaj7", "M3") 'Emaj7' Chord.transpose("F#dim/A", "P5") 'C#dim/E'

Source code in src/tonal_py/chord.py
def transpose(chord_name: str, ivl: str) -> str:
    """Transpose a chord name by an interval.

    Both the tonic and the bass (if any) are transposed; the chord type
    is preserved.

    Args:
        chord_name: A chord name string.
        ivl: An interval string.

    Returns:
        The transposed chord name. Returns the input unchanged if the
        chord has no parseable tonic.

    Example:
        >>> from tonal_py import Chord
        >>> Chord.transpose("Cmaj7", "M3")
        'Emaj7'
        >>> Chord.transpose("F#dim/A", "P5")
        'C#dim/E'
    """
    tonic, type_, bass = tokenize(chord_name)
    if not tonic:
        return chord_name
    tr_bass = _pitch_distance.transpose(bass, ivl)
    slash = "/" + tr_bass if tr_bass else ""
    return _pitch_distance.transpose(tonic, ivl) + type_ + slash

chord_scales

chord_scales(name: str) -> list[str]

Find scales whose notes contain all of this chord's notes.

Parameters:

Name Type Description Default
name str

A chord name string.

required

Returns:

Type Description
list[str]

Scale names whose pitch class set is a superset of the chord's.

Example

from tonal_py import Chord "major" in Chord.chord_scales("Cmaj7") True

Source code in src/tonal_py/chord.py
def chord_scales(name: str) -> list[str]:
    """Find scales whose notes contain all of this chord's notes.

    Args:
        name: A chord name string.

    Returns:
        Scale names whose pitch class set is a superset of the chord's.

    Example:
        >>> from tonal_py import Chord
        >>> "major" in Chord.chord_scales("Cmaj7")
        True
    """
    s = get(name)
    is_chord_included = pcset.is_superset_of(s.chroma)
    return [scale.name for scale in scale_type.all() if is_chord_included(scale.chroma)]

extended

extended(chord_name: str) -> list[str]

Find chords that extend this one (contain all its notes plus more).

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required

Returns:

Type Description
list[str]

Chord names whose pitch class set is a strict superset.

Example

from tonal_py import Chord any("maj9" in c for c in Chord.extended("Cmaj7")) True

Source code in src/tonal_py/chord.py
def extended(chord_name: str) -> list[str]:
    """Find chords that extend this one (contain all its notes plus more).

    Args:
        chord_name: A chord name string.

    Returns:
        Chord names whose pitch class set is a strict superset.

    Example:
        >>> from tonal_py import Chord
        >>> any("maj9" in c for c in Chord.extended("Cmaj7"))
        True
    """
    s = get(chord_name)
    is_super = pcset.is_superset_of(s.chroma)
    out = []
    for c in chord_type.all():
        if is_super(c.chroma) and c.aliases:
            out.append((s.tonic or "") + c.aliases[0])
    return out

reduced

reduced(chord_name: str) -> list[str]

Find simpler chords contained within this one.

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required

Returns:

Type Description
list[str]

Chord names whose pitch class set is a strict subset.

Example

from tonal_py import Chord any(c == "CM" for c in Chord.reduced("Cmaj7")) True

Source code in src/tonal_py/chord.py
def reduced(chord_name: str) -> list[str]:
    """Find simpler chords contained within this one.

    Args:
        chord_name: A chord name string.

    Returns:
        Chord names whose pitch class set is a strict subset.

    Example:
        >>> from tonal_py import Chord
        >>> any(c == "CM" for c in Chord.reduced("Cmaj7"))
        True
    """
    s = get(chord_name)
    is_sub = pcset.is_subset_of(s.chroma)
    out = []
    for c in chord_type.all():
        if is_sub(c.chroma) and c.aliases:
            out.append((s.tonic or "") + c.aliases[0])
    return out

notes

notes(chord_name: str, tonic: str | None = None) -> list[str]

Get the notes of a chord, optionally overriding the tonic.

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required
tonic str | None

Optional tonic override. When provided, transposes the chord type to that tonic instead of using the chord's own.

None

Returns:

Type Description
list[str]

List of note names. Empty if the chord has no resolvable tonic.

Example

from tonal_py import Chord Chord.notes("Cmaj7") ['C', 'E', 'G', 'B'] Chord.notes("maj7", "Eb") ['Eb', 'G', 'Bb', 'D']

Source code in src/tonal_py/chord.py
def notes(chord_name: str, tonic: str | None = None) -> list[str]:
    """Get the notes of a chord, optionally overriding the tonic.

    Args:
        chord_name: A chord name string.
        tonic: Optional tonic override. When provided, transposes the
            chord type to that tonic instead of using the chord's own.

    Returns:
        List of note names. Empty if the chord has no resolvable tonic.

    Example:
        >>> from tonal_py import Chord
        >>> Chord.notes("Cmaj7")
        ['C', 'E', 'G', 'B']
        >>> Chord.notes("maj7", "Eb")
        ['Eb', 'G', 'Bb', 'D']
    """
    chord = get(chord_name)
    note_ = tonic or chord.tonic
    if not note_ or chord.empty:
        return []
    return [_pitch_distance.transpose(note_, i) for i in chord.intervals]

degrees

degrees(chord_name: str, tonic: str | None = None) -> Callable[[int], str]

Return a function mapping 1-based chord degree to a note name.

Degree 0 is invalid (returns ""). Negative degrees count backwards from the chord's last note. The function wraps cyclically through the chord's interval pattern.

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required
tonic str | None

Optional tonic override.

None

Returns:

Type Description
Callable[[int], str]

A function (degree: int) -> str.

Example

from tonal_py import Chord deg = Chord.degrees("Cmaj7") deg(1) 'C' deg(2) 'E' deg(0) ''

Source code in src/tonal_py/chord.py
def degrees(chord_name: str, tonic: str | None = None) -> Callable[[int], str]:
    """Return a function mapping 1-based chord degree to a note name.

    Degree 0 is invalid (returns `""`). Negative degrees count backwards
    from the chord's last note. The function wraps cyclically through the
    chord's interval pattern.

    Args:
        chord_name: A chord name string.
        tonic: Optional tonic override.

    Returns:
        A function `(degree: int) -> str`.

    Example:
        >>> from tonal_py import Chord
        >>> deg = Chord.degrees("Cmaj7")
        >>> deg(1)
        'C'
        >>> deg(2)
        'E'
        >>> deg(0)
        ''
    """
    chord = get(chord_name)
    note_ = tonic or chord.tonic
    transposer = _pitch_distance.tonic_intervals_transposer(list(chord.intervals), note_)

    def fn(degree: int) -> str:
        if degree == 0:
            return ""
        return transposer(degree - 1 if degree > 0 else degree)

    return fn

steps

steps(chord_name: str, tonic: str | None = None) -> Callable[[int], str]

Return a function mapping 0-based step to a note name.

Like degrees but 0-indexed and accepts any integer (wraps cyclically).

Parameters:

Name Type Description Default
chord_name str

A chord name string.

required
tonic str | None

Optional tonic override.

None

Returns:

Type Description
Callable[[int], str]

A function (step: int) -> str.

Example

from tonal_py import Chord step = Chord.steps("Cmaj7") step(0) 'C' step(1) 'E' step(4) # wraps; pitch-class tonic so no octave shift 'C'

Source code in src/tonal_py/chord.py
def steps(chord_name: str, tonic: str | None = None) -> Callable[[int], str]:
    """Return a function mapping 0-based step to a note name.

    Like [`degrees`][tonal_py.chord.degrees] but 0-indexed and accepts any
    integer (wraps cyclically).

    Args:
        chord_name: A chord name string.
        tonic: Optional tonic override.

    Returns:
        A function `(step: int) -> str`.

    Example:
        >>> from tonal_py import Chord
        >>> step = Chord.steps("Cmaj7")
        >>> step(0)
        'C'
        >>> step(1)
        'E'
        >>> step(4)        # wraps; pitch-class tonic so no octave shift
        'C'
    """
    chord = get(chord_name)
    note_ = tonic or chord.tonic
    return _pitch_distance.tonic_intervals_transposer(list(chord.intervals), note_)