Skip to main content
add more explanation to suggestions
Source Link
Reinderien
  • 71.2k
  • 5
  • 76
  • 257

You're missing some typehints, such as root: tk.Tk, and questionList: list which needs an inner type.

getResponse() should not accept a root or entry parameter; you can closure-bind to root and textBox in the outer scope.

You should make better use of StringVar and IntVar; tk offers these so that you can talk about the data more than you talk about the user interface.

Don't use the Latin letter x for multiplication. You've correctly used the Unicode character for division; do the same for multiplication.

Avoid calling into random.randint directly; this will make unit testing diffcult. Instead, accept a random generator object. If and when you add unit tests, you can pass it a random generator that has had a constant seed set.

Don't != None; use is not None instead since None is a singleton.

Use the operator module to give you your simple binary function references rather than needing to write out the expressions yourself.

You're missing some typehints, such as root: tk.Tk.

You're missing some typehints, such as root: tk.Tk, and questionList: list which needs an inner type.

getResponse() should not accept a root or entry parameter; you can closure-bind to root and textBox in the outer scope.

You should make better use of StringVar and IntVar; tk offers these so that you can talk about the data more than you talk about the user interface.

Don't use the Latin letter x for multiplication. You've correctly used the Unicode character for division; do the same for multiplication.

Avoid calling into random.randint directly; this will make unit testing diffcult. Instead, accept a random generator object. If and when you add unit tests, you can pass it a random generator that has had a constant seed set.

Don't != None; use is not None instead since None is a singleton.

Use the operator module to give you your simple binary function references rather than needing to write out the expressions yourself.

add oop style
Source Link
Reinderien
  • 71.2k
  • 5
  • 76
  • 257

Closure style

import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


def mode_selection(
    width: int = 300, height: int = 300,
) -> Operation | None:
    def select(selected_option: Operation) -> None:
        nonlocal option
        option = selected_option
        root.destroy()

    root = tk.Tk()
    root.title('Select Mode')
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    for mode in MODES:
        button = tk.Button(
            frame, text=mode.name,
            command=functools.partial(select, mode),
        )
        button.pack(pady=5, fill=tk.X)

    option: Operation | None = None
    root.mainloop()
    return option


def ask_questions(
    questions: Sequence[Question],
    mode: Operation,
    width: int = 300,
    height: int = 200,
) -> list[int]:
    responses: list[int] = []
    question_iter = enumerate(questions, start=1)

    def advance() -> None:
        try:
            question_index, question = next(question_iter)
        except StopIteration:
            root.destroy()
            return

        question_label.config(text=str(question))
        question_counter.config(
            text=f'Question {question_index} out of {len(questions)}',
        )
        answer_var.set(value=0)
        entry.focus_set()
        entry.select_range(0, 'end')

    def get_response(*args) -> None:
        responses.append(answer_var.get())
        advance()

    root = tk.Tk()
    root.title(mode.name)
    root.bind('<Return>', get_response)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    question_counter = tk.Label(frame, font=('Arial', 16))
    question_counter.pack(pady=5)
    question_label = tk.Label(frame, font=('Arial', 20))
    question_label.pack(pady=5)
    answer_var = tk.IntVar(frame, name='answer')
    entry = tk.Entry(frame, textvariable=answer_var)
    entry.pack(pady=5)
    tk.Button(frame, text='Enter', command=get_response).pack(pady=5)

    advance()
    root.mainloop()
    return responses


def show_results(
    questions: Sequence[Question],
    responses: Sequence[int],
    width: int = 300,
    height: int = 250,
    questions_per_page: int = 5,
) -> None:
    answers = [
        Answer(question=question, answer=answer)
        for question, answer in zip(questions, responses)
    ]
    n_correct = sum(1 for answer in answers if answer.is_correct)
    all_correct = n_correct == len(questions)
    paged_answers = itertools.batched(
        (answer for answer in answers if not answer.is_correct),
        n=questions_per_page,
    )

    def next_page(*args) -> None:
        try:
            batch = next(paged_answers)
        except StopIteration:
            root.destroy()
            return

        for label, answer in zip(incorrect_labels, batch):
            label.config(text=str(answer))
        for label in incorrect_labels[len(batch):]:
            label.config(text='')

    root = tk.Tk()
    root.title(f'Score: {n_correct}/{len(questions)}')
    root.bind('<Return>', next_page)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    results_title = tk.Label(
        frame,
        text=f'You missed {"no" if all_correct else "the following"} questions',
    )
    results_title.pack(pady=5)

    incorrect_labels = [
        tk.Label(frame)
        for _ in range(questions_per_page)
    ]
    for label in incorrect_labels:
        label.pack(pady=5)

    tk.Button(frame, text='Continue', command=next_page).pack(pady=5)

    if not all_correct:
        next_page()
    root.mainloop()


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    session_mode = mode_selection()
    if session_mode is None:
        return

    questions = tuple(create_questions(session_mode))
    responses = ask_questions(questions, session_mode)
    show_results(questions=questions, responses=responses)


