Key Signatures

This module explores the relationship between keys and key signatures.

In musical notation, different musical keys are represented by patterns of sharps and flats on the staff called key signatures. The relationship between keys and key signatures is illustrated through a diagram called the circle of fifths:

Image credit and original source

In the circle of fifths above, major keys are given in red, minor keys are given in green, and corresponding key signatures (along with the number of sharps or flats) are given in black. Note that the number of sharps in each key signature increases as we travel clockwise from the top, and the number of flats increases in the reverse direction.

This module will focus on using coding to illustrate the relationships between keys and key signatures. (For more on the music theory implications, check out the Open Music Theory entry.)

Everything we will cover can essentially be boiled down to one of two questions:

  1. How do we determine the key, given the key signature?
  2. How do we determine the key signature, given the key?

Let’s begin with the first question and, at least to start, focus on the major keys on the right half of the circle of fifths. Let’s think of each of these key signatures as defined by the number of sharps it contains: C major contains 0, G major contains 1, D major contains 2, etc.

We can create a list of key names (as strings), where the index number gives the number of sharps (we start with C since index numbers count from zero):

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

If we know the key signature, we can count the number of sharps and use that as the index number to determine the key:

sharp_keys[2]
> 'D'

sharp_keys[6]
> 'F#'

We can do the same thing with flats, counting in the opposite direction on the circle:

flat_keys = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb']

flat_keys[4]	 
> 'Ab'

flat_keys[0]		 
> 'C'

While each of these methods works on its own, it probably makes more sense to integrate them into a single tool for finding keys. To bring them together, we need two pieces of information for each calculation: (1) whether the key signature has sharps or flats, and (2) how many there are. We can organize them as elements in a list:

my_key = ['flats', 3]

We’ll use a conditional if statement to separate flats from sharps (and the print function to test our block of code):

if my_key[0] == 'flats':
 print('flats')
elif my_key[0] == 'sharps':
 print('sharps')
> flats

Then we can modify the print function so that it actually displays the key with a statement to access the element in the appropriate list:

if my_key[0] == 'flats':
 print(flat_keys[my_key[1]])
elif my_key[0] == 'sharps':
 print(sharp_keys[my_key[1]])
> Eb

Try this block of code with different key signatures:

my_key = ['sharps', 4]

my_key = ['flats', 6]

my_key = ['sharps', 1]

We can also integrate all of this into a function to make things tidier:

def key_finder(s_or_f, num):
 flat_keys = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb']
 sharp_keys = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#']
 if s_or_f == 'flats':
  print(flat_keys[num])
 elif s_or_f == 'sharps':
  print(sharp_keys[num])

key_finder('sharps', 3) 
> A

Make sure to put the word “sharps” or “flats” in quotation marks when using it as an argument to the function.

We can add a conditional at the beginning to exclude numbers of sharps or flats outside of the expected range, and a final “else” statement if the user enters something other than “sharps” or “flats”:

def key_finder(s_or_f, num):
 flat_keys = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb']
 sharp_keys = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#']
 if (num < 0) or (num > 7):
  print("Not a valid key signature.")
 else:
  if s_or_f == 'flats':
   print(flat_keys[num])
  elif s_or_f == 'sharps':
   print(sharp_keys[num])
  else:
   print("Not a valid key signature.")

key_finder('flats', 5)
> Db

key_finder('sharps', 8)	 
> Not a valid key signature.

We can easily adapt this to the minor keys by updating the source lists:

def key_finder(s_or_f, num):
 flat_keys = ['a', 'd', 'g', 'c', 'f', 'bb', 'eb', 'ab']
 sharp_keys = ['a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#']
 if (num < 0) or (num > 7):
  print("Not a valid key signature.")
 else:
  if s_or_f == 'flats':
   print(flat_keys[num])
  elif s_or_f == 'sharps':
   print(sharp_keys[num])
  else:
   print("Not a valid key signature.")

Now let’s examine the reverse situation: given a key name, how can we find the key signature?

As we have already established, the position of each element in each list is equal to the number of sharps or flats for our key signature. If we stick with the same structure, the index() function will be helpful in obtaining the position of a given value:

flat_keys = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb']

flat_keys.index('Bb') 
> 2

Consequently, there will be two steps to this function: (1) determine which list contains the given key, and (2) determine the index number within that list.

Rather than using two arguments (key name and major/minor), we’ll use a single string as input, taking advantage of the upper-/lower-case convention for major/minor keys (e.g. “F#” for F-sharp major, “f#” for f-sharp minor).

We can use the “in” keyword to determine whether the key name is in a given list, and print its index position for that list only by using a series of “if” statements:

def find_signature(key):
 flat_minor_keys = ['a', 'd', 'g', 'c', 'f', 'bb', 'eb', 'ab']
 sharp_minor_keys = ['a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#']
 flat_major_keys = ['C', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb']
 sharp_major_keys = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#']
 if key in flat_minor_keys:
  print(flat_minor_keys.index(key), 'flats')
 elif key in flat_major_keys:
  print(flat_major_keys.index(key), 'flats')
 elif key in sharp_major_keys:
  print(sharp_major_keys.index(key), 'sharps')
 elif key in sharp_minor_keys:
  print(sharp_minor_keys.index(key), 'sharps')
 else:
  print('Invalid key')

As in the previous function, we can use a final “else” statement to let the user know if their input was invalid:

find_signature('f#')	 
> 3 sharps

find_signature('A')
> 3 sharps

find_signature('H#')
> Invalid key

Extensions

  1. How can we integrate major and minor keys into a single key-finding function?
  2. Can you build a function to output relative keys?
  3. What are alternative structures for determining which list contains the given key?
  4. How might we use dictionaries to store and access key and key signature information?

Further Reading

For more on the music theory-related aspects of keys and key signatures, check out the Open Music Theory entry.