Basic Music Theory in ~200 Lines of Python Manohar Vanga

Note: all the code for this article can be found here as a Github gist. There’s also a nice discussion on Hacker News with lots of comments that might be of interest.

I’m a self-taught guitarist of many years, and like a lot of self-taught musicians, am woefully inept at (Western) music theory.

So naturally, I decided to write some code.

This article explains the very basics of Western music theory in around 200 lines of Python.

We will first look at the notes in Western music theory, use them to derive the chromatic scale in a given key, and then to combine it with interval formulas to derive common scales and chords.

Finally, we will look at modes, which are whole collections of scales derived from common scales, that can be used to evoke more subtle moods and atmospheres than the happy-sad dichotomy that major and minor scales provide.

The Twelve Notes

The musical alphabet of Western music consists of the letters A through G, and they represent different pitches of notes.

We can represent the musical alphabet with the following list in Python:

alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G']

However, these notes are not evenly spaced in their frequencies. To get a more even spacing between pitches, we have the following twelve notes:

notes_basic = [
['A'],
['A#', 'Bb'],
['B'],
['C'],
['C#', 'Db'],
['D'],
['D#', 'Eb'],
['E'],
['F'],
['F#', 'Gb'],
['G'],
['G#', 'Ab'],
]

There are four things to note here: first, each of the notes are a half step or semitone apart, second, the way this is represented is by an optional trailing symbol (called an accidental) to indicate a half-step raise (sharp, ♯) or a half-step lowering (flat, ♭) of the base note, and third, the above notes simply loop back and start over, but at a higher octave.

Finally, you’ll notice that some of these notes are represented by lists containing more than one name: these are enharmonic equivalents, a fancy way of saying that the same note can have different “spellings”. So for example the note half a step above A is A♯, but it can also be thought of as half a step below B, and can thus be referred to as B♭. For historical reasons, there are no sharps or flats between the notes B/C, and E/F.

The key thing to remember on why we need these equivalents is that, when we start to derive common scales (major, minor and the modes), consecutive notes must start with consecutive letters. Having enharmonic equivalents with different alphabets allows us to derive these scales correctly.

In fact, for certain keys, the above enharmonic notes are not sufficient. In order to satisfy the “different-alphabets-for-consecutive-notes rule”, we end up having to use double sharps and double flats that raise or lower a note by a full step. These scales generally have equivalents that do not require these double accidentals, but for completeness, we can include all possible enharmonic equivalents by rewriting our notes as follows:

notes = [
['B#',  'C',  'Dbb'],
['B##', 'C#', 'Db'],
['C##', 'D',  'Ebb'],
['D#',  'Eb', 'Fbb'],
['D##', 'E',  'Fb'],
['E#',  'F',  'Gbb'],
['E##', 'F#', 'Gb'],
['F##', 'G',  'Abb'],
['G#',  'Ab'],
['G##', 'A',  'Bbb'],
['A#',  'Bb', 'Cbb'],
['A##', 'B',  'Cb'],
]

Chromatic Scales

The chromatic scale is the easiest scale possible, and simply consists of all the (twelve) semitones between an octave of a given key (the main note in a scale, also called the tonic).

We can generate a chromatic scale for any given key very easily: (i) find the index of the note in our notes list, and then (ii) left-rotate the notes list that many times.

Finding the Index of a Given Note

Let’s write a simple function to find a particular note in this list:

def find_note_index(scale, search_note):
''' Given a scale, find the index of a particular note '''
for index, note in enumerate(scale):
# Deal with situations where we have a list of enharmonic
# equivalents, as well as just a single note as and str.
if type(note) == list:
if search_note in note:
return index
elif type(note) == str:
if search_note == note:
return index

The find_note_index() function takes as parameters a sequence of notes (scale), and a note to search for (search_note), and returns the index via a simple linear search. We handle two cases within the loop: (i) where the provided scale consists of individual notes (like our alphabet list above), or (ii) where it consists of a list of enharmonic equivalents (like our notes or notes_basic lists above). Here is an example of how the function works for both:

>>> find_note_index(notes, 'A')    # notes is a list of lists
9
>>> find_note_index(alphabet, 'A') # alphabet is a list of notes
0

Left-Rotating a Scale

We can now write a function to rotate a given scale by n steps:

