Theory-based quiz grader using similarity I wrote a fairly simple program that grades theory-based quizzes. It is fairly straightforward. I would love to get insights and ideas on how it might be improved.
consine_similarity.py
&quot;&quot;&quot;This module uses consine distance to check the similarity between two sentences&quot;&quot;&quot;

from nltk.corpus import stopwords
from nltk.cluster.util import cosine_distance
from nltk.tokenize import word_tokenize

def consine_similarity(sent1: str, sent2: str) -&gt; float:
    &quot;&quot;&quot;Consine similarity between two sentences

        sent1: str
        sent2: str
    &quot;&quot;&quot;

    sent1_list = word_tokenize(sent1)
    sent2_list = word_tokenize(sent2)
    stop_words = stopwords.words('english')
    all_words = list(set(sent1_list + sent2_list))

    vector1 = [0] * len(all_words)
    vector2 = [0] * len(all_words)

    for word in sent1_list:
        if word in stop_words:
            continue
        vector1[all_words.index(word)] += 1

    for word in sent2_list:
        if word in stop_words:
            continue
        vector2[all_words.index(word)] += 1

    return 1 - cosine_distance(vector1, vector2)


quiz.py
&quot;&quot;&quot;Theory-based quiz application&quot;&quot;&quot;

from dataclasses import dataclass
from random import shuffle
from consine_similarity import consine_similarity

@dataclass
class Quiz:
    &quot;&quot;&quot;Quiz data&quot;&quot;&quot;

    quiz: str
    answer: str

    def __str__(self):
        return '{}'.format(self.quiz)

def start_quiz(quiz_list: list) -&gt; None:
    &quot;&quot;&quot;Start the quiz application&quot;&quot;&quot;

    shuffle(quiz_list)
    consine_list = []

    for quiz in quiz_list:
        print(quiz)
        answer = input('&gt; ')
        consine_list.append(consine_similarity(quiz.answer, answer))
        print()

    grade = sum(consine_list) / len(consine_list) * 100

    print('Your grade is {:.2f}%'.format(grade))

if __name__ == &quot;__main__&quot;:
    QUIZ_LIST = [
        Quiz(quiz='What is a computer?',
             answer='An electronic device for storing and processing data, \
                 typically in binary form, according to instructions given to it'),
        Quiz(quiz='What are logic gates?',
             answer='These are the basic building blocks of any digital system. It is an \
                  electronic circuit having one or more than one input and only one output'),
        Quiz(quiz='What is BIOS',
             answer='This is firmware used to perform hardware initialization during the booting \
                 process and to provide runtime services for operating systems and programs'),
    ]
    start_quiz(QUIZ_LIST)
There is no need to include stop words in the dictionary if they are filtered out of the vectors anyway. Thus, tokenization and stop words removal can be considered as a single step. Secondly, the vectors can be initialized with the correct values immediately by taking advantage of collections.Counter
from collections import Counter
from typing import List
from nltk.corpus import stopwords
from nltk.cluster.util import cosine_distance
from nltk.tokenize import word_tokenize

def tokenize_without_stop_words(sentence:str) -&gt; List[str]:
    return [
        word
        for word in word_tokenize(sentence)
        if word not in stopwords.words('english')
    ]

def cosine_similarity(first_sentence: str, second_sentence: str) -&gt; float:
    first_words = tokenize_without_stop_words(first_sentence)
    second_words = tokenize_without_stop_words(second_sentence)

    dictionary = list(set(first_words+second_words))

    def encode(words: List[str]) -&gt; List[int]:
        word_counts = Counter(words)
        return [
            word_counts[word]
            for word in dictionary
        ]

    first_vector = encode(first_words)
    second_vector = encode(second_words)

    return 1 - cosine_distance(first_vector, second_vector)