if __name__ == '__main__':
    run_main()

OOP style

import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


class ModeSelection:
    def __init__(
        self, width: int = 300, height: int = 300,
    ) -> None:
        self.root = tk.Tk()
        self.root.title('Select Mode')
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        for mode in MODES:
            button = tk.Button(
                frame, text=mode.name,
                command=functools.partial(self.select, mode),
            )
            button.pack(pady=5, fill=tk.X)

        self.mode: Operation | None = None

    def select(self, selected_option: Operation) -> None:
        self.mode = selected_option
        self.root.destroy()


class QuestionPrompt:
    def __init__(
        self,
        questions: Sequence[Question],
        width: int = 300, height: int = 200,
    ) -> None:
        self.root = tk.Tk()
        self.root.title(questions[0].operation.name)
        self.root.bind('<Return>', self.get_response)
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        self.counter_var = tk.StringVar(name='counter')
        question_counter = tk.Label(frame, textvariable=self.counter_var, font=('Arial', 16))
        question_counter.pack(pady=5)

        self.question_var = tk.StringVar(name='question')
        question_label = tk.Label(frame, textvariable=self.question_var, font=('Arial', 20))
        question_label.pack(pady=5)

        self.answer_var = tk.IntVar(frame, name='answer')
        self.entry = tk.Entry(frame, textvariable=self.answer_var)
        self.entry.pack(pady=5)

        tk.Button(frame, text='Enter', command=self.get_response).pack(pady=5)

        self.questions = questions
        self.responses: list[int] = []
        self.question_iter = enumerate(questions, start=1)

        self.advance()

    def advance(self) -> None:
        try:
            question_index, question = next(self.question_iter)
        except StopIteration:
            self.root.destroy()
            return

        self.question_var.set(str(question))
        self.counter_var.set(
            f'Question {question_index} out of {len(self.questions)}',
        )
        self.answer_var.set(value=0)
        self.entry.focus_set()
        self.entry.select_range(0, 'end')

    def get_response(self, *args) -> None:
        self.responses.append(self.answer_var.get())
        self.advance()


class Results:
    def __init__(
        self,
        questions: Sequence[Question],
        responses: Sequence[int],
        width: int = 300, height: int = 250,
        questions_per_page: int = 5,
    ) -> None:
        answers = [
            Answer(question=question, answer=answer)
            for question, answer in zip(questions, responses)
        ]
        n_correct = sum(1 for answer in answers if answer.is_correct)
        all_correct = n_correct == len(questions)
        self.paged_answers = itertools.batched(
            (answer for answer in answers if not answer.is_correct),
            n=questions_per_page,
        )

        self.root = tk.Tk()
        self.root.title(f'Score: {n_correct}/{len(questions)}')
        self.root.bind('<Return>', self.next_page)
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        results_title = tk.Label(
            frame,
            text=f'You missed {"no" if all_correct else "the following"} questions',
        )
        results_title.pack(pady=5)

        self.incorrect_vars = [
            tk.StringVar(name=f'incorrect_{i}')
            for i in range(questions_per_page)
        ]

        for var in self.incorrect_vars:
            tk.Label(frame, textvariable=var).pack(pady=5)

        tk.Button(frame, text='Continue', command=self.next_page).pack(pady=5)

        if not all_correct:
            self.next_page()

    def next_page(self, *args) -> None:
        try:
            batch = next(self.paged_answers)
        except StopIteration:
            self.root.destroy()
            return

        for label, answer in zip(self.incorrect_vars, batch):
            label.set(str(answer))
        for label in self.incorrect_vars[len(batch):]:
            label.set('')


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    selection = ModeSelection()
    selection.mainloop()
    if selection.mode is None:
        return

    questions = tuple(create_questions(selection.mode))

    prompt = QuestionPrompt(questions=questions)
    prompt.mainloop()

    results = Results(questions=questions, responses=prompt.responses)
    results.mainloop()


