Repository: microsoft/electionguard-python

Issue:
error handling for words.py
`src/electionguard/words.py`: the methods here don't handle bad input, and will instead implicitly raise errors or have undesirable behavior. Perhaps these should return `Optional` results, and require error checking.

Discussion:

Patch:
diff --git a/src/electionguard/tracker.py b/src/electionguard/tracker.py
index 751b8f5..0560a9c 100644
--- a/src/electionguard/tracker.py
+++ b/src/electionguard/tracker.py
@@ -1,4 +1,4 @@
-from typing import List
+from typing import List, Optional
 
 from .hash import hash_elems
 from .group import ElementModQ, q_to_bytes, bytes_to_q
@@ -32,29 +32,39 @@ def get_rotating_tracker_hash(
 
 def tracker_hash_to_words(
     tracker_hash: ElementModQ, seperator: str = DEFAULT_SEPERATOR
-) -> str:
+) -> Optional[str]:
     """
     Convert tracker has to human readable / friendly words
     :param hash: Tracker hash
-    :return: Human readable tracker string
+    :return: Human readable tracker string or None
     """
 
     segments = q_to_bytes(tracker_hash)
-    words = [get_word(value) for value in segments]
-    # TODO Reduce length of segments
+    words: List[str] = []
+    for value in segments:
+        word = get_word(value)
+        if word is None:
+            return None
+        words.append(word)
+    # FIXME ISSUE #82 Minimize length of tracker
     return seperator.join(words)
 
 
 def tracker_words_to_hash(
     tracker_words: str, seperator: str = DEFAULT_SEPERATOR
-) -> ElementModQ:
+) -> Optional[ElementModQ]:
     """
     Convert tracker from human readable / friendly words to hash
     :param tracker_words: Tracker words
     :param seperator: Seperator used between words
-    :return: Tracker hash
+    :return: Tracker hash or None
     """
     words = tracker_words.split(seperator)
-    int_values = [get_index_from_word(word) for word in words]
+    int_values: List[int] = []
+    for word in words:
+        index = get_index_from_word(word)
+        if index is None:
+            return None
+        int_values.append(index)
     value = bytes(int_values)
     return bytes_to_q(value)
diff --git a/src/electionguard/words.py b/src/electionguard/words.py
index 59d05cc..8dd1531 100644
--- a/src/electionguard/words.py
+++ b/src/electionguard/words.py
@@ -1,27 +1,29 @@
+from typing import Optional
+
 MIN_INDEX = 0
 MAX_INDEX = 4095
 
 
-def get_word(index: int) -> str:
+def get_word(index: int) -> Optional[str]:
     """
     Get word (4096 options) based on 16 bit index for use with trackers.
-    :param index: index of word
-    :return: word
+    :param index: index of word between 0 and 4095
+    :return: word or None if index outside bounds
     """
     if index < MIN_INDEX:
-        return words[MIN_INDEX]
+        return None
     if index > MAX_INDEX:
-        return words[MAX_INDEX]
+        return None
     return words[index]
 
 
-def get_index_from_word(word: str) -> int:
+def get_index_from_word(word: str) -> Optional[int]:
     """
     Get the index of a word (4096 options) based on 16 bit index for use with trackers.
     :param word: word
-    :return: index of word
+    :return: index of word or None if not found
     """
-    return words.index(word)
+    return words.index(word) if word in words else None
 
 
 words = [


Test patch:
diff --git a/tests/test_words.py b/tests/test_words.py
index ed18d08..c4ad51c 100644
--- a/tests/test_words.py
+++ b/tests/test_words.py
@@ -30,8 +30,20 @@ class TestWord(TestCase):
         INDEX_BELOW_MIN = -1
         INDEX_ABOVE_MAX = 4096
 
-        word_min = get_word(INDEX_BELOW_MIN)
-        word_max = get_word(INDEX_ABOVE_MAX)
+        # Act
+        word_past_min = get_word(INDEX_BELOW_MIN)
+        word_past_max = get_word(INDEX_ABOVE_MAX)
 
-        self.assertEqual(word_min, "aardvark")
-        self.assertEqual(word_max, "prospect")
+        # Assert
+        self.assertIsNone(word_past_min)
+        self.assertIsNone(word_past_max)
+
+    def test_get_index_of_word_not_in_list(self):
+        # Arrange
+        FAILING_WORD = "thiswordshouldfail"
+
+        # Act
+        failed_index = get_index_from_word(FAILING_WORD)
+
+        # Assert
+        self.assertIsNone(failed_index)