# Multiplication or addition of decimals or integers for prehighschool; non deterministic testing

I'm creating a simple android app for pre-highschool students which teaches the very basics of addition or multiplication of integers or decimals.

The part of the program in this question is aimed at allowing them to practice without the need of a teacher giving them exercises and checking their results.

Note: This is the first time I m using unit tests so I'm not experienced at unit-testing at all.

The following are of great interest to me:

1. Unit tests:

• What should I improve? I don't aim for 100% code coverage since that feels more of a fixation on following blindly a "rule" without the actual need for it (but of course I might be wrong).
• Since my methods contain random.random() numbers I was thinking of creating tests that test a method millions or billions of times (e.g. assertIsInstance(_int_term(), int) inside a for _ in range(10**8) loop). This would very strongly indicate (but not with absolute certainty) that the methods work correctly. Should I create such tests?
• Methods with optional parameters, e.g. Terms._int_term(max_val): The parameters are made optional only because I wanted to be able to test the method easily (that is, call it without the need to instantiate Terms() class. If I were not using unit tests, I would simply make it a non static method, and would remove the parameter completely. Is this a bad practice?
2. Docstrings: Are more docstrings needed? Or do good method names suffice? The reason I didn't create dosctings for everything is that they could go stale in case I change code without also updating all the related docstrings.

Of course any other comments unrelated to the above 2 points are also very welcome.

## Directory tree

• project_dir
• main.py
• tests
• test_main
• test_terms.py

## Code

Main module:

"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+2-4" ,        "-2"
"(-2)(+7)",     "-14"
"-1.60-2.04",   "-3.64"

The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.

Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:

"2-4",          # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)",       # Deviates (same as example above)
"+2-4(-5)",     # Deviates since it combines addition and multiplication

"""

import decimal
from random import randint, choice, random

class Terms(object):

MIN_TERMS_COUNT = 2
MAX_TERMS_COUNT = 3
TERMS_TYPES = {'int', 'float'}
MAX_ABS_VALUE = 10

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}

TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)

def __init__(self, difficulty_lvl):
self.difficulty_lvl = difficulty_lvl
self.terms_count = self.DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[self.difficulty_lvl]['terms_count']
self.terms_type = self.DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[self.difficulty_lvl]['terms_type']

@staticmethod
def _int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)

@staticmethod
def _float_term(max_val=MAX_ABS_VALUE):
return random() * max_val

@staticmethod
def final_term(term_without_sign):
"""
Creates a single term.

:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])

if sign == '-':
final_term = -1 * term_without_sign
else:
final_term = term_without_sign

return final_term

def all_terms(self):
lst = []

if self.terms_type == 'int':
func = self._int_term
elif self.terms_type == 'float':
func = self._float_term
else:
raise NotImplemented('{}'.format(self.terms_type))

for _ in range(self.terms_count):
term_without_sign = func()
final_term = self.final_term(term_without_sign=term_without_sign)
lst.append(final_term)

return lst

"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""

def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type

@staticmethod
def round_single_term_2_decimals(given_float):

num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)

def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.

:return: (list)
"""

lst = []

for t in self.terms_as_numbers:

if isinstance(t, int):
lst.append(t)

else:
t = self.round_single_term_2_decimals(given_float=t)
lst.append(t)

return lst

def terms_as_strings(self):
lst = []

for t in self.terms_to_rounded_numbers():

if t > 0:
lst.append('+{}'.format(t))

elif t == 0:
random_sign = choice(['+', '-'])
lst.append('{sign}{term}'.format(sign=random_sign, term=t))

else:
lst.append(str(t))

return lst

def operation_str(self):
"""
Creates the operation string presented to the user.

