Skip to content

Scale

scale

Scale construction, detection, and analysis.

The Scale namespace builds scales from a tonic and type, detects scales from notes, and answers questions like "what chords fit this scale?", "what scales contain this scale?", and "what are the modes of this scale?".

Scale names follow the format <tonic> <type>, where:

  • Tonic is any note name (C, Bb, F#)
  • Type is any scale type from the ScaleType dictionary (major, dorian, harmonic minor, messiaen's mode #3...)

Either part can be omitted: "C" is just the tonic with no type, "major" is the type with no tonic.

Example

from tonal_py import Scale Scale.get("C major").notes ('C', 'D', 'E', 'F', 'G', 'A', 'B') Scale.detect(["C", "D", "E", "F", "G", "A", "B"], {"match": "exact"}) ['C major']

Source parity: @tonaljs/scale

ScaleDetectOptions

Bases: TypedDict

Options for detect.

Attributes:

Name Type Description
tonic str

Override the inferred tonic (which defaults to the first note in the input). Useful when the first note isn't actually the tonal center.

match str

Either "exact" (only return exact matches) or any other value / omitted (also return scales whose pitch class set is a superset).

tokenize

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

Split a scale string into (tonic, type).

Either component may be empty: - "C major" -> ("C", "major") - "major" -> ("", "major") - "C" -> ("C", "") - garbage -> ("", "garbage")

Parameters:

Name Type Description Default
name Any

A scale string.

required

Returns:

Type Description
tuple[str, str]

(tonic, type).

Example

from tonal_py import Scale Scale.tokenize("C major") ('C', 'major') Scale.tokenize("major") ('', 'major')

Source code in src/tonal_py/scale.py
def tokenize(name: Any) -> tuple[str, str]:
    """Split a scale string into `(tonic, type)`.

    Either component may be empty:
    - `"C major"` -> `("C", "major")`
    - `"major"`   -> `("", "major")`
    - `"C"`       -> `("C", "")`
    - garbage     -> `("", "garbage")`

    Args:
        name: A scale string.

    Returns:
        `(tonic, type)`.

    Example:
        >>> from tonal_py import Scale
        >>> Scale.tokenize("C major")
        ('C', 'major')
        >>> Scale.tokenize("major")
        ('', 'major')
    """
    if not isinstance(name, str):
        return ("", "")
    space_index = name.find(" ")
    tonic_part = name[:space_index] if space_index >= 0 else name
    tonic = _pitch_note.note(tonic_part)
    if space_index < 0:
        n = _pitch_note.note(name)
        return (n.name, "") if not n.empty else ("", name.lower())
    if tonic.empty:
        n = _pitch_note.note(name)
        return (n.name, "") if not n.empty else ("", name.lower())
    type_ = name[len(tonic.name) + 1 :].lower()
    return (tonic.name, type_ if type_ else "")

get

get(src: Any) -> Scale

Build a Scale from a name string or [tonic, type] tokens.

Parameters:

Name Type Description Default
src Any

A scale name string or a 2-element list [tonic, type].

required

Returns:

Type Description
Scale

A Scale dataclass. Returns NO_SCALE when the type is unknown.

Example

from tonal_py import Scale s = Scale.get("C major") s.notes ('C', 'D', 'E', 'F', 'G', 'A', 'B') s.tonic 'C' s.type 'major' Scale.get("major").notes # no tonic -> no notes ()

Source code in src/tonal_py/scale.py
def get(src: Any) -> Scale:
    """Build a `Scale` from a name string or `[tonic, type]` tokens.

    Args:
        src: A scale name string or a 2-element list `[tonic, type]`.

    Returns:
        A `Scale` dataclass. Returns `NO_SCALE` when the type is unknown.

    Example:
        >>> from tonal_py import Scale
        >>> s = Scale.get("C major")
        >>> s.notes
        ('C', 'D', 'E', 'F', 'G', 'A', 'B')
        >>> s.tonic
        'C'
        >>> s.type
        'major'
        >>> Scale.get("major").notes        # no tonic -> no notes
        ()
    """
    tokens = src if isinstance(src, list) else tokenize(src)
    tonic_name = _pitch_note.note(tokens[0]).name
    st = scale_type.get(tokens[1])
    if st.empty:
        return NO_SCALE
    type_ = st.name
    notes_ = (
        tuple(_pitch_distance.transpose(tonic_name, i) for i in st.intervals)
        if tonic_name
        else ()
    )
    name_ = tonic_name + " " + type_ if tonic_name else type_
    return Scale(
        empty=False,
        name=name_,
        set_num=st.set_num,
        chroma=st.chroma,
        normalized=st.normalized,
        intervals=st.intervals,
        aliases=st.aliases,
        type=type_,
        tonic=tonic_name or None,
        notes=notes_,
    )

detect

detect(notes_in: list[str], options: ScaleDetectOptions | None = None) -> list[str]

Identify scales matching a list of notes.

The first note in the input is used as the tonic unless options.tonic overrides it. By default, returns exact matches plus any scales that extend the input. Pass match="exact" to suppress the extensions.

Parameters:

Name Type Description Default
notes_in list[str]

Notes to detect scales from.

required
options ScaleDetectOptions | None None

Returns:

Type Description
list[str]

Matching scale names (e.g. ["C major", "C bebop"]).

Example

from tonal_py import Scale Scale.detect(["C", "D", "E", "F", "G", "A", "B"], {"match": "exact"}) ['C major']

Source code in src/tonal_py/scale.py
def detect(notes_in: list[str], options: ScaleDetectOptions | None = None) -> list[str]:
    """Identify scales matching a list of notes.

    The first note in the input is used as the tonic unless `options.tonic`
    overrides it. By default, returns exact matches plus any scales that
    extend the input. Pass `match="exact"` to suppress the extensions.

    Args:
        notes_in: Notes to detect scales from.
        options: See [`ScaleDetectOptions`][tonal_py.scale.ScaleDetectOptions].

    Returns:
        Matching scale names (e.g. `["C major", "C bebop"]`).

    Example:
        >>> from tonal_py import Scale
        >>> Scale.detect(["C", "D", "E", "F", "G", "A", "B"], {"match": "exact"})
        ['C major']
    """
    options = options or {}  # type: ignore[assignment]
    notes_chroma = pcset.chroma(notes_in)
    tonic_choice = options.get("tonic") or (notes_in[0] if notes_in else "")
    tonic = _pitch_note.note(tonic_choice)
    if tonic.empty:
        return []
    tonic_chroma = int(tonic.chroma)
    pitch_classes = list(notes_chroma)
    pitch_classes[tonic_chroma] = "1"
    rotated = collection.rotate(tonic_chroma, pitch_classes)
    scale_chroma = "".join(rotated)
    match = next((st for st in scale_type.all() if st.chroma == scale_chroma), None)
    results: list[str] = []
    if match:
        results.append(tonic.name + " " + match.name)
    if options.get("match") == "exact":
        return results
    for scale_name in extended(scale_chroma):
        results.append(tonic.name + " " + scale_name)
    return results

scale_chords

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

Find chord types whose notes are all contained in this scale.

Parameters:

Name Type Description Default
name str

A scale name.

required

Returns:

Type Description
list[str]

Chord aliases for chords compatible with the scale.

Example

from tonal_py import Scale chords = Scale.scale_chords("C major") "M" in chords # major triad True "maj7" in chords # major 7 True

Source code in src/tonal_py/scale.py
def scale_chords(name: str) -> list[str]:
    """Find chord types whose notes are all contained in this scale.

    Args:
        name: A scale name.

    Returns:
        Chord aliases for chords compatible with the scale.

    Example:
        >>> from tonal_py import Scale
        >>> chords = Scale.scale_chords("C major")
        >>> "M" in chords     # major triad
        True
        >>> "maj7" in chords  # major 7
        True
    """
    s = get(name)
    in_scale = pcset.is_subset_of(s.chroma)
    return [c.aliases[0] for c in chord_type.all() if in_scale(c.chroma) and c.aliases]

extended

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

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

Accepts either a scale name or a 12-bit chroma string directly.

Parameters:

Name Type Description Default
name str

A scale name or chroma string.

required

Returns:

Type Description
list[str]

Scale names whose pitch class set is a superset.

Example

from tonal_py import Scale "major" in Scale.extended("C major pentatonic") True

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

    Accepts either a scale name or a 12-bit chroma string directly.

    Args:
        name: A scale name or chroma string.

    Returns:
        Scale names whose pitch class set is a superset.

    Example:
        >>> from tonal_py import Scale
        >>> "major" in Scale.extended("C major pentatonic")
        True
    """
    chroma_str = name if pcset.is_chroma(name) else get(name).chroma
    is_super = pcset.is_superset_of(chroma_str)
    return [s.name for s in scale_type.all() if is_super(s.chroma)]

reduced

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

Find scales contained within this one (subset of notes).

Parameters:

Name Type Description Default
name str

A scale name.

required

Returns:

Type Description
list[str]

Scale names whose pitch class set is a subset.

Example

from tonal_py import Scale "major pentatonic" in Scale.reduced("C major") True

Source code in src/tonal_py/scale.py
def reduced(name: str) -> list[str]:
    """Find scales contained within this one (subset of notes).

    Args:
        name: A scale name.

    Returns:
        Scale names whose pitch class set is a subset.

    Example:
        >>> from tonal_py import Scale
        >>> "major pentatonic" in Scale.reduced("C major")
        True
    """
    is_sub = pcset.is_subset_of(get(name).chroma)
    return [s.name for s in scale_type.all() if is_sub(s.chroma)]

scale_notes

scale_notes(notes_in: list[str]) -> list[str]

Sort + dedupe a notes list, then rotate so the first note is the tonic.

Useful for building a scale from a melodic fragment.

Parameters:

Name Type Description Default
notes_in list[str]

A list of notes.

required

Returns:

Type Description
list[str]

Sorted unique pitch classes, rotated so the input's first note

list[str]

is at index 0.

Example

from tonal_py import Scale Scale.scale_notes(["G", "C", "E", "G", "A"]) ['G', 'A', 'C', 'E']

Source code in src/tonal_py/scale.py
def scale_notes(notes_in: list[str]) -> list[str]:
    """Sort + dedupe a notes list, then rotate so the first note is the tonic.

    Useful for building a scale from a melodic fragment.

    Args:
        notes_in: A list of notes.

    Returns:
        Sorted unique pitch classes, rotated so the input's first note
        is at index 0.

    Example:
        >>> from tonal_py import Scale
        >>> Scale.scale_notes(["G", "C", "E", "G", "A"])
        ['G', 'A', 'C', 'E']
    """
    pcs = [n.pc for n in (_pitch_note.note(x) for x in notes_in) if n.pc]
    if not pcs:
        return []
    tonic = pcs[0]
    sorted_pcs = _note_mod.sorted_uniq_names(pcs)
    return collection.rotate(sorted_pcs.index(tonic), sorted_pcs)

mode_names

mode_names(name: str) -> list[tuple[str, str]]

Get all modes of a scale, paired with their starting tonics.

Parameters:

Name Type Description Default
name str

A scale name.

required

Returns:

Type Description
list[tuple[str, str]]

List of (tonic, mode_name) tuples — one per mode, in order.

Example

from tonal_py import Scale Scale.mode_names("C major") # doctest: +NORMALIZE_WHITESPACE [('C', 'major'), ('D', 'dorian'), ('E', 'phrygian'), ('F', 'lydian'), ('G', 'mixolydian'), ('A', 'minor'), ('B', 'locrian')]

Source code in src/tonal_py/scale.py
def mode_names(name: str) -> list[tuple[str, str]]:
    """Get all modes of a scale, paired with their starting tonics.

    Args:
        name: A scale name.

    Returns:
        List of `(tonic, mode_name)` tuples — one per mode, in order.

    Example:
        >>> from tonal_py import Scale
        >>> Scale.mode_names("C major")  # doctest: +NORMALIZE_WHITESPACE
        [('C', 'major'), ('D', 'dorian'), ('E', 'phrygian'),
         ('F', 'lydian'), ('G', 'mixolydian'), ('A', 'minor'),
         ('B', 'locrian')]
    """
    s = get(name)
    if s.empty:
        return []
    tonics = s.notes if s.tonic else s.intervals
    out: list[tuple[str, str]] = []
    for i, ch in enumerate(pcset.modes(s.chroma)):
        mode_name = get(ch).name
        if mode_name:
            out.append((tonics[i], mode_name))
    return out

range_of

range_of(scale_input: Any) -> Callable[[str, str], list[str]]

Build a function that produces in-scale notes between two pitches.

Parameters:

Name Type Description Default
scale_input Any

A scale name or list of notes.

required

Returns:

Type Description
Callable[[str, str], list[str]]

A function (from_note, to_note) -> list[str] that returns all

Callable[[str, str], list[str]]

notes in the scale between (and including) the bounds.

Example

from tonal_py import Scale rng = Scale.range_of("C major") rng("C4", "G4") ['C4', 'D4', 'E4', 'F4', 'G4']

Source code in src/tonal_py/scale.py
def range_of(scale_input: Any) -> Callable[[str, str], list[str]]:
    """Build a function that produces in-scale notes between two pitches.

    Args:
        scale_input: A scale name or list of notes.

    Returns:
        A function `(from_note, to_note) -> list[str]` that returns all
        notes in the scale between (and including) the bounds.

    Example:
        >>> from tonal_py import Scale
        >>> rng = Scale.range_of("C major")
        >>> rng("C4", "G4")
        ['C4', 'D4', 'E4', 'F4', 'G4']
    """
    get_name = _get_note_name_of(scale_input)

    def fn(from_note: str, to_note: str) -> list[str]:
        from_n = _pitch_note.note(from_note)
        to_n = _pitch_note.note(to_note)
        if from_n.empty or to_n.empty:
            return []
        return [
            name for name in (get_name(m) for m in collection.range(int(from_n.height), int(to_n.height))) if name
        ]

    return fn

degrees

degrees(scale_name: str) -> Callable[[int], str]

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

Degree 0 returns "". Negative degrees count backwards from the scale's last note.

Parameters:

Name Type Description Default
scale_name str

A scale name.

required

Returns:

Type Description
Callable[[int], str]

A function (degree: int) -> str.

Example

from tonal_py import Scale deg = Scale.degrees("C major") deg(1) 'C' deg(5) 'G' deg(0) ''

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

    Degree 0 returns `""`. Negative degrees count backwards from the
    scale's last note.

    Args:
        scale_name: A scale name.

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

    Example:
        >>> from tonal_py import Scale
        >>> deg = Scale.degrees("C major")
        >>> deg(1)
        'C'
        >>> deg(5)
        'G'
        >>> deg(0)
        ''
    """
    s = get(scale_name)
    transposer = _pitch_distance.tonic_intervals_transposer(list(s.intervals), s.tonic)

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

    return fn

steps

steps(scale_name: str) -> Callable[[int], str]

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

Like degrees but 0-indexed; wraps cyclically.

Parameters:

Name Type Description Default
scale_name str

A scale name.

required

Returns:

Type Description
Callable[[int], str]

A function (step: int) -> str.

Example

from tonal_py import Scale step = Scale.steps("C major") step(0) 'C' step(7) # wraps to next octave's tonic 'C'

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

    Like [`degrees`][tonal_py.scale.degrees] but 0-indexed; wraps cyclically.

    Args:
        scale_name: A scale name.

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

    Example:
        >>> from tonal_py import Scale
        >>> step = Scale.steps("C major")
        >>> step(0)
        'C'
        >>> step(7)        # wraps to next octave's tonic
        'C'
    """
    s = get(scale_name)
    return _pitch_distance.tonic_intervals_transposer(list(s.intervals), s.tonic)