| name | keys-and-harmony-with-pytheory |
| description | Analyze and generate keys and chord progressions with PyTheory. Use when the user asks what key some notes are in, the diatonic chords of a key, a Roman- numeral analysis of a progression, what chord comes next, secondary dominants, borrowed chords, chords grouped by function (tonic/subdominant/dominant), the key-level circle of fifths, negative harmony, how to modulate between keys (pivot chords / modulation path), or to generate a progression. For a single chord's voicing/analysis use the chord-lab skill; for full arrangements use the composing skill. |
| license | MIT |
| allowed-tools | Write, Read, Bash(python3:*), Bash(uv run:*) |
Keys & Harmony
Working with keys and progressions — the harmonic level above a single chord.
Keys and their diatonic content
from pytheory import Key
k = Key("C", "major")
k.chords
k.seventh_chords
k.note_names
k.signature
k.relative
Detect the key
Key.detect("C", "E", "G", "B", "D")
Progressions
k = Key("G", "major")
[c.symbol for c in k.progression("I", "V", "vi", "IV")]
[c.symbol for c in k.nashville(1, 5, 6, 4)]
k.random_progression(4)
Roman-numeral analysis of an existing progression:
from pytheory import analyze_progression, Chord
chords = [Chord.from_symbol(s) for s in ["C", "G", "Am", "F"]]
analyze_progression(chords, key="C", mode="major")
Secondary dominants — applied dominants that tonicise a non-tonic
degree. detect_secondary_dominant identifies one chord (the analytical
inverse of Key.secondary_dominant(degree), which builds one); pass
secondary_dominants=True to analyze_progression to label them in
context (instead of the bare degree):
from pytheory import detect_secondary_dominant, analyze_progression, Chord
detect_secondary_dominant(Chord.from_symbol("D7"), "C")
detect_secondary_dominant(Chord.from_symbol("E7"), "C")
prog = [Chord.from_symbol(s) for s in ("C", "D7", "G7", "C")]
analyze_progression(prog, "C", secondary_dominants=True)
From the terminal, pytheory analyze C D7 G7 C prints the whole picture —
detected key, Roman numerals (with secondary dominants), and cadences
(add --key/--mode to fix the key).
Cadences — the harmonic punctuation that ends a phrase. Pass the last
two chords (and the key) to detect_cadence, or scan a whole progression
with find_cadences:
from pytheory import detect_cadence, find_cadences, Chord
C = Chord.from_name
detect_cadence(C("G"), C("C"), "C")
detect_cadence(C("G"), C("Am"), "C")
detect_cadence(C("F"), C("C"), "C")
detect_cadence(C("Dm"), C("G"), "C")
detect_cadence(C("E"), C("Am"), "A", "minor")
pac_I = Chord.from_midi_message(48, 52, 55, 60)
detect_cadence(C("G"), pac_I, "C")
find_cadences([C(n) for n in ("C","F","G","Am")], "C")
Non-chord tones — label the melody notes that aren't in the harmony
(passing / neighbor / suspension / anticipation / appoggiatura / escape).
Pass one chord for the whole line, or one chord per note:
from pytheory import analyze_non_chord_tones, Chord, Tone
mel = [Tone.from_string(n) for n in ("C4","D4","E4")]
[r["type"] for r in analyze_non_chord_tones(mel, Chord.from_name("C"))]
Harmonic color & motion
k = Key("C", "major")
k.secondary_dominant(5).symbol
k.borrowed_chords
k.suggest_next(Chord.from_symbol("G"))
Chords grouped by harmonic function (interchangeable within a group), the
key-level circle of fifths, and negative harmony:
k = Key("C", "major")
k.chords_by_function()
k.tonic_chords(); k.subdominant_chords(); k.dominant_chords()
cof = k.circle_of_fifths()
cof["position"]
cof["relative"], cof["parallel"]
cof["dominant"]["key"], cof["dominant"]["shared_chords"]
neg = k.negative_harmony()
neg["axis"]
neg["negative_dominant"]
neg["scale"], neg["chords"]
Modulation
src, dst = Key("C", "major"), Key("G", "major")
src.pivot_chords(dst)
src.modulation_path(dst)
Hear a progression
from pytheory.play import play_progression
from pytheory import Key
play_progression(Key("G", "major").progression("I", "V", "vi", "IV"), t=700)
Tips
progression() takes Roman numerals (lowercase = minor: "ii", "vi");
nashville() takes degree numbers.
analyze_progression(chords, key=, mode=) returns numerals (or None for
chords outside the key) — handy for "what are these chords doing?".
- To turn a progression into a full track (drums, bass, synths, export), hand off
to the composing-with-pytheory skill.