def rotate(scale, n):
''' Left-rotate a scale by n positions. '''
return scale[n:] + scale[:n]

We slice the scale list at position n and exchange the two halves. Here is an example of rotating our alphabet list three places (which brings the note D to the front):

>>> alphabet
['A', 'B', 'C', 'D', 'E', 'F', 'G']
>>> rotate(alphabet, 3)
['D', 'E', 'F', 'G', 'A', 'B', 'C']

Generating a Chromatic Scale in a Given Key

We can now finally write our chromatic() function that generates a chromatic scale for a given key by rotating the notes array:

def chromatic(key):
''' Generate a chromatic scale in a given key. '''
# Figure out how much to rotate the notes list by and return
# the rotated version.
num_rotations = find_note_index(notes, key)
return rotate(notes, num_rotations)

The chromatic() function above finds the index of the provided key in the notes list (using our find_note_index() function), and then rotates it by that amount to bring it to the front (using our rotate() function). Here is an example of generating the D chromatic scale:

>>> import pprint
>>> pprint.pprint(chromatic('D'))
[['C##', 'D', 'Ebb'],
['D#', 'Eb', 'Fbb'],
['D##', 'E', 'Fb'],
['E#', 'F', 'Gbb'],
['E##', 'F#', 'Gb'],
['F##', 'G', 'Abb'],
['G#', 'Ab'],
['G##', 'A', 'Bbb'],
['A#', 'Bb', 'Cbb'],
['A##', 'B', 'Cb'],
['B#', 'C', 'Dbb'],
['B##', 'C#', 'Db']]

For chromatic scales, one typically uses sharps when ascending and flats when descending. However, for now, we leave enharmonic equivalents just as they are; we will see how to pick the correct note to use later.

Intervals

Intervals specify the relative distance between notes.

The notes of a chromatic scale can therfore be given names based on their relative distance from the tonic or root note. Below are the standard names for each note, ordered identical to the indexes in the notes list:

intervals = [
['P1', 'd2'],  # Perfect unison   Diminished second
['m2', 'A1'],  # Minor second     Augmented unison
['M2', 'd3'],  # Major second     Diminished third
['m3', 'A2'],  # Minor third      Augmented second
['M3', 'd4'],  # Major third      Diminished fourth
['P4', 'A3'],  # Perfect fourth   Augmented third
['d5', 'A4'],  # Diminished fifth Augmented fourth
['P5', 'd6'],  # Perfect fifth    Diminished sixth
['m6', 'A5'],  # Minor sixth      Augmented fifth
['M6', 'd7'],  # Major sixth      Diminished seventh
['m7', 'A6'],  # Minor seventh    Augmented sixth
['M7', 'd8'],  # Major seventh    Diminished octave
['P8', 'A7'],  # Perfect octave   Augmented seventh
]

Again, the same note can have different interval names. For example, the root note can be thought of as a perfect unison or an diminished 2nd.

Picking Out Notes from Enharmonic Equivalents

Given a chromatic scale in a given key, and an interval name in the above array, we can pin point the exact note to use (and filter it out from a set of enharmonic equivalents). Let’s look at the basic way to do this.

As an example, let’s look at how to find the note corresponding to M3, or the major third interval, from the D chromatic scale.

1. From our intervals array, we can see that the index at which we find M3 is 4. That is 'M3' in intervals == True.
2. Now we look at the same index in our D chromatic scale (modulo its length). We find that chromatic('D') is the list of notes ['E##', 'F#', 'Gb'].
3. The number in M3 (i.e., the 3) indicates the alphabet we need to use, with 1 indicating the root alphabet. So for example, for the key of D, 1=D, 2=E, 3=F, 4=G, 5=A, 6=B, 7=C, 8=D… and so on. So we need to look for a note in our list of notes (['E##', 'F#', 'Gb']) containing the alphabet F. That’s the note F#.
4. Conclusion: the major third (M3) relative to D is F#.

Programmatically Labeling Intervals for a Given Key

We can write a relatively simple function to apply this logic for us programmatically, and give us a dict mapping all interval names to the right note names in a given key:

def make_intervals_standard(key):
# Our labeled set of notes mapping interval names to notes
labels = {}

# Step 1: Generate a chromatic scale in our desired key
chromatic_scale = chromatic(key)