if __name__ == '__main__':
    run_main()
import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


def mode_selection(
    width: int = 300, height: int = 300,
) -> Operation | None:
    def select(selected_option: Operation) -> None:
        nonlocal option
        option = selected_option
        root.destroy()

    root = tk.Tk()
    root.title('Select Mode')
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    for mode in MODES:
        button = tk.Button(
            frame, text=mode.name,
            command=functools.partial(select, mode),
        )
        button.pack(pady=5, fill=tk.X)

    option: Operation | None = None
    root.mainloop()
    return option


def ask_questions(
    questions: Sequence[Question],
    mode: Operation,
    width: int = 300,
    height: int = 200,
) -> list[int]:
    responses: list[int] = []
    question_iter = enumerate(questions, start=1)

    def advance() -> None:
        try:
            question_index, question = next(question_iter)
        except StopIteration:
            root.destroy()
            return

        question_label.config(text=str(question))
        question_counter.config(
            text=f'Question {question_index} out of {len(questions)}',
        )
        answer_var.set(value=0)
        entry.focus_set()
        entry.select_range(0, 'end')

    def get_response(*args) -> None:
        responses.append(answer_var.get())
        advance()

    root = tk.Tk()
    root.title(mode.name)
    root.bind('<Return>', get_response)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    question_counter = tk.Label(frame, font=('Arial', 16))
    question_counter.pack(pady=5)
    question_label = tk.Label(frame, font=('Arial', 20))
    question_label.pack(pady=5)
    answer_var = tk.IntVar(frame, name='answer')
    entry = tk.Entry(frame, textvariable=answer_var)
    entry.pack(pady=5)
    tk.Button(frame, text='Enter', command=get_response).pack(pady=5)

    advance()
    root.mainloop()
    return responses


def show_results(
    questions: Sequence[Question],
    responses: Sequence[int],
    width: int = 300,
    height: int = 250,
    questions_per_page: int = 5,
) -> None:
    answers = [
        Answer(question=question, answer=answer)
        for question, answer in zip(questions, responses)
    ]
    n_correct = sum(1 for answer in answers if answer.is_correct)
    all_correct = n_correct == len(questions)
    paged_answers = itertools.batched(
        (answer for answer in answers if not answer.is_correct),
        n=questions_per_page,
    )

    def next_page(*args) -> None:
        try:
            batch = next(paged_answers)
        except StopIteration:
            root.destroy()
            return

        for label, answer in zip(incorrect_labels, batch):
            label.config(text=str(answer))
        for label in incorrect_labels[len(batch):]:
            label.config(text='')

    root = tk.Tk()
    root.title(f'Score: {n_correct}/{len(questions)}')
    root.bind('<Return>', next_page)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    results_title = tk.Label(
        frame,
        text=f'You missed {"no" if all_correct else "the following"} questions',
    )
    results_title.pack(pady=5)

    incorrect_labels = [
        tk.Label(frame)
        for _ in range(questions_per_page)
    ]
    for label in incorrect_labels:
        label.pack(pady=5)

    tk.Button(frame, text='Continue', command=next_page).pack(pady=5)

    if not all_correct:
        next_page()
    root.mainloop()


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    session_mode = mode_selection()
    if session_mode is None:
        return

    questions = tuple(create_questions(session_mode))
    responses = ask_questions(questions, session_mode)
    show_results(questions=questions, responses=responses)


if __name__ == '__main__':
    run_main()

Closure style

import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


def mode_selection(
    width: int = 300, height: int = 300,
) -> Operation | None:
    def select(selected_option: Operation) -> None:
        nonlocal option
        option = selected_option
        root.destroy()

    root = tk.Tk()
    root.title('Select Mode')
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    for mode in MODES:
        button = tk.Button(
            frame, text=mode.name,
            command=functools.partial(select, mode),
        )
        button.pack(pady=5, fill=tk.X)

    option: Operation | None = None
    root.mainloop()
    return option