:return: (str)
"""

terms_as_strings = self.terms_as_strings()

op_str = ''.join(terms_as_strings)

elif self.op_type == 'multiplication':
op_str = ''
for t in terms_as_strings:
op_str += '({})'.format(t)

else:
raise NotImplemented('{}'.format(self.op_type))

return op_str

result = 0
for t in self.terms_to_rounded_numbers():
result += t

elif self.op_type == 'multiplication':

result = 1
for t in self.terms_to_rounded_numbers():
result *= t

else:
raise NotImplemented('{}'.format(self.op_type))

return result

if __name__ == '__main__':

# VISUAL TESTS
if 1:

TOTAL_DIFFICULTY_LVLS = Terms.TOTAL_DIFFICULTY_LVLS

# --------------------------------------------------
# all_terms()
print('\n'+'-'*80)
print('TERMS')

def print_all_terms(difficulty_lvl):
print('\nDifficulty {}'.format(difficulty_lvl))
# (terms' lists printed for each difficulty lvl)
terms_lsts_count = 3
for _ in range(terms_lsts_count):
print(Terms(difficulty_lvl=difficulty_lvl).all_terms())

# (tests all difficulties)
for d in range(1, TOTAL_DIFFICULTY_LVLS + 1):
print_all_terms(difficulty_lvl=d)

# --------------------------------------------------
print('\n'+'-'*80)
print('OPERATION STRING')
for d in range(1, TOTAL_DIFFICULTY_LVLS + 1):
print('\nDifficulty: {}'.format(d))

for _ in range(3):
question = inst.operation_str()
msg = '{q} = {a}'.format(q=question, a=answer)
print(msg)


Test module test_terms.py:

from unittest import TestCase

class TestDifficultyMap(TestCase):

def setUp(self):

import main

self.Term = main.Terms
self.difficulty_dct = self.Term.DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP
self.terms_counts_found = {v['terms_count'] for v in self.difficulty_dct.values()}
self.terms_types_found = {v['terms_type'] for v in self.difficulty_dct.values()}

def test_min_terms_count_DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP(self):

min_terms_count_allowed = self.Term.MIN_TERMS_COUNT
min_terms_count_found = min(self.terms_counts_found)

self.assertGreaterEqual(
min_terms_count_found, min_terms_count_allowed
)

def test_max_terms_count_DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP(self):

max_terms_count_allowed = self.Term.MAX_TERMS_COUNT
max_terms_count_found = max(self.terms_counts_found)

self.assertGreaterEqual(
max_terms_count_found, max_terms_count_allowed
)

def test_terms_types_DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP(self):

self.assertEquals(
self.terms_types_found, self.Term.TERMS_TYPES
)

class TestIntAndFloatTerm(TestCase):

def setUp(self):

import main
self.Term = main.Terms

# _int_term
def test__int_term(self):
self.assertEqual(
self.Term._int_term(max_val=0), 0
)

def test_is_int__int_term(self):
self.assertIsInstance(
self.Term._int_term(max_val=1), int
)

# _float_term
def test__float_term(self):
max_val = 15
term_val = self.Term._float_term(max_val=max_val)
self.assertTrue(0 <= term_val <= max_val)

def test_is_float__float_term(self):
self.assertIsInstance(
self.Term._float_term(max_val=1), float
)

class TestAllTermsMethod(TestCase):

def setUp(self):

import main
self.Term = main.Terms
self.difficulties_available = self.Term.DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP.keys()

def test_len_above_min_all_terms(self):
inst = self.Term(difficulty_lvl=1)

min_terms_count_allowed = self.Term.MIN_TERMS_COUNT
terms_lst = inst.all_terms()

self.assertGreaterEqual(len(terms_lst), min_terms_count_allowed)

def _contains_x_type_all_terms(self, x_type):
found_type = False

for lvl in self.difficulties_available:
inst = self.Term(difficulty_lvl=lvl)

terms = inst.all_terms()

for t in terms:
if type(t) is x_type:
found_type = True

self.assertTrue(found_type, msg='Did not find {}.'.format(x_type))

def test_contains_float_all_terms(self):
self._contains_x_type_all_terms(x_type=float)

def test_contains_int_all_terms(self):
self._contains_x_type_all_terms(x_type=int)


Test module test_questionandanswer.py:

from unittest import TestCase

class TestQuestion(TestCase):

def setUp(self):

from decimal import Decimal

self.positive_float_to_expected_2nd_decimal_rounded = {
0.0049: Decimal('0.00'),
0: Decimal('0'),
0.005: Decimal('0.01'),
2.255: Decimal('2.26'),
9.999: Decimal('10'),
0.004: Decimal('0'),
}

self.negative_float_to_expected_2nd_decimal_rounded = {
-k: -v for k, v in self.positive_float_to_expected_2nd_decimal_rounded.items()}

def test_positive_round_single_term_2_decimals(self):

for given_float, expected in self.positive_float_to_expected_2nd_decimal_rounded.items():

def test_negative_round_single_term_2_decimals(self):

for given_float, expected in self.negative_float_to_expected_2nd_decimal_rounded.items():


Style

Python has a style guide called PEP 8 which is definitly worth reading and and worth following if you do not have good reasons not to. In you case, your usage of empty lines for instance is not compliant to PEP8. You'll find tools online to check your code compliancy to PEP8 in a automated way if you want to.

Code organisation

You've written your code in classes which is sometimes a good thing. Sometimes, you don't need a class.

In any case, most of the things in the Terms class could be extracted out of it (also removing the need for many self.). By the way, it may be interesting to point out that variables defined at the class levels can be pretty complicated and are not entirely needed and could be simple global variables.

Also, you have variables that are not used at all and you should get rid of them.

Once everything than can be moved out of the class (and renamed) is moved out, you get:

"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+2-4" ,        "-2"
"(-2)(+7)",     "-14"
"-1.60-2.04",   "-3.64"

The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.

Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:

"2-4",          # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)",       # Deviates (same as example above)
"+2-4(-5)",     # Deviates since it combines addition and multiplication

"""