# The alphabets starting at provided key's alphabet
alphabet_key = rotate(alphabet, find_note_index(alphabet, key))

# Iterate through all intervals (list of lists)
for index, interval_list in enumerate(intervals):

# Step 2: Find the notes to search through based on degree
notes_to_search = chromatic_scale[index % len(chromatic_scale)]

for interval_name in interval_list:
# Get the interval degree
degree = int(interval_name) - 1 # e.g. M3 --> 3, m7 --> 7

# Get the alphabet to look for
alphabet_to_search = alphabet_key[degree % len(alphabet_key)]

try:
note = [x for x in notes_to_search if x == alphabet_to_search]
except:
note = notes_to_search

labels[interval_name] = note

return labels

And here is the dict we get back for the key of C:

>>> import pprint
>>> pprint.pprint(make_intervals_standard('C'), sort_dicts=False)
{'P1': 'C',
'd2': 'Dbb',
'm2': 'Db',
'A1': 'C#',
'M2': 'D',
'd3': 'Ebb',
'm3': 'Eb',
'A2': 'D#',
'M3': 'E',
'd4': 'Fb',
'P4': 'F',
'A3': 'E#',
'd5': 'Gb',
'A4': 'F#',
'P5': 'G',
'd6': 'Abb',
'm6': 'Ab',
'A5': 'G#',
'M6': 'A',
'd7': 'Bbb',
'm7': 'Bb',
'A6': 'A#',
'M7': 'B',
'd8': 'Cb',
'P8': 'C',
'A7': 'B#'}

Interval Formulas

We can now specify formulas, or groups of notes, using interval names, and be able to map them to any key that we want:

def make_formula(formula, labeled):
'''
Given a comma-separated interval formula, and a set of labeled
notes in a key, return the notes of the formula.
'''
return [labeled[x] for x in formula.split(',')]

Major Scale Formula

For example, the formula for a major scale is:

formula = 'P1,M2,M3,P4,P5,M6,M7,P8'

We can use this to generate the major scale easily for different keys as shown below:

>>> for key in alphabet:
>>>     print(key, make_formula(formula, make_intervals_standard(key)))
C ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
D ['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
E ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#', 'E']
F ['F', 'G', 'A', 'Bb', 'C', 'D', 'E', 'F']
G ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
A ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#', 'A']
B ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#', 'B']

Prettifying Scales

Let’s also quickly write a function to print scales in a nicer way:

def dump(scale, separator=' '):
'''
Pretty-print the notes of a scale. Replaces b and # characters
for unicode flat and sharp symbols.
'''
return separator.join(['{:<3s}'.format(x) for x in scale]) \
.replace('b', '\u266d') \
.replace('#', '\u266f')

Here is a nicer output using the correct unicode characters:

>>> for key in alphabet:
>>>     scale = make_formula(formula, make_intervals_standard(key))
>>>     print('{}: {}'.format(key, dump(scale)))
C: C   D   E   F   G   A   B   C
D: D   E   F♯  G   A   B   C♯  D
E: E   F♯  G♯  A   B   C♯  D♯  E
F: F   G   A   B♭  C   D   E   F
G: G   A   B   C   D   E   F♯  G
A: A   B   C♯  D   E   F♯  G♯  A
B: B   C♯  D♯  E   F♯  G♯  A♯  B

Using Major-Scale Intervals for Formulas

An alternative approach to naming formulas is based on the notes of the major scale. This is easier when playing instruments as you can derive scales and chords in a given key if youre familiar with its major scale.

Here are the interval names relative to the major scale in a given key:

intervals_major = [
[ '1', 'bb2'],
['b2',  '#1'],
[ '2', 'bb3',   '9'],
['b3',  '#2'],
[ '3',  'b4'],
[ '4',  '#3',  '11'],
['b5',  '#4', '#11'],
[ '5', 'bb6'],
['b6',  '#5'],
[ '6', 'bb7',  '13'],
['b7',  '#6'],
[ '7',  'b8'],
[ '8',  '#7'],
]

I’ve also added common intervals used in more complex chords (9th’s, 11th’s, and 13th’s). These are essentially wrapped around modulo eight. So, for example, a 9th is just a 2nd, but an octave higher.

We can also modify our make_intervals() function to use this:

def make_intervals(key, interval_type='standard'):
...
for index, interval_list in enumerate(intervals):
...
intervs = intervals if interval_type == 'standard' else intervals_major
for interval_name in intervs:
# Get the interval degree
if interval_type == 'standard':
degree = int(interval_name) - 1 # e.g. M3 --> 3, m7 --> 7
elif interval_type == 'major':
degree = int(re.sub('[b#]', '', interval_name)) - 1
...
return labels

Above, we’ve just added a new parameter (interval_type) to the make_intervals() function, and calculate the degree differently in the inner loop. If interval_type is specified as 'major', we just remove all b and # characters before converting to an integer to get the degree.

Deriving Common Scales and Chords

Here are a bunch of formulas covering the most common scales and chords:

formulas = {
# Scale formulas
'scales': {
# Major scale, its modes, and minor scale
'major':              '1,2,3,4,5,6,7',
'minor':              '1,2,b3,4,5,b6,b7',
# Melodic minor and its modes
'melodic_minor':      '1,2,b3,4,5,6,7',
# Harmonic minor and its modes
'harmonic_minor':     '1,2,b3,4,5,b6,7',
# Blues scales
'major_blues':        '1,2,b3,3,5,6',
'minor_blues':        '1,b3,4,b5,5,b7',
# Penatatonic scales
'pentatonic_major':   '1,2,3,5,6',
'pentatonic_minor':   '1,b3,4,5,b7',
'pentatonic_blues':   '1,b3,4,b5,5,b7',
},
'chords': {
# Major
'major':              '1,3,5',
'major_6':            '1,3,5,6',
'major_6_9':          '1,3,5,6,9',
'major_7':            '1,3,5,7',
'major_9':            '1,3,5,7,9',
'major_13':           '1,3,5,7,9,11,13',
'major_7_#11':        '1,3,5,7,#11',
# Minor
'minor':              '1,b3,5',
'minor_6':            '1,b3,5,6',
'minor_6_9':          '1,b3,5,6,9',
'minor_7':            '1,b3,5,b7',
'minor_9':            '1,b3,5,b7,9',
'minor_11':           '1,b3,5,b7,9,11',
'minor_7_b5':         '1,b3,b5,b7',
# Dominant
'dominant_7':         '1,3,5,b7',
'dominant_9':         '1,3,5,b7,9',
'dominant_11':        '1,3,5,b7,9,11',
'dominant_13':        '1,3,5,b7,9,11,13',
'dominant_7_#11':     '1,3,5,b7,#11',
# Diminished
'diminished':         '1,b3,b5',
'diminished_7':       '1,b3,b5,bb7',
'diminished_7_half':  '1,b3,b5,b7',
# Augmented
'augmented':          '1,3,#5',
# Suspended
'sus2':               '1,2,5',
'sus4':               '1,4,5',
'7sus2':              '1,2,5,b7',
'7sus4':              '1,4,5,b7',
},
}

Here is the output when generating all these scales and chords in the key of C:

intervs = make_intervals('C', 'major')
for ftype in formulas:
print(ftype)
for name, formula in formulas[ftype].items():
v = make_formula(formula, intervs)
print('\t{}: {}'.format(name, dump(v)))
scales
major: C   D   E   F   G   A   B
minor: C   D   E♭  F   G   A♭  B♭
melodic_minor: C   D   E♭  F   G   A   B
harmonic_minor: C   D   E♭  F   G   A♭  B
major_blues: C   D   E♭  E   G   A
minor_blues: C   E♭  F   G♭  G   B♭
pentatonic_major: C   D   E   G   A
pentatonic_minor: C   E♭  F   G   B♭
pentatonic_blues: C   E♭  F   G♭  G   B♭
chords
major: C   E   G
major_6: C   E   G   A
major_6_9: C   E   G   A   D
major_7: C   E   G   B
major_9: C   E   G   B   D
major_13: C   E   G   B   D   F   A
major_7_#11: C   E   G   B   F♯
minor: C   E♭  G
minor_6: C   E♭  G   A
minor_6_9: C   E♭  G   A   D
minor_7: C   E♭  G   B♭
minor_9: C   E♭  G   B♭  D
minor_11: C   E♭  G   B♭  D   F
minor_7_b5: C   E♭  G♭  B♭
dominant_7: C   E   G   B♭
dominant_9: C   E   G   B♭  D
dominant_11: C   E   G   B♭  D   F
dominant_13: C   E   G   B♭  D   F   A
dominant_7_#11: C   E   G   B♭  F♯
diminished: C   E♭  G♭
diminished_7: C   E♭  G♭  B♭♭
diminished_7_half: C   E♭  G♭  B♭
augmented: C   E   G♯
sus2: C   D   G
sus4: C   F   G
7sus2: C   D   G   B♭
7sus4: C   F   G   B♭

Modes

Modes are essentially left-rotations of a scale.

mode = rotate

The thing to note is that the resulting rotated scale, or mode, is in a different key since the root note changes after the rotation.

For every key, there are exactly seven modes of the major scale depending on the number of left-rotations applied, and each has a specific name:

major_mode_rotations = {
'Ionian':     0,
'Dorian':     1,
'Phrygian':   2,
'Lydian':     3,
'Mixolydian': 4,
'Aeolian':    5,
'Locrian':    6,
}

Using this, we can now generate modes of the major scale for any given key. Here’s an example for the C major scale:

intervs = make_intervals('C', 'major')
c_major_scale = make_formula(formulas['scales']['major'], intervs)
for m in major_mode_rotations:
v = mode(c_major_scale, major_mode_rotations[m])
print('{} {}: {}'.format(dump([v]), m, dump(v)))

And here is the result. Remember that the root note changes with each rotation:

C   Ionian: C   D   E   F   G   A   B
D   Dorian: D   E   F   G   A   B   C
E   Phrygian: E   F   G   A   B   C   D
F   Lydian: F   G   A   B   C   D   E
G   Mixolydian: G   A   B   C   D   E   F
A   Aeolian: A   B   C   D   E   F   G
B   Locrian: B   C   D   E   F   G   A

Above, we’re looking at modes that derive from a given scale. However, in practice, what you care about are modes for a given key. So given the key of C, we would want to know the C Ionian, the C Dorian, the C Mixolydian and so on.

Another way to put this is that a “C Mixolidian”, for example, is not the same as “the Mixolydian of C”. The former means a Mixolydian scale where the root note is a C. The latter means the Mixolydian of the C major scale (i.e., G Mixolydian from above).

We can also generate modes in a given key quite easily.

keys = [
'B#',  'C', 'C#', 'Db', 'D', 'D#',  'Eb', 'E',  'Fb', 'E#',  'F',
'F#', 'Gb', 'G', 'G#',  'Ab', 'A', 'A#',  'Bb', 'B',  'Cb',
]

modes = {}
for key in keys:
intervs = make_intervals(key, 'major')
c_major_scale = make_formula(formulas['scales']['major'], intervs)
for m in major_mode_rotations:
v = mode(c_major_scale, major_mode_rotations[m])
if v not in modes:
modes[v] = {}
modes[v][m] = v

Above, we go through each key, and build up a dict containing the modes of each key as we come across them (by checking the first note of the mode).

Now, for example, if we print out modes['C'], we get the following:

{'Aeolian': ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb'],
'Dorian': ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb'],
'Ionian': ['C', 'D', 'E', 'F', 'G', 'A', 'B'],
'Locrian': ['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb'],
'Lydian': ['C', 'D', 'E', 'F#', 'G', 'A', 'B'],
'Mixolydian': ['C', 'D', 'E', 'F', 'G', 'A', 'Bb'],
'Phrygian': ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb']}

Summary

So we’ve looked at basic notes in Western music theory. How to derive chromatic scales from these notes. How to use interval names to pick out the right notes from enharmonic equivalents. Then we looked at how to generate scales and chords of various kinds using interval formulas, both using standard interval names and intervals relative to the major-scale. Finally, we saw that modes are simply rotations of a scale, and can be viewed in two way for a given key: the mode derived by rotating the scale of a given key (which will be in a another key), and the mode derived from some key such that the first note is the key we want.

1. You might wonder why we loop over twelve notes but call it an octave, implying only eight notes. The reason is that most common scales like the major and minor scales are made up of subsets of seven notes from these twelve. Therefore, the notes in scales wrap around on the eight note. Thus the term octave. ↩︎