def ask_questions(
    questions: Sequence[Question],
    mode: Operation,
    width: int = 300,
    height: int = 200,
) -> list[int]:
    responses: list[int] = []
    question_iter = enumerate(questions, start=1)

    def advance() -> None:
        try:
            question_index, question = next(question_iter)
        except StopIteration:
            root.destroy()
            return

        question_label.config(text=str(question))
        question_counter.config(
            text=f'Question {question_index} out of {len(questions)}',
        )
        answer_var.set(value=0)
        entry.focus_set()
        entry.select_range(0, 'end')

    def get_response(*args) -> None:
        responses.append(answer_var.get())
        advance()

    root = tk.Tk()
    root.title(mode.name)
    root.bind('<Return>', get_response)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    question_counter = tk.Label(frame, font=('Arial', 16))
    question_counter.pack(pady=5)
    question_label = tk.Label(frame, font=('Arial', 20))
    question_label.pack(pady=5)
    answer_var = tk.IntVar(frame, name='answer')
    entry = tk.Entry(frame, textvariable=answer_var)
    entry.pack(pady=5)
    tk.Button(frame, text='Enter', command=get_response).pack(pady=5)

    advance()
    root.mainloop()
    return responses


def show_results(
    questions: Sequence[Question],
    responses: Sequence[int],
    width: int = 300,
    height: int = 250,
    questions_per_page: int = 5,
) -> None:
    answers = [
        Answer(question=question, answer=answer)
        for question, answer in zip(questions, responses)
    ]
    n_correct = sum(1 for answer in answers if answer.is_correct)
    all_correct = n_correct == len(questions)
    paged_answers = itertools.batched(
        (answer for answer in answers if not answer.is_correct),
        n=questions_per_page,
    )

    def next_page(*args) -> None:
        try:
            batch = next(paged_answers)
        except StopIteration:
            root.destroy()
            return

        for label, answer in zip(incorrect_labels, batch):
            label.config(text=str(answer))
        for label in incorrect_labels[len(batch):]:
            label.config(text='')

    root = tk.Tk()
    root.title(f'Score: {n_correct}/{len(questions)}')
    root.bind('<Return>', next_page)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    results_title = tk.Label(
        frame,
        text=f'You missed {"no" if all_correct else "the following"} questions',
    )
    results_title.pack(pady=5)

    incorrect_labels = [
        tk.Label(frame)
        for _ in range(questions_per_page)
    ]
    for label in incorrect_labels:
        label.pack(pady=5)

    tk.Button(frame, text='Continue', command=next_page).pack(pady=5)

    if not all_correct:
        next_page()
    root.mainloop()


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    session_mode = mode_selection()
    if session_mode is None:
        return

    questions = tuple(create_questions(session_mode))
    responses = ask_questions(questions, session_mode)
    show_results(questions=questions, responses=responses)


if __name__ == '__main__':
    run_main()

OOP style

import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


class ModeSelection:
    def __init__(
        self, width: int = 300, height: int = 300,
    ) -> None:
        self.root = tk.Tk()
        self.root.title('Select Mode')
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        for mode in MODES:
            button = tk.Button(
                frame, text=mode.name,
                command=functools.partial(self.select, mode),
            )
            button.pack(pady=5, fill=tk.X)

        self.mode: Operation | None = None

    def select(self, selected_option: Operation) -> None:
        self.mode = selected_option
        self.root.destroy()


class QuestionPrompt:
    def __init__(
        self,
        questions: Sequence[Question],
        width: int = 300, height: int = 200,
    ) -> None:
        self.root = tk.Tk()
        self.root.title(questions[0].operation.name)
        self.root.bind('<Return>', self.get_response)
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        self.counter_var = tk.StringVar(name='counter')
        question_counter = tk.Label(frame, textvariable=self.counter_var, font=('Arial', 16))
        question_counter.pack(pady=5)

        self.question_var = tk.StringVar(name='question')
        question_label = tk.Label(frame, textvariable=self.question_var, font=('Arial', 20))
        question_label.pack(pady=5)

        self.answer_var = tk.IntVar(frame, name='answer')
        self.entry = tk.Entry(frame, textvariable=self.answer_var)
        self.entry.pack(pady=5)

        tk.Button(frame, text='Enter', command=self.get_response).pack(pady=5)

        self.questions = questions
        self.responses: list[int] = []
        self.question_iter = enumerate(questions, start=1)

        self.advance()

    def advance(self) -> None:
        try:
            question_index, question = next(self.question_iter)
        except StopIteration:
            self.root.destroy()
            return

        self.question_var.set(str(question))
        self.counter_var.set(
            f'Question {question_index} out of {len(self.questions)}',
        )
        self.answer_var.set(value=0)
        self.entry.focus_set()
        self.entry.select_range(0, 'end')

    def get_response(self, *args) -> None:
        self.responses.append(self.answer_var.get())
        self.advance()


class Results:
    def __init__(
        self,
        questions: Sequence[Question],
        responses: Sequence[int],
        width: int = 300, height: int = 250,
        questions_per_page: int = 5,
    ) -> None:
        answers = [
            Answer(question=question, answer=answer)
            for question, answer in zip(questions, responses)
        ]
        n_correct = sum(1 for answer in answers if answer.is_correct)
        all_correct = n_correct == len(questions)
        self.paged_answers = itertools.batched(
            (answer for answer in answers if not answer.is_correct),
            n=questions_per_page,
        )

        self.root = tk.Tk()
        self.root.title(f'Score: {n_correct}/{len(questions)}')
        self.root.bind('<Return>', self.next_page)
        self.mainloop = self.root.mainloop
        set_geometry(root=self.root, width=width, height=height)
        frame = create_frame(self.root)

        results_title = tk.Label(
            frame,
            text=f'You missed {"no" if all_correct else "the following"} questions',
        )
        results_title.pack(pady=5)

        self.incorrect_vars = [
            tk.StringVar(name=f'incorrect_{i}')
            for i in range(questions_per_page)
        ]

        for var in self.incorrect_vars:
            tk.Label(frame, textvariable=var).pack(pady=5)

        tk.Button(frame, text='Continue', command=self.next_page).pack(pady=5)

        if not all_correct:
            self.next_page()

    def next_page(self, *args) -> None:
        try:
            batch = next(self.paged_answers)
        except StopIteration:
            self.root.destroy()
            return

        for label, answer in zip(self.incorrect_vars, batch):
            label.set(str(answer))
        for label in self.incorrect_vars[len(batch):]:
            label.set('')


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    selection = ModeSelection()
    selection.mainloop()
    if selection.mode is None:
        return

    questions = tuple(create_questions(selection.mode))

    prompt = QuestionPrompt(questions=questions)
    prompt.mainloop()

    results = Results(questions=questions, responses=prompt.responses)
    results.mainloop()


if __name__ == '__main__':
    run_main()
Source Link
Reinderien
  • 71.2k
  • 5
  • 76
  • 257

TkWindows is not a useful class; it has no member variables. Either convert it to a module, or flatten it to a set of functions in the global namespace.

You're missing some typehints, such as root: tk.Tk.

You already have import tkinter as tk; you should keep it and remove your from tkinter import so that all of the library symbols can stay namespace-qualified.

It doesn't help to represent the window dimensions as a tuple - just represent them as individual variables. Write a utility method to apply your geometry string instead of copying and pasting that code.

You haven't made a very consistent decision between representing state in classes and representing state in closure variables (nonlocal). I show a more consistent application of closures, plus some immutable classes to represent questions and answers.

Don't int(x/2); use floor division x // 2.

Lots of other things to improve, but this is a start:

import functools
import itertools
import operator
import random
import tkinter as tk
from typing import Callable, Iterator, Sequence

from mypy.build import NamedTuple


class Operation(NamedTuple):
    name: str
    symbol: str
    fun: Callable[[int, int], int]


MODES = (
    Operation(name='Addition', symbol='+', fun=operator.add),
    Operation(name='Subtraction', symbol='-', fun=operator.sub),
    Operation(name='Multiplication', symbol='×', fun=operator.mul),
    Operation(name='Division', symbol='÷', fun=operator.truediv),
)


class Question(NamedTuple):
    operation: Operation
    a: int
    b: int

    @classmethod
    def random(cls, operation: Operation, rand: random.Random) -> 'Question':
        return cls(
            operation=operation,
            a=rand.randint(1, 10),
            b=rand.randint(1, 10),
        )

    def __str__(self) -> str:
        return f'{self.a} {self.operation.symbol} {self.b}'

    @property
    def answer(self) -> int:
        return self.operation.fun(self.a, self.b)


class Answer(NamedTuple):
    question: Question
    answer: int

    @property
    def is_correct(self) -> bool:
        return self.answer == self.question.answer

    def __str__(self) -> str:
        return (
            f'{self.question} = {self.question.answer}'
            f'   |   Your answer was: {self.answer}'
        )