import decimal
from random import randint, choice, random, seed

MAX_ABS_VALUE = 10

def rand_int_term(max_val=MAX_ABS_VALUE):
return randint(0, max_val)

def rand_float_term(max_val=MAX_ABS_VALUE):
return random() * max_val

def create_term(term_without_sign):
"""
Creates a single term.

:param term_without_sign:
:return: (num)
"""
sign = choice(['-', '+'])

if sign == '-':
term = -1 * term_without_sign
else:
term = term_without_sign

return term

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type='int'
),
2: dict(
terms_count=3,
terms_type='int'
),
3: dict(
terms_count=2,
terms_type='float'
)
}

TOTAL_DIFFICULTY_LVLS = len(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP)

class Terms(object):

def __init__(self, difficulty_lvl):
terms_data = DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP[difficulty_lvl]
self.terms_count = terms_data['terms_count']
self.terms_type = terms_data['terms_type']

def all_terms(self):
lst = []

if self.terms_type == 'int':
func = rand_int_term
elif self.terms_type == 'float':
func = rand_float_term
else:
raise NotImplemented('{}'.format(self.terms_type))

for _ in range(self.terms_count):
term_without_sign = func()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)

return lst

"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""

def __init__(self, difficulty_lvl, op_type):
self.terms_as_numbers = Terms(difficulty_lvl=difficulty_lvl).all_terms()
self.op_type = op_type

@staticmethod
def round_single_term_2_decimals(given_float):

num = decimal.Decimal(str(given_float))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)

def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.

:return: (list)
"""

lst = []

for t in self.terms_as_numbers:

if isinstance(t, int):
lst.append(t)

else:
t = self.round_single_term_2_decimals(given_float=t)
lst.append(t)

return lst

def terms_as_strings(self):
lst = []

for t in self.terms_to_rounded_numbers():

if t > 0:
lst.append('+{}'.format(t))

elif t == 0:
random_sign = choice(['+', '-'])
lst.append('{sign}{term}'.format(sign=random_sign, term=t))

else:
lst.append(str(t))

return lst

def operation_str(self):
"""
Creates the operation string presented to the user.

