Python script to test music sight reading I decided to write a program to test music theory, and, while it would've been much easier for me to make it elegant and perfect in Java, I thought I'd take the opportunity to get more familiar with Python.

Below is the full script.  It simply prints a staff in treble of bass clef with a note somewhere and asks the user to identify it.

Things I'm particularly interested in getting feedback about:


The @staticmethod in the Staff class.  I tried this both as an instance function (is it even appropriate to call it this in Python?) and as the static version below, but both seemed kind of clunky to me.  It doesn't make much sense to me that I have to refer to the class's fields with the class name when I'm already in the class (e.g., Staff.lines instead of just lines, or self.lines if it's an instance function).  It makes me think I'm doing something wrong.
I've heard multiple times that having constants isn't very Pythonic.  Is there a better way for me to handle the TREBLE_CLEF, BASS_CLEF, TREBLE_CLEF_NOTES, and BASS_CLEF_NOTES fields?
I'm interested in a solution which allows the user to input a command like exit into the prompt in order to leave the program.  I know I could've put this in the test_note function, but that would break the principle that each function should do only one thing and do it well (i.e., the test_note function test's the user's knowledge of the staff -- it shouldn't be responsible at all for controlling the application or its state).  I couldn't think of an elegant way to do this and still preserve that principle, so I just bypassed the issue for now, haha.
Am I using classes appropriately?  Any way I can use them better?


Of course, any and all feedback is welcome, especially including anything which breaks from conventional Python style.  I'd like to be as Pythonic as possible.



import random
import time

class Clef:

    def __init__(self, name, notes):
        self.name = name
        self.notes = notes

    def random_note(self):
        return random.choice(list(self.notes.keys()))

    def get_note(self, index):
        return self.notes[index]

class Staff:

    lines = 9
    line_length = 9
    line_text = '-' * line_length
    space_text = ' ' * line_length
    note_text = 'O'

    @staticmethod
    def display(note_index):
        for i in range(Staff.lines):
            add_note = i == note_index
            print_line = Staff.line_text if i % 2 == 0 else Staff.space_text
            if i == note_index:
                print_line = print_line[:4] + Staff.note_text + print_line[5:]
            print(print_line)


TREBLE_CLEF_NOTES = { 0 : 'F',
                      1 : 'E',
                      2 : 'D',
                      3 : 'C',
                      4 : 'B',
                      5 : 'A',
                      6 : 'G',
                      7 : 'F',
                      8 : 'E' }

BASS_CLEF_NOTES = { 0 : 'A',
                    1 : 'G',
                    2 : 'F',
                    3 : 'E',
                    4 : 'D',
                    5 : 'C',
                    6 : 'B',
                    7 : 'A',
                    8 : 'G' }

TREBLE_CLEF = Clef('Treble', TREBLE_CLEF_NOTES)
BASS_CLEF = Clef('Bass', BASS_CLEF_NOTES)

def test_note(clef, note_index):
    print(clef.name)
    Staff.display(note_index)
    answer = clef.get_note(note_index)
    user_choice = input('Enter the note: ').strip().upper()
    if user_choice == answer:
        print('Correct!')
    else:
        print('Wrong.  This is ' + answer)

def main():
    print('To exit, type CTRL + C')
    while True:
        print('\n')
        clef = random.choice([TREBLE_CLEF, BASS_CLEF])
        test_note(clef, clef.random_note())
        time.sleep(2)

if __name__ == '__main__':
    main()
Warning for other reviewers : input and input are different depending on the version of Python you are using. This is about Python 3.



Instead of defining the notes of a clef as a dictionnary mapping consecutive integers from 0 to n to names, you could use a list.

You'll have the much more natural :

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


and you just need to change :

def random_note(self):
    return random.choice(range(len(self.notes)))




add_note = i == note_index does not seem useful : get rid of it.



The way you are playing with string slicing to display a note or a line doesn't need to be that complicated. Also, defining values as class members was a nice touch but the fact that you hardcode indices in the logic with the string slicing kind of removes the all point. As for me, I'll keep things simple and not so well organised for the time being as these values are unlikely to be useful for anything else than displaying.

Here's my suggestion :

def display(note_index):
    for i in range(9):
        line_element = '-' if i%2 else ' '
        print(line_element * 4 + ('O' if i == note_index else line_element) + line_element*5)




I have to go, I'll try to add more details later. In the meantime, if you are interested in Python and music theory, you might be interested by this github project.