def create_frame(root: tk.Tk) -> tk.Frame:
    frame = tk.Frame(root)
    frame.pack(expand=True)
    root.after(ms=1, func=root.focus_force)
    return frame


def set_geometry(
    root: tk.Tk, width: int, height: int,
) -> None:
    left = (root.winfo_screenwidth() - width)//2
    top = (root.winfo_screenheight() - height)//2
    root.geometry(f'{width}x{height}+{left}+{top}')


def mode_selection(
    width: int = 300, height: int = 300,
) -> Operation | None:
    def select(selected_option: Operation) -> None:
        nonlocal option
        option = selected_option
        root.destroy()

    root = tk.Tk()
    root.title('Select Mode')
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    for mode in MODES:
        button = tk.Button(
            frame, text=mode.name,
            command=functools.partial(select, mode),
        )
        button.pack(pady=5, fill=tk.X)

    option: Operation | None = None
    root.mainloop()
    return option


def ask_questions(
    questions: Sequence[Question],
    mode: Operation,
    width: int = 300,
    height: int = 200,
) -> list[int]:
    responses: list[int] = []
    question_iter = enumerate(questions, start=1)

    def advance() -> None:
        try:
            question_index, question = next(question_iter)
        except StopIteration:
            root.destroy()
            return

        question_label.config(text=str(question))
        question_counter.config(
            text=f'Question {question_index} out of {len(questions)}',
        )
        answer_var.set(value=0)
        entry.focus_set()
        entry.select_range(0, 'end')

    def get_response(*args) -> None:
        responses.append(answer_var.get())
        advance()

    root = tk.Tk()
    root.title(mode.name)
    root.bind('<Return>', get_response)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    question_counter = tk.Label(frame, font=('Arial', 16))
    question_counter.pack(pady=5)
    question_label = tk.Label(frame, font=('Arial', 20))
    question_label.pack(pady=5)
    answer_var = tk.IntVar(frame, name='answer')
    entry = tk.Entry(frame, textvariable=answer_var)
    entry.pack(pady=5)
    tk.Button(frame, text='Enter', command=get_response).pack(pady=5)

    advance()
    root.mainloop()
    return responses


def show_results(
    questions: Sequence[Question],
    responses: Sequence[int],
    width: int = 300,
    height: int = 250,
    questions_per_page: int = 5,
) -> None:
    answers = [
        Answer(question=question, answer=answer)
        for question, answer in zip(questions, responses)
    ]
    n_correct = sum(1 for answer in answers if answer.is_correct)
    all_correct = n_correct == len(questions)
    paged_answers = itertools.batched(
        (answer for answer in answers if not answer.is_correct),
        n=questions_per_page,
    )

    def next_page(*args) -> None:
        try:
            batch = next(paged_answers)
        except StopIteration:
            root.destroy()
            return

        for label, answer in zip(incorrect_labels, batch):
            label.config(text=str(answer))
        for label in incorrect_labels[len(batch):]:
            label.config(text='')

    root = tk.Tk()
    root.title(f'Score: {n_correct}/{len(questions)}')
    root.bind('<Return>', next_page)
    set_geometry(root=root, width=width, height=height)
    frame = create_frame(root)

    results_title = tk.Label(
        frame,
        text=f'You missed {"no" if all_correct else "the following"} questions',
    )
    results_title.pack(pady=5)

    incorrect_labels = [
        tk.Label(frame)
        for _ in range(questions_per_page)
    ]
    for label in incorrect_labels:
        label.pack(pady=5)

    tk.Button(frame, text='Continue', command=next_page).pack(pady=5)

    if not all_correct:
        next_page()
    root.mainloop()


def create_questions(
    operation: Operation,
    num_of_questions: int = 15,
    rand: random.Random | None = None,
) -> Iterator[Question]:
    if rand is None:
        rand = random.Random()
    for _ in range(num_of_questions):
        yield Question.random(
            operation=operation, rand=rand,
        )


def run_main() -> None:
    session_mode = mode_selection()
    if session_mode is None:
        return

    questions = tuple(create_questions(session_mode))
    responses = ask_questions(questions, session_mode)
    show_results(questions=questions, responses=responses)


if __name__ == '__main__':
    run_main()