:return: (str)
"""

terms_as_strings = self.terms_as_strings()

op_str = ''.join(terms_as_strings)

elif self.op_type == 'multiplication':
op_str = ''
for t in terms_as_strings:
op_str += '({})'.format(t)

else:
raise NotImplemented('{}'.format(self.op_type))

return op_str

result = 0
for t in self.terms_to_rounded_numbers():
result += t

elif self.op_type == 'multiplication':

result = 1
for t in self.terms_to_rounded_numbers():
result *= t

else:
raise NotImplemented('{}'.format(self.op_type))

return result

if __name__ == '__main__':

# VISUAL TESTS
if 1:

seed(42)  # Tmporal addition from Josay to have same results on different runs

# --------------------------------------------------
# all_terms()
print('\n'+'-'*80)
print('TERMS')

def print_all_terms(difficulty_lvl):
print('\nDifficulty {}'.format(difficulty_lvl))
# (terms' lists printed for each difficulty lvl)
terms_lsts_count = 3
for _ in range(terms_lsts_count):
print(Terms(difficulty_lvl=difficulty_lvl).all_terms())

# (tests all difficulties)
for d in range(1, TOTAL_DIFFICULTY_LVLS + 1):
print_all_terms(difficulty_lvl=d)

# --------------------------------------------------
print('\n'+'-'*80)
print('OPERATION STRING')
for d in range(1, TOTAL_DIFFICULTY_LVLS + 1):
print('\nDifficulty: {}'.format(d))

for _ in range(3):
question = inst.operation_str()
msg = '{q} = {a}'.format(q=question, a=answer)
print(msg)


(I just realised that I forgot to extract stuff from QuestionAndAnswer but you get the spirit.)

Avoid useless string manipulation

You have a dictionnary where values are strings and these strings are later one used to retrieve the relevant function. It may be easier to just map keys to functions.

You simply get :

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = {
1: dict(
terms_count=2,
terms_type=rand_int_term
),
2: dict(
terms_count=3,
terms_type=rand_int_term
),
3: dict(
terms_count=2,
terms_type=rand_float_term
)
}

...

def all_terms(self):
lst = []
for _ in range(self.terms_count):
term_without_sign = self.terms_type()
term = create_term(term_without_sign=term_without_sign)
lst.append(term)


(term_type probably needs to be renamed, I'll leave this to you)

List comprehension

You can use list comprehension to rewrite all_terms in a more concise/pythonic way :

def all_terms(self):
return [ create_term(term_without_sign=self.terms_type())
for _ in range(self.terms_count)]


Remove duplicated code

In terms_to_rounded_numbers, you have :

        if isinstance(t, int):
lst.append(t)
else:
t = round_single_term_2_decimals(given_float=t)
lst.append(t)


which could simply be written:

        if not isinstance(t, int):
t = round_single_term_2_decimals(given_float=t)
lst.append(t)


But here again, list comprehension can be used:

Using the right data type

At the moment, difficulties are stored in a dictionnary mapping consecutive integers to values. This looks a lot like a list (except that the indexing starts from 1 instead of 0).

Variable names

given_float is a weird parameter name for a function. Maybe float_arg would make more sense.

Avoid string concatenation

From PEP 8:

For example, do not rely on CPython's efficient implementation of in-place string concatenation for statements in the form a += b or a = a + b . This optimization is fragile even in CPython (it only works for some types) and isn't present at all in implementations that don't use refcounting. In performance sensitive parts of the library, the ''.join() form should be used instead. This will ensure that concatenation occurs in linear time across various implementations.

Thus, you could write:

    elif self.op_type == 'multiplication':
op_str = ''.join('({})'.format(t) for t in terms_as_strings)


Avoid useless string manipulation (again)

In create_term, you call sign = random.choice(['-', '+']) to get the character corresponding to a sign. It may be easier to do:

return term_without_sign * random.choice([-1, 1])


This could also be applied to terms_as_strings but I do not quite understand what you were trying to do.

List comprehension (again)

It would probably be worth extracting the conversion from terms_as_strings in a function on its own and then use it in a list comprehension.

def term_as_string(t):
if t > 0:
return '+{}'.format(t)
elif t == 0:
random_sign = random.choice(['+', '-'])
return '{sign}{term}'.format(sign=random_sign, term=t)
else:
return str(t)

....

def terms_as_strings(self):
return [term_as_string(t) for t in self.terms_to_rounded_numbers()]


Avoid string manipulation (again)

At the moment, you have a list of string representing operations types and in a few places, you look if the string you are handling corresponds to any of the string you know and perform the relevant computations. This has multiple problems: you have a lot of repeated code and it is easy to forgot a place if you want to add an operation. It would make sense to use a data structure with all the relevant pieces of information. A tuple and more particularly, a namedtuple seems relevant for this task.

import operator
from collections import namedtuple
Operation = namedtuple('Operation', 'name symbol func neutral format')  # name and symbol do not seems to be useful at the moment

OPERATIONS_TYPES = {
Operation('multiplication', '*', operator.mul, 1, '{}'),
}

...

def operation_str(self):
"""
Creates the operation string presented to the user.

:return: (str)
"""
terms_as_strings = self.terms_as_strings()
return ''.join(self.op_type.format.format(t) for t in terms_as_strings)

op = self.op_type
result = op.neutral
for t in self.terms_to_rounded_numbers():
result = op.func(result, t)
return result


Using the right data type (again with actual solution)

As said before, the fact that DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP is a bit puzzling as you are accessing it using range and getting item at the given index. A list is probably the right thing to use here. Also, this is not really pythonic nor efficient. I'd recommand reading/watching Ned Batchelder's presentation : "Loop Like A Native".

In your case, instead of getting the element at difficulty_lvl in Terms, it may be easier to provide it directly to the constructor. Doing the same thing also in QuestionAndAnswser, you can then simply look over the list. Then, if you do need the index, you can use enumerate.

All this requires many changes everywhere but once done, you can get rid of the TOTAL_DIFFICULTY_LVLS variable.

Remaining code looks like :

"""
Used for the creation of "questions" for the user,
along with the expected answer (that is, the result of the operation)

Some examples of "questions" and their "answers":

"+2-4" ,        "-2"
"(-2)(+7)",     "-14"
"-1.60-2.04",   "-3.64"

The program should be aimed specifically at teaching
the very basics of addition or multiplication of integers or decimals,
in a scaling difficulty.

Exercises that deviate from those specific concepts should be avoided.
Examples of what should NOT be implemented:

"2-4",          # Deviates (slightly) since it also teaches that 2 == +2
"-2(+7)",       # Deviates (same as example above)
"+2-4(-5)",     # Deviates since it combines addition and multiplication

"""

import decimal
import random
import operator
from collections import namedtuple

MAX_ABS_VALUE = 10

def rand_int_term(max_val=MAX_ABS_VALUE):
return random.randint(0, max_val)

def rand_float_term(max_val=MAX_ABS_VALUE):
return random.random() * max_val

def create_term(term_without_sign):
"""
Creates a single term.

:param term_without_sign:
:return: (num)
"""
return term_without_sign * random.choice([-1, 1])

DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP = [
dict(
terms_count=2,
terms_type=rand_int_term
),
dict(
terms_count=3,
terms_type=rand_int_term
),
dict(
terms_count=2,
terms_type=rand_float_term
)
]

class Terms(object):

def __init__(self, difficulty):
self.terms_count = difficulty['terms_count']
self.terms_type =  difficulty['terms_type']

def all_terms(self):
return [ create_term(term_without_sign=self.terms_type())
for _ in range(self.terms_count)]

Operation = namedtuple('Operation', 'name symbol func neutral format')

OPERATIONS_TYPES = {
Operation('multiplication', '*', operator.mul, 1, '{}'),
}

def round_single_term_2_decimals(float_arg):
num = decimal.Decimal(str(float_arg))
return num.quantize(decimal.Decimal('.01'), decimal.ROUND_HALF_UP)

def term_as_string(t):
if t > 0:
return '+{}'.format(t)
elif t == 0:
random_sign = random.choice(['+', '-'])
return '{sign}{term}'.format(sign=random_sign, term=t)
else:
return str(t)

"""
Based on difficulty and operation type,
creates terms (either float or ints) which are then rounded to allow
specific number of decimals.
"""

def __init__(self, difficulty, op_type):
self.terms_as_numbers = Terms(difficulty=difficulty).all_terms()
self.op_type = op_type

def terms_to_rounded_numbers(self):
"""
Ensures all terms have an appropriate number of decimals.

:return: (list)
"""
return [t if isinstance(t, int) else round_single_term_2_decimals(float_arg=t)
for t in self.terms_as_numbers]

def terms_as_strings(self):
return [term_as_string(t) for t in self.terms_to_rounded_numbers()]

def operation_str(self):
"""
Creates the operation string presented to the user.

:return: (str)
"""
terms_as_strings = self.terms_as_strings()
return ''.join(self.op_type.format.format(t) for t in terms_as_strings)

op = self.op_type
result = op.neutral
for t in self.terms_to_rounded_numbers():
result = op.func(result, t)
return result

if __name__ == '__main__':

random.seed(42)  # Temporal addition from Josay to have same results on different runs
newline = '\n'+'-'*80

# --------------------------------------------------
# all_terms()
print(newline)
print('TERMS')

# (tests all difficulties)
for i, difficulty in enumerate(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP, start=1):
print('\nDifficulty {}'.format(i))
for _ in range(3):
print(Terms(difficulty=difficulty).all_terms())

# --------------------------------------------------
print(newline)
print('OPERATION STRING')
for i, difficulty in enumerate(DIFFICULTY_TO_TERMS_COUNT_AND_OP_TYPE_MAP, start=1):
print('\nDifficulty: {}'.format(i))
for operation in OPERATIONS_TYPES:
for _ in range(3):

• I'm using PyCharm and thankfully it guides me in the PEP8 direction most of the time; i do agree that some of my empty lines are redundant though. The namedtuple() suggestion is amazing since it forces specific keys and allows easy "lookup", and i think can be used for the DIFFICULTY_MAP elements as well. Unused vars and duplicate code was an issue as you pointed out. I do have some objections on string join() though, comprehensions instead of for loops, and converting DIFFICULTY_MAP to a list. It feels like premature optimization in some cases and [..TO BE CONTINUED] – user Jul 22 '16 at 17:22
• ... going to somewhat less readable code. E.g. my top priority is having readable code which string concatenation accomplishes better than join(). Although I do agree that in general the latter will be faster, but specifically for my use case I doubt any of the Term-creation functionality would be a bottleneck (additionally user will be spending a lot of time between requesting new terms). – user Jul 22 '16 at 17:28
• I m uncomfortable with DIFFICULTY_MAP suggestion since it contains the implicit assumption that difficulties are ordered, e.g. DIFFICULTY_MAP[0] corresponds to difficulty_lvl==1. – user Jul 22 '16 at 17:37