I am training for a Symfony certificate. I found [a Github repository][1] providing questions to prepare for the exam. The repository contains a data directory with a bunch of questions mapped by topic in Yaml format. Because raw yaml files are not user-friendly to train with, I made a quick Python script providing a user-interface which displays the questions and allows you to answer. The script allows the user to select a topic and the amount of questions to load from that topic. The questions are loaded randomly and dynamically from the files. After the user answered all the questions a table is provided giving an overview of the correct answers and the score. [![Python3 quiz interface 1][2]][2] [![Python3 quiz interface 2][3]][3] The code is fully functional but I am wondering if improvements can be made. - Can the readability be improved? - Can the code structure be improved? - Am I following the PEP styling guide? - Can some snippets be shortened? ```python import os import random import yaml from prettytable import PrettyTable DATA_FOLDER = 'data/' class Styling: GREEN = '\033[92m' YELLOW = '\033[33m' RED = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' ENDBOLD = '\033[0m' class CLIHandler: @staticmethod def display_message(message, color='', bold=False): print(f'{color}{Styling.BOLD if bold else ""}{message}{Styling.ENDBOLD if bold else ""}{Styling.ENDC}') @staticmethod def display_topics(topics): CLIHandler.display_message('-- SYMFONY CERTIFICATE PRACTICE --\n', Styling.GREEN, True) for i, topic in enumerate(topics): CLIHandler.display_message(f'{i + 1}. {topic.name}') @staticmethod def display_question(question, question_number): CLIHandler.display_message(f'\n{Styling.YELLOW}Question #{question_number}{Styling.ENDC} {Styling.BOLD}{question}{Styling.ENDBOLD}', bold=True) @staticmethod def display_table(field_names, rows, align='l'): table = PrettyTable() table.align = align table.field_names = [f'{Styling.BOLD}{Styling.GREEN}{field}{Styling.ENDC}{Styling.ENDBOLD}' for field in field_names] for row in rows: table.add_row(row) print('\n', table) @staticmethod def input_message(message): return input(f'{Styling.BOLD}{message}: {Styling.ENDBOLD} ') @staticmethod def clear_screen(): os.system('clear') class Topic: def __init__(self, name, path): self.name = name self.path = path self.questions = [] def load_topic(self, amount_of_questions): while len(self.questions) < amount_of_questions: question = self.__load_random_question() if question not in self.questions: self.questions.append(question) def show_questions(self): for i, question in enumerate(self.questions): question.show_question(i + 1) try: answers = input('> ').split(',') question.select_answers(answers) except (ValueError, IndexError): CLIHandler.display_message('Invalid input. Skipping...', Styling.RED) def show_results(self): rows = [] for i, question in enumerate(self.questions): question_str = question.question[:75] + (question.question[:75] and '..') correct_answers = question.correct_answers() your_answers = question.selected_answers() result = '✅' if set(correct_answers) == set(your_answers) else '❌' rows.append([ f'{Styling.YELLOW}#{i + 1}{Styling.ENDC} {question_str}', ', '.join([answer.value[:35] + (answer.value[:35] and '..') for answer in correct_answers]), result ]) correct_count = self.__correct_answers_count() CLIHandler.display_table( field_names=['Question', 'Correct Answer', 'Result'], rows=rows ) CLIHandler.display_message( f'{Styling.BOLD}Score:{Styling.ENDBOLD} {Styling.RED}Wrong: {len(self.questions) - correct_count}{Styling.ENDC} - ' f'{Styling.GREEN}Correct: {correct_count}{Styling.ENDC}' ) def __correct_answers_count(self): return sum( set(question.selected_answers()) == set(question.correct_answers()) for question in self.questions ) def __load_random_question(self): question_file = random.choice([f for f in os.listdir(self.path) if f.endswith('.yaml')]) with open(self.path + '/' + question_file) as file: questions = yaml.safe_load(file)['questions'] random_question = random.choice(questions) question = Question(random_question['uuid'], random_question['question']) for answer in random_question['answers']: question.add_answer(Answer(answer['value'], answer['correct'])) return question @staticmethod def list_topics(): return [Topic(topic.lower(), os.path.join(DATA_FOLDER, topic)) for topic in os.listdir(DATA_FOLDER)] class Question: def __init__(self, uuid, question): self.uuid = uuid self.question = question.replace('\n', ' ').replace('\r', ' ') self.answers = [] def __eq__(self, other): if isinstance(other, Question): return self.uuid == other.uuid return False def add_answer(self, answer): self.answers.append(answer) def show_question(self, question_number): CLIHandler.display_question(self.question, question_number) for i, answer in enumerate(self.answers): CLIHandler.display_message(f'\t[{Styling.GREEN}{i + 1}{Styling.ENDC}] {answer.value}') def select_answers(self, answers): answers = [int(answer) - 1 for answer in answers] for answer in answers: if 0 <= answer < len(self.answers): self.answers[answer].select() CLIHandler.display_message(f'{Styling.YELLOW}Your answer: {Styling.ENDC}{self.answers[answer].value}') else: raise IndexError('Invalid answer selected') def selected_answers(self): return [answer for answer in self.answers if answer.selected] def correct_answers(self): return [answer for answer in self.answers if answer.correct] class Answer: def __init__(self, value, correct): self.value = value self.correct = correct self.selected = False def select(self): self.selected = True def main(): topics = Topic.list_topics() CLIHandler.display_topics(topics) try: selected_topic = int(CLIHandler.input_message('\nSelect topic')) - 1 amount_of_questions = int(CLIHandler.input_message('Amount of questions')) except ValueError: CLIHandler.display_message('Invalid input. Exiting...', Styling.RED) return if not (0 <= selected_topic < len(topics)): CLIHandler.display_message('Invalid input. Exiting...', Styling.RED) return CLIHandler.clear_screen() topic = topics[selected_topic] topic.load_topic(amount_of_questions) topic.show_questions() topic.show_results() if __name__ == '__main__': main() ``` [1]: https://github.com/efficience-it/certification-symfony/tree/7.0 [2]: https://i.sstatic.net/8Ab8mpTK.png [3]: https://i.sstatic.net/H3GlFxcO.png