Repository: jatkinson1000/archeryutils (language: python)

Issue:
Tests needed for classifications
Tests for handicaps have been added, but are yet to be written for classifications.
Complete lack of testing
At present there is a complete lack of testing.


Discussion / hints:

Fix patch:
diff --git a/archeryutils/classifications/classifications.py b/archeryutils/classifications/classifications.py
index e33ef5e..42a234b 100644
--- a/archeryutils/classifications/classifications.py
+++ b/archeryutils/classifications/classifications.py
@@ -1,42 +1,134 @@
-import numpy as np
+"""
+Code for calculating Archery GB classifications.
+
+Extended Summary
+----------------
+Code to add functionality to the basic handicap equations code
+in handicap_equations.py including inverse function and display.
+
+Routine Listings
+----------------
+read_ages_json
+read_bowstyles_json
+read_genders_json
+read_classes_json
+get_groupname
+_make_AGB_outdoor_classification_dict
+_make_AGB_indoor_classification_dict
+_make_AGB_field_classification_dict
+calculate_AGB_outdoor_classification
+AGB_outdoor_classification_scores
+calculate_AGB_indoor_classification
+AGB_indoor_classification_scores
+calculate_AGB_field_classification
+AGB_field_classification_scores
+
+"""
 import json
 from pathlib import Path
+import numpy as np
 
-from archeryutils import rounds, load_rounds
+from archeryutils import load_rounds
 from archeryutils.handicaps import handicap_equations as hc_eq
 
 
 def read_ages_json(age_file=Path(__file__).parent / "AGB_ages.json"):
-    # Read in age group info as list of dicts
-    with open(age_file) as json_file:
+    """
+    Read AGB age categories in from neighbouring json file to list of dicts.
+
+    Parameters
+    ----------
+    age_file : Path
+        path to json file
+
+    Returns
+    -------
+    ages : list of dict
+        AGB age category data from file
+
+    References
+    ----------
+    Archery GB Rules of Shooting
+    """
+    with open(age_file, encoding="utf-8") as json_file:
         ages = json.load(json_file)
     return ages
 
 
 def read_bowstyles_json(bowstyles_file=Path(__file__).parent / "AGB_bowstyles.json"):
-    # Read in bowstyleclass info as list of dicts
-    with open(bowstyles_file) as json_file:
+    """
+    Read AGB  bowstyles in from neighbouring json file to list of dicts.
+
+    Parameters
+    ----------
+    bowstyles_file : Path
+        path to json file
+
+    Returns
+    -------
+    bowstyles : list of dict
+        AGB bowstyle category data from file
+
+    References
+    ----------
+    Archery GB Rules of Shooting
+    """
+    with open(bowstyles_file, encoding="utf-8") as json_file:
         bowstyles = json.load(json_file)
     return bowstyles
 
 
 def read_genders_json(genders_file=Path(__file__).parent / "AGB_genders.json"):
+    """
+    Read AGB genders in from neighbouring json file to list of dicts.
+
+    Parameters
+    ----------
+    genders_file : Path
+        path to json file
+
+    Returns
+    -------
+    genders : list of dict
+        AGB gender data from file
+
+    References
+    ----------
+    Archery GB Rules of Shooting
+    """
     # Read in gender info as list
-    with open(genders_file) as json_file:
+    with open(genders_file, encoding="utf-8") as json_file:
         genders = json.load(json_file)["genders"]
     return genders
 
 
 def read_classes_json(classes_file=Path(__file__).parent / "AGB_classes.json"):
+    """
+    Read AGB classes in from neighbouring json file to list of dicts.
+
+    Parameters
+    ----------
+    classes_file : Path
+        path to json file
+
+    Returns
+    -------
+    classes : list of dict
+        AGB classes data from file
+
+    References
+    ----------
+    Archery GB Rules of Shooting
+    """
     # Read in classification names as dict
-    with open(classes_file) as json_file:
+    with open(classes_file, encoding="utf-8") as json_file:
         classes = json.load(json_file)
     return classes
 
 
 def get_groupname(bowstyle, gender, age_group):
     """
-    Subroutine to generate a single string id for a particular category
+    Generate a single string id for a particular category.
 
     Parameters
     ----------
@@ -51,11 +143,7 @@ def get_groupname(bowstyle, gender, age_group):
     -------
     groupname : str
         single, lower case str id for this category
-
-    References
-    ----------
     """
-
     groupname = (
         f"{age_group.lower().replace(' ', '')}_"
         f"{gender.lower()}_"
@@ -67,13 +155,15 @@ def get_groupname(bowstyle, gender, age_group):
 
 def _make_AGB_outdoor_classification_dict():
     """
-    Subroutine to generate a dictionary of dictionaries providing handicaps for
-    each classification band and a list prestige rounds for each category from
-    data files.
-    Appropriate for 2023 ArcheryGB age groups and classifications
+    Generate AGB outdoor classification data.
+
+    Generate a dictionary of dictionaries providing handicaps for each
+    classification band and a list prestige rounds for each category from data files.
+    Appropriate for 2023 ArcheryGB age groups and classifications.
 
     Parameters
     ----------
+    None
 
     Returns
     -------
@@ -88,7 +178,6 @@ def _make_AGB_outdoor_classification_dict():
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     # Lists of prestige rounds defined by 'codename' of 'Round' class
     # TODO: convert this to json?
     prestige_imperial = [
@@ -188,7 +277,7 @@ def _make_AGB_outdoor_classification_dict():
 
                 class_HC = np.empty(len(AGB_classes))
                 min_dists = np.empty((len(AGB_classes), 3))
-                for i, classification in enumerate(AGB_classes):
+                for i in range(len(AGB_classes)):
                     # Assign handicap for this classification
                     class_HC[i] = (
                         bowstyle["datum"]
@@ -207,15 +296,15 @@ def _make_AGB_outdoor_classification_dict():
                         try:
                             # Age group trickery:
                             # U16 males and above step down for B2 and beyond
-                            if gender.lower() in ["male"] and age[
+                            if gender.lower() in ("male") and age[
                                 "age_group"
-                            ].lower().replace(" ", "") in [
+                            ].lower().replace(" ", "") in (
                                 "adult",
                                 "50+",
                                 "under21",
                                 "under18",
                                 "under16",
-                            ]:
+                            ):
                                 min_dists[i, :] = padded_dists[
                                     max_dist_index + i - 3 : max_dist_index + i
                                 ]
@@ -231,7 +320,8 @@ def _make_AGB_outdoor_classification_dict():
                         except IndexError as e:
                             # Shouldn't really get here...
                             print(
-                                f"{e} cannot select minimum distances for {gender} and {age['age_group']}"
+                                f"{e} cannot select minimum distances for "
+                                f"{gender} and {age['age_group']}"
                             )
                             min_dists[i, :] = dists[-3:]
 
@@ -269,10 +359,7 @@ def _make_AGB_outdoor_classification_dict():
                             prestige_rounds.append(roundname)
                     # Additional fix for Male 50+, U18, and U16
                     if gender.lower() == "male":
-                        if (
-                            age["age_group"].lower() == "50+"
-                            or age["age_group"].lower() == "under 18"
-                        ):
+                        if age["age_group"].lower() in ("50+", "under 18"):
                             prestige_rounds.append(prestige_720[1])
                         elif age["age_group"].lower() == "under 16":
                             prestige_rounds.append(prestige_720[2])
@@ -299,11 +386,14 @@ def _make_AGB_outdoor_classification_dict():
 
 def _make_AGB_indoor_classification_dict():
     """
-    Subroutine to generate a dictionary of dictionaries providing handicaps for
-    each classification band
+    Generate AGB outdoor classification data.
+
+    Generate a dictionary of dictionaries providing handicaps for
+    each classification band.
 
     Parameters
     ----------
+    None
 
     Returns
     -------
@@ -316,7 +406,6 @@ def _make_AGB_indoor_classification_dict():
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     AGB_indoor_classes = ["A", "B", "C", "D", "E", "F", "G", "H"]
 
     # Generate dict of classifications
@@ -344,11 +433,14 @@ def _make_AGB_indoor_classification_dict():
 
 def _make_AGB_field_classification_dict():
     """
-    Subroutine to generate a dictionary of dictionaries providing handicaps for
-    each classification band
+    Generate AGB outdoor classification data.
+
+    Generate a dictionary of dictionaries providing handicaps for
+    each classification band.
 
     Parameters
     ----------
+    None
 
     Returns
     -------
@@ -361,7 +453,6 @@ def _make_AGB_field_classification_dict():
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     AGB_field_classes = [
         "Grand Master Bowman",
         "Master Bowman",
@@ -498,8 +589,10 @@ def _make_AGB_field_classification_dict():
 
 def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age_group):
     """
-    Subroutine to calculate a classification from a score given suitable inputs
-    Appropriate for 2023 ArcheryGB age groups and classifications
+    Calculate AGB outdoor classification from score.
+
+    Calculate a classification from a score given suitable inputs.
+    Appropriate for 2023 ArcheryGB age groups and classifications.
 
     Parameters
     ----------
@@ -524,7 +617,6 @@ def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     # TODO: Need routines to sanitise/deal with variety of user inputs
 
     # TODO: Should this be defined outside the function to reduce I/O or does
@@ -537,7 +629,7 @@ def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age
         ]
     )
 
-    if bowstyle.lower() in ["traditional", "flatbow"]:
+    if bowstyle.lower() in ("traditional", "flatbow"):
         bowstyle = "Barebow"
 
     groupname = get_groupname(bowstyle, gender, age_group)
@@ -546,17 +638,16 @@ def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age
     hc_params = hc_eq.HcParams()
 
     # Get scores required on this round for each classification
-    class_scores = []
-    for i, class_i in enumerate(group_data["classes"]):
-        class_scores.append(
-            hc_eq.score_for_round(
-                all_outdoor_rounds[roundname],
-                group_data["class_HC"][i],
-                "AGB",
-                hc_params,
-                round_score_up=True,
-            )[0]
-        )
+    class_scores = [
+        hc_eq.score_for_round(
+            all_outdoor_rounds[roundname],
+            group_data["class_HC"][i],
+            "AGB",
+            hc_params,
+            round_score_up=True,
+        )[0]
+        for i, class_i in enumerate(group_data["classes"])
+    ]
     # class_data = dict(
     #    zip(group_data["classes"], zip(group_data["min_dists"], class_scores))
     # )
@@ -571,25 +662,25 @@ def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age
     if roundname not in AGB_outdoor_classifications[groupname]["prestige_rounds"]:
         # TODO: a list of dictionary keys is super dodgy python...
         #   can this be improved?
-        for item in list(class_data.keys())[0:3]:
-            del class_data[item]
+        for MB_class in list(class_data.keys())[0:3]:
+            del class_data[MB_class]
 
         # If not prestige, what classes are eligible based on category and distance
         to_del = []
         round_max_dist = all_outdoor_rounds[roundname].max_distance()
-        for item in class_data:
-            if class_data[item]["min_dists"][-1] > round_max_dist:
-                to_del.append(item)
-        for item in to_del:
-            del class_data[item]
+        for class_i in class_data.items():
+            if class_i[1]["min_dists"][-1] > round_max_dist:
+                to_del.append(class_i[0])
+        for class_i in to_del:
+            del class_data[class_i]
 
     # Classification based on score - accounts for fractional HC
     # TODO Make this its own function for later use in geberating tables?
     # Of those classes remaining, what is the highest classification this score gets?
     to_del = []
-    for item in class_data:
-        if class_data[item]["score"] > score:
-            to_del.append(item)
+    for item in class_data.items():
+        if item[1]["score"] > score:
+            to_del.append(item[0])
     for item in to_del:
         del class_data[item]
 
@@ -602,8 +693,10 @@ def calculate_AGB_outdoor_classification(roundname, score, bowstyle, gender, age
 
 def AGB_outdoor_classification_scores(roundname, bowstyle, gender, age_group):
     """
-    Subroutine to calculate classification scores for a specific category and round
-    Appropriate for 2023 ArcheryGB age groups and classifications
+    Calculate AGB outdoor classification scores for category.
+
+    Subroutine to calculate classification scores for a specific category and round.
+    Appropriate for 2023 ArcheryGB age groups and classifications.
 
     Parameters
     ----------
@@ -626,7 +719,6 @@ def AGB_outdoor_classification_scores(roundname, bowstyle, gender, age_group):
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     # TODO: Should this be defined outside the function to reduce I/O or does
     #   it have no effect?
     all_outdoor_rounds = load_rounds.read_json_to_round_dict(
@@ -644,17 +736,16 @@ def AGB_outdoor_classification_scores(roundname, bowstyle, gender, age_group):
     hc_params = hc_eq.HcParams()
 
     # Get scores required on this round for each classification
-    class_scores = []
-    for i, class_i in enumerate(group_data["classes"]):
-        class_scores.append(
-            hc_eq.score_for_round(
-                all_outdoor_rounds[roundname],
-                group_data["class_HC"][i],
-                "AGB",
-                hc_params,
-                round_score_up=True,
-            )[0]
-        )
+    class_scores = [
+        hc_eq.score_for_round(
+            all_outdoor_rounds[roundname],
+            group_data["class_HC"][i],
+            "AGB",
+            hc_params,
+            round_score_up=True,
+        )[0]
+        for i in range(len(group_data["classes"]))
+    ]
 
     # Reduce list based on other criteria besides handicap
     # is it a prestige round? If not remove MB scores
@@ -674,8 +765,10 @@ def calculate_AGB_indoor_classification(
     roundname, score, bowstyle, gender, age_group, hc_scheme="AGBold"
 ):
     """
-    Subroutine to calculate a classification from a score given suitable inputs
-    Appropriate for 2023 ArcheryGB age groups and classifications
+    Calculate AGB indoor classification from score.
+
+    Subroutine to calculate a classification from a score given suitable inputs.
+    Appropriate for 2023 ArcheryGB age groups and classifications.
 
     Parameters
     ----------
@@ -702,7 +795,6 @@ def calculate_AGB_indoor_classification(
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     # TODO: Need routines to sanitise/deal with variety of user inputs
 
     # TODO: Should this be defined outside the function to reduce I/O or does
@@ -716,7 +808,7 @@ def calculate_AGB_indoor_classification(
 
     # deal with reduced categories:
     age_group = "Adult"
-    if bowstyle.lower() not in ["compound"]:
+    if bowstyle.lower() not in ("compound"):
         bowstyle = "Recurve"
 
     groupname = get_groupname(bowstyle, gender, age_group)
@@ -725,27 +817,26 @@ def calculate_AGB_indoor_classification(
     hc_params = hc_eq.HcParams()
 
     # Get scores required on this round for each classification
-    class_scores = []
-    for i, class_i in enumerate(group_data["classes"]):
-        class_scores.append(
-            hc_eq.score_for_round(
-                all_indoor_rounds[roundname],
-                group_data["class_HC"][i],
-                hc_scheme,
-                hc_params,
-                round_score_up=True,
-            )[0]
-        )
+    class_scores = [
+        hc_eq.score_for_round(
+            all_indoor_rounds[roundname],
+            group_data["class_HC"][i],
+            hc_scheme,
+            hc_params,
+            round_score_up=True,
+        )[0]
+        for i, class_i in enumerate(group_data["classes"])
+    ]
 
     class_data = dict(zip(group_data["classes"], class_scores))
 
     # What is the highest classification this score gets?
     to_del = []
-    for item in class_data:
-        if class_data[item] > score:
-            to_del.append(item)
-    for item in to_del:
-        del class_data[item]
+    for score_bound in class_data:
+        if class_data[score_bound] > score:
+            to_del.append(score_bound)
+    for del_class in to_del:
+        del class_data[del_class]
 
     # NB No fiddle for Worcester required with this logic...
     # Beware of this later on, however, if we wish to rectify the 'anomaly'
@@ -762,8 +853,10 @@ def AGB_indoor_classification_scores(
     roundname, bowstyle, gender, age_group, hc_scheme="AGBold"
 ):
     """
-    Subroutine to calculate classification scores for a specific category and round
-    Appropriate ArcheryGB age groups and classifications
+    Calculate AGB indoor classification scores for category.
+
+    Subroutine to calculate classification scores for a specific category and round.
+    Appropriate ArcheryGB age groups and classifications.
 
     Parameters
     ----------
@@ -788,7 +881,6 @@ def AGB_indoor_classification_scores(
     ArcheryGB Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7
     """
-
     # TODO: Should this be defined outside the function to reduce I/O or does
     #   it have no effect?
     all_indoor_rounds = load_rounds.read_json_to_round_dict(
@@ -800,7 +892,7 @@ def AGB_indoor_classification_scores(
 
     # deal with reduced categories:
     age_group = "Adult"
-    if bowstyle.lower() not in ["compound"]:
+    if bowstyle.lower() not in ("compound"):
         bowstyle = "Recurve"
 
     groupname = get_groupname(bowstyle, gender, age_group)
@@ -809,24 +901,25 @@ def AGB_indoor_classification_scores(
     hc_params = hc_eq.HcParams()
 
     # Get scores required on this round for each classification
-    class_scores = []
-    for i, class_i in enumerate(group_data["classes"]):
-        class_scores.append(
-            hc_eq.score_for_round(
-                all_indoor_rounds[roundname],
-                group_data["class_HC"][i],
-                hc_scheme,
-                hc_params,
-                round_score_up=True,
-            )[0]
-        )
+    class_scores = [
+        hc_eq.score_for_round(
+            all_indoor_rounds[roundname],
+            group_data["class_HC"][i],
+            hc_scheme,
+            hc_params,
+            round_score_up=True,
+        )[0]
+        for i, class_i in enumerate(group_data["classes"])
+    ]
 
     return class_scores
 
 
 def calculate_AGB_field_classification(roundname, score, bowstyle, gender, age_group):
     """
-    Subroutine to calculate a classification from a score given suitable inputs
+    Calculate AGB field classification from score.
+
+    Subroutine to calculate a classification from a score given suitable inputs.
 
     Parameters
     ----------
@@ -851,7 +944,6 @@ def calculate_AGB_field_classification(roundname, score, bowstyle, gender, age_g
     ArcheryGB 2023 Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7 (2023)
     """
-
     # TODO: Need routines to sanitise/deal with variety of user inputs
 
     # TODO: Should this be defined outside the function to reduce I/O or does
@@ -863,7 +955,7 @@ def calculate_AGB_field_classification(roundname, score, bowstyle, gender, age_g
     )
 
     # deal with reduced categories:
-    if age_group.lower().replace(" ", "") in ["adult", "50+", "under21"]:
+    if age_group.lower().replace(" ", "") in ("adult", "50+", "under21"):
         age_group = "Adult"
     else:
         age_group = "Under 18"
@@ -875,29 +967,35 @@ def calculate_AGB_field_classification(roundname, score, bowstyle, gender, age_g
 
     # Check Round is appropriate:
     # Sighted can have any Red 24, unsightes can have any blue 24
-    if bowstyle.lower() in ["compound", "recurve"] and "wa_field_24_red_" not in roundname:
-        return "unclassified"
     if (
-        bowstyle.lower() in ["barebow", "longbow", "traditional", "flatbow"]
-        and "wa_field_24_blue_" not in roundname):
+        bowstyle.lower() in ("compound", "recurve")
+        and "wa_field_24_red_" not in roundname
+    ):
         return "unclassified"
-    else:
-        # What is the highest classification this score gets?
-        class_scores = dict(zip(group_data["classes"], group_data["class_scores"]))
-        for item in class_scores:
-            if class_scores[item] > score:
-                pass
-            else:
-                return item
-
-        # if lower than 3rd class score return "UC"
+    if (
+        bowstyle.lower() in ("barebow", "longbow", "traditional", "flatbow")
+        and "wa_field_24_blue_" not in roundname
+    ):
         return "unclassified"
 
+    # What is the highest classification this score gets?
+    class_scores = dict(zip(group_data["classes"], group_data["class_scores"]))
+    for item in class_scores:
+        if class_scores[item] > score:
+            pass
+        else:
+            return item
+
+    # if lower than 3rd class score return "UC"
+    return "unclassified"
+
 
 def AGB_field_classification_scores(roundname, bowstyle, gender, age_group):
     """
-    Subroutine to calculate classification scores for a specific category and round
-    Appropriate ArcheryGB age groups and classifications
+    Calculate AGB field classification scores for category.
+
+    Subroutine to calculate classification scores for a specific category and round.
+    Appropriate ArcheryGB age groups and classifications.
 
     Parameters
     ----------
@@ -920,7 +1018,6 @@ def AGB_field_classification_scores(roundname, bowstyle, gender, age_group):
     ArcheryGB Rules of Shooting
     ArcheryGB Shooting Administrative Procedures - SAP7
     """
-
     # TODO: Should this be defined outside the function to reduce I/O or does
     #   it have no effect?
     all_field_rounds = load_rounds.read_json_to_round_dict(
@@ -930,7 +1027,7 @@ def AGB_field_classification_scores(roundname, bowstyle, gender, age_group):
     )
 
     # deal with reduced categories:
-    if age_group.lower().replace(" ", "") in ["adult", "50+", "under21"]:
+    if age_group.lower().replace(" ", "") in ("adult", "50+", "under21"):
         age_group = "Adult"
     else:
         age_group = "Under 18"
@@ -943,9 +1040,11 @@ def AGB_field_classification_scores(roundname, bowstyle, gender, age_group):
 
 
 if __name__ == "__main__":
-
-    for item in AGB_outdoor_classifications:
-        print(item, AGB_outdoor_classifications[item]["prestige_rounds"])
+    for classification in AGB_outdoor_classifications.items():
+        print(
+            classification[0],
+            classification[1]["prestige_rounds"],
+        )
 
     print(
         calculate_AGB_outdoor_classification(
diff --git a/archeryutils/constants.py b/archeryutils/constants.py
index 332afbd..bb3ef30 100644
--- a/archeryutils/constants.py
+++ b/archeryutils/constants.py
@@ -1,11 +1,2 @@
-# Author        : Jack Atkinson
-#
-# Contributors  : Jack Atkinson
-#
-# Date Created  : 2022-08-18
-# Last Modified : 2022-08-18 by Jack Atkinson
-#
-# Summary       : Constants for use in this package
-#
-
+"""Constants used in the archeryutils package."""
 YARD_TO_METRE = 0.9144
diff --git a/archeryutils/handicaps/handicap_equations.py b/archeryutils/handicaps/handicap_equations.py
index f494152..c1f0c58 100644
--- a/archeryutils/handicaps/handicap_equations.py
+++ b/archeryutils/handicaps/handicap_equations.py
@@ -1,100 +1,199 @@
-# Author        : Jack Atkinson
-#
-# Contributors  : Jack Atkinson
-#
-# Date Created  : 2022-08-15
-# Last Modified : 2022-08-22 by Jack Atkinson
-#
-# Summary       : Code for archery calculating handicaps using various systems
-#
-
-import numpy as np
+"""
+Code for archery handicap scheme calculations using various schemes.
+
+Extended Summary
+----------------
+Code to calculate information using a number of schemes:
+  - Old Archery GB (D Lane)
+  - New Archery GB (J Atkinson, 2023)
+  - Old Archery Australia (J Park)
+  - New Archery Australia (J Park)
+Calculates arrow and round scores for a variety of target faces of given
+distance and diameter:
+  - 5-zone
+  - 10-zone
+  - 10-zone 6-ring
+  - 10-zone compound
+  - 10-zone 5-ring
+  - 10-zone 5-ring compound
+  - WA_field
+  - IFAA_field
+  - Beiter-hit-miss
+  - Worcester
+  - Worcester 2-ring)
+
+Routine Listings
+----------------
+HcParams
+sigma_t
+sigma_r
+arrow_score
+score_for_round
+
+References
+----------
+Old AGB - D Lane
+New AGB - J Atkinson
+AA & AA2 - J Park
+"""
 import json
 from typing import Union, Optional, Tuple, List
 from dataclasses import dataclass
+import numpy as np
 
 from archeryutils import targets, rounds
 
 
 @dataclass
 class HcParams:
-    # KEY PARAMETERS AND CONSTANTS FOR NEW AGB HANDICAP SCHEME
-    AGB_datum = 6.0  # offset required to set handicap 0 at desired score
-    AGB_step = 3.5  # percentage change in group size for each handicap step
-    AGB_ang_0 = 5.0e-4  # baseline angle used for group size 0.5 [millirad]
-    AGB_kd = 0.00365  # distance scaling factor [1/metres]
-
-    # KEY PARAMETERS AND CONSTANTS FOR OLD AGB HANDICAP SCHEME
-    AGBo_datum = 12.9  # offset required to set handicap 0 at desired score
-    AGBo_step = 3.6  # percentage change in group size for each handicap step
-    AGBo_ang_0 = 5.0e-4  # baseline angle used for group size 0.5 [millirad]
-    AGBo_k1 = 1.429e-6  # constant used in handicap equation
-    AGBo_k2 = 1.07  # constant used in handicap equation
-    AGBo_k3 = 4.3  # constant used in handicap equation
-    AGBo_p1 = 2.0  # exponent of distance scaling
-    AGBo_arw_d = 7.14e-3  # arrow diameter used in the old AGB algorithm by D. Lane
-
-    # KEY PARAMETERS AND CONSTANTS FOR THE ARCHERY AUSTRALIA SCHEME
-    AA_k0 = 2.37  # offset required to set handicap 100 at desired score
-    AA_ks = 0.027  # change with each step of geometric progression
-    AA_kd = 0.004  # distance scaling factor [1/metres]
-
-    # KEY PARAMETERS AND CONSTANTS FOR THE UPDATED ARCHERY AUSTRALIA SCHEME
-    AA2_k0 = 2.57  # offset required to set handicap 100 at desired score
-    AA2_ks = 0.027  # change with each step of geometric progression
-    AA2_f1 = 0.815  # 'linear' scaling factor
-    AA2_f2 = 0.185  # 'quadratic' scaling factor
-    AA2_d0 = 50.0  # Normalisation distance [metres]
-
-    # DEFAULT ARROW DIAMETER
-    arw_d_in = 9.3e-3  # Diameter of an indoor arrow [metres]
-    arw_d_out = 5.5e-3  # Diameter of an outdoor arrow [metres]
+    """
+    Class to hold information for various handicap schemes.
+
+    Attributes
+    ----------
+    KEY PARAMETERS AND CONSTANTS FOR NEW AGB HANDICAP SCHEME
+    AGB_datum : float
+        offset required to set handicap 0 at desired score
+    AGB_step : float
+        percentage change in group size for each handicap step
+    AGB_ang_0 : float
+        baseline angle used for group size 0.5 [millirad]
+    AGB_kd : float
+        distance scaling factor [1/metres]
+
+    KEY PARAMETERS AND CONSTANTS FOR OLD AGB HANDICAP SCHEME
+    AGBo_datum : float
+        offset required to set handicap 0 at desired score
+    AGBo_step : float
+        percentage change in group size for each handicap step
+    AGBo_ang_0 : float
+        baseline angle used for group size 0.5 [millirad]
+    AGBo_k1 : float
+        constant used in handicap equation
+    AGBo_k2 : float
+        constant used in handicap equation
+    AGBo_k3 : float
+        constant used in handicap equation
+    AGBo_p1 : float
+        exponent of distance scaling
+    AGBo_arw_d : float
+        arrow diameter used in the old AGB algorithm by D. Lane
+
+    KEY PARAMETERS AND CONSTANTS FOR THE ARCHERY AUSTRALIA SCHEME
+    AA_k0 : float
+        offset required to set handicap 100 at desired score
+    AA_ks : float
+        change with each step of geometric progression
+    AA_kd : float
+        distance scaling factor [1/metres]
+
+    KEY PARAMETERS AND CONSTANTS FOR THE UPDATED ARCHERY AUSTRALIA SCHEME
+    AA2_k0 : float
+        offset required to set handicap 100 at desired score
+    AA2_ks : float
+        change with each step of geometric progression
+    AA2_f1 : float
+        'linear' scaling factor
+    AA2_f2 : float
+        'quadratic' scaling factor
+    AA2_d0 : float
+        Normalisation distance [metres]
+
+    DEFAULT ARROW DIAMETER
+    arw_d_in : float
+        Diameter of an indoor arrow [metres]
+    arw_d_out : float
+        Diameter of an outdoor arrow [metres]
+
+    """
+
+    AGB_datum = 6.0
+    AGB_step = 3.5
+    AGB_ang_0 = 5.0e-4
+    AGB_kd = 0.00365
+
+    AGBo_datum = 12.9
+    AGBo_step = 3.6
+    AGBo_ang_0 = 5.0e-4
+    AGBo_k1 = 1.429e-6
+    AGBo_k2 = 1.07
+    AGBo_k3 = 4.3
+    AGBo_p1 = 2.0
+    AGBo_arw_d = 7.14e-3
+
+    AA_k0 = 2.37
+    AA_ks = 0.027
+    AA_kd = 0.004
+
+    AA2_k0 = 2.57
+    AA2_ks = 0.027
+    AA2_f1 = 0.815
+    AA2_f2 = 0.185
+    AA2_d0 = 50.0
+
+    AA_arw_d_out = 5.0e-3
+
+    arw_d_in = 9.3e-3
+    arw_d_out = 5.5e-3
 
     @classmethod
     def load_json_params(cls, jsonpath):
-        json_HcParams = cls()
-        with open(jsonpath, "r") as read_file:
+        """
+        Class method to load params from a json file.
+
+        Parameters
+        ----------
+        jsonpath : str
+            path to json file with handicap parameters
+
+        Returns
+        -------
+        json_hc_params : dataclass
+            dataclass of handicap parameters read from file
+
+        """
+        json_hc_params = cls()
+        with open(jsonpath, "r", encoding="utf-8") as read_file:
             paramsdict = json.load(read_file)
-        json_HcParams.AGB_datum = paramsdict["AGB_datum"]
-        json_HcParams.AGB_step = paramsdict["AGB_step"]
-        json_HcParams.AGB_ang_0 = paramsdict["AGB_ang_0"]
-        json_HcParams.AGB_kd = paramsdict["AGB_kd"]
-        json_HcParams.AGBo_datum = paramsdict["AGBo_datum"]
-        json_HcParams.AGBo_step = paramsdict["AGBo_step"]
-        json_HcParams.AGBo_ang_0 = paramsdict["AGBo_ang_0"]
-        json_HcParams.AGBo_k1 = paramsdict["AGBo_k1"]
-        json_HcParams.AGBo_k2 = paramsdict["AGBo_k2"]
-        json_HcParams.AGBo_k3 = paramsdict["AGBo_k3"]
-        json_HcParams.AGBo_p1 = paramsdict["AGBo_p1"]
-        json_HcParams.AGBo_arw_d = paramsdict["AGBo_arw_d"]
-        json_HcParams.AA_k0 = paramsdict["AA_k0"]
-        json_HcParams.AA_ks = paramsdict["AA_ks"]
-        json_HcParams.AA_kd = paramsdict["AA_kd"]
-        json_HcParams.AA2_k0 = paramsdict["AA2_k0"]
-        json_HcParams.AA2_ks = paramsdict["AA2_ks"]
-        json_HcParams.AA2_f1 = paramsdict["AA2_f1"]
-        json_HcParams.AA2_f2 = paramsdict["AA2_f2"]
-        json_HcParams.AA2_d0 = paramsdict["AA2_d0"]
-        json_HcParams.arw_d_in = paramsdict["arrow_diameter_indoors"]
-        json_HcParams.arw_d_out = paramsdict["arrow_diameter_outdoors"]
-
-        return json_HcParams
+        json_hc_params.AGB_datum = paramsdict["AGB_datum"]
+        json_hc_params.AGB_step = paramsdict["AGB_step"]
+        json_hc_params.AGB_ang_0 = paramsdict["AGB_ang_0"]
+        json_hc_params.AGB_kd = paramsdict["AGB_kd"]
+        json_hc_params.AGBo_datum = paramsdict["AGBo_datum"]
+        json_hc_params.AGBo_step = paramsdict["AGBo_step"]
+        json_hc_params.AGBo_ang_0 = paramsdict["AGBo_ang_0"]
+        json_hc_params.AGBo_k1 = paramsdict["AGBo_k1"]
+        json_hc_params.AGBo_k2 = paramsdict["AGBo_k2"]
+        json_hc_params.AGBo_k3 = paramsdict["AGBo_k3"]
+        json_hc_params.AGBo_p1 = paramsdict["AGBo_p1"]
+        json_hc_params.AGBo_arw_d = paramsdict["AGBo_arw_d"]
+        json_hc_params.AA_k0 = paramsdict["AA_k0"]
+        json_hc_params.AA_ks = paramsdict["AA_ks"]
+        json_hc_params.AA_kd = paramsdict["AA_kd"]
+        json_hc_params.AA2_k0 = paramsdict["AA2_k0"]
+        json_hc_params.AA2_ks = paramsdict["AA2_ks"]
+        json_hc_params.AA2_f1 = paramsdict["AA2_f1"]
+        json_hc_params.AA2_f2 = paramsdict["AA2_f2"]
+        json_hc_params.AA2_d0 = paramsdict["AA2_d0"]
+        json_hc_params.AA_arw_d_out = paramsdict["AA_arw_d_out"]
+        json_hc_params.arw_d_in = paramsdict["arrow_diameter_indoors"]
+        json_hc_params.arw_d_out = paramsdict["arrow_diameter_outdoors"]
+
+        return json_hc_params
 
 
 def sigma_t(
-    h: Union[float, np.ndarray],
+    handicap: Union[float, np.ndarray],
     hc_sys: str,
     dist: Union[float, np.ndarray],
     hc_dat: HcParams,
 ) -> Union[float, np.ndarray]:
     """
-    function sigma_t
-    Calculates the angular deviation for a given handicap scheme, handicap value,
-    and distance.
+    Calculate angular deviation for given scheme, handicap, and distance.
 
     Parameters
     ----------
-    h : ndarray or float
+    handicap : ndarray or float
         handicap to calculate sigma_t at
     hc_sys : str
         identifier for handicap system
@@ -118,24 +217,23 @@ def sigma_t(
       Park, J (2014)
       https://doi.org/10.1177%2F1754337114539308
     """
-
     if hc_sys == "AGB":
         # New AGB (Archery GB) System
         # Written by Jack Atkinson
         sig_t = (
             hc_dat.AGB_ang_0
-            * ((1.0 + hc_dat.AGB_step / 100.0) ** (h + hc_dat.AGB_datum))
+            * ((1.0 + hc_dat.AGB_step / 100.0) ** (handicap + hc_dat.AGB_datum))
             * np.exp(hc_dat.AGB_kd * dist)
         )
 
     elif hc_sys == "AGBold":
         # Old AGB (Archery GB) System
         # Written by David Lane (2013)
-        K = hc_dat.AGBo_k1 * hc_dat.AGBo_k2 ** (h + hc_dat.AGBo_k3)
+        K = hc_dat.AGBo_k1 * hc_dat.AGBo_k2 ** (handicap + hc_dat.AGBo_k3)
         F = 1 + K * dist**hc_dat.AGBo_p1
         sig_t = (
             hc_dat.AGBo_ang_0
-            * ((1.0 + hc_dat.AGBo_step / 100.0) ** (h + hc_dat.AGBo_datum))
+            * ((1.0 + hc_dat.AGBo_step / 100.0) ** (handicap + hc_dat.AGBo_datum))
             * F
         )
 
@@ -149,7 +247,7 @@ def sigma_t(
         sig_t = (
             1.0e-3
             * np.sqrt(2)
-            * np.exp(hc_dat.AA_k0 - hc_dat.AA_ks * h + hc_dat.AA_kd * dist)
+            * np.exp(hc_dat.AA_k0 - hc_dat.AA_ks * handicap + hc_dat.AA_kd * dist)
         )
 
     elif hc_sys == "AA2":
@@ -162,7 +260,7 @@ def sigma_t(
         sig_t = (
             np.sqrt(2)
             * 1.0e-3
-            * np.exp(hc_dat.AA2_k0 - hc_dat.AA2_ks * h)
+            * np.exp(hc_dat.AA2_k0 - hc_dat.AA2_ks * handicap)
             * (hc_dat.AA2_f1 + hc_dat.AA2_f2 * dist / hc_dat.AA2_d0)
         )
 
@@ -182,19 +280,21 @@ def sigma_t(
 
 
 def sigma_r(
-    h: Union[float, np.ndarray],
+    handicap: Union[float, np.ndarray],
     hc_sys: str,
     dist: Union[float, np.ndarray],
     hc_dat: HcParams,
 ) -> Union[float, np.ndarray]:
     """
-    function sigma_r
-    Calculates the angular deviation for a given handicap scheme, handicap value
-    Wraps around sigma_t() and multiplies by distance
+    Calculate deviation for a given scheme and handicap value.
+
+    Standard deviation as a proxy for 'group size' based on
+    handicap parameters, scheme, and distance.
+    Wraps around sigma_t() and multiplies by distance.
 
     Parameters
     ----------
-    h : ndarray or float
+    handicap : ndarray or float
         handicap to calculate sigma_t at
     hc_sys : str
         identifier for handicap system
@@ -207,32 +307,28 @@ def sigma_r(
     -------
     sig_r : float or ndarray
         standard deviation of group size [metres]
-
-    References
-    ----------
     """
-    sig_t = sigma_t(h, hc_sys, dist, hc_dat)
+    sig_t = sigma_t(handicap, hc_sys, dist, hc_dat)
     sig_r = dist * sig_t
 
     return sig_r
 
 
-def arrow_score(
+def arrow_score(  # pylint: disable=too-many-branches
     target: targets.Target,
-    h: Union[float, np.ndarray],
+    handicap: Union[float, np.ndarray],
     hc_sys: str,
     hc_dat: HcParams,
     arw_d: Optional[float] = None,
 ) -> float:
     """
-    Subroutine to calculate the average arrow score for a given
-    target and handicap rating.
+    Calculate the average arrow score for a given target and handicap rating.
 
     Parameters
     ----------
     target : targets.Target
         A Target class specifying the target to be used
-    h : ndarray or float
+    handicap : ndarray or float
         handicap value to calculate score for
     hc_sys : string
         identifier for the handicap system
@@ -248,9 +344,11 @@ def arrow_score(
 
     References
     ----------
+    - The construction of the graduated handicap tables for target archery
+      Lane, D (2013)
     """
-    # Set arrow diameter. Use provided, if AGBold scheme set value, otherwise select
-    # default from params based on in/out
+    # Set arrow diameter. Use provided, if AGBold or AA/AA2 scheme set value,
+    # otherwise select default from params based on in-/out-doors
     if arw_d is None:
         if hc_sys == "AGBold":
             arw_rad = hc_dat.AGBo_arw_d / 2.0
@@ -258,12 +356,15 @@ def arrow_score(
             if target.indoor:
                 arw_rad = hc_dat.arw_d_in / 2.0
             else:
-                arw_rad = hc_dat.arw_d_out / 2.0
+                if hc_sys in ("AA", "AA2"):
+                    arw_rad = hc_dat.AA_arw_d_out / 2.0
+                else:
+                    arw_rad = hc_dat.arw_d_out / 2.0
     else:
         arw_rad = arw_d / 2.0
 
     tar_dia = target.diameter
-    sig_r = sigma_r(h, hc_sys, target.distance, hc_dat)
+    sig_r = sigma_r(handicap, hc_sys, target.distance, hc_dat)
 
     if target.scoring_system == "5_zone":
         s_bar = (
@@ -344,7 +445,7 @@ def arrow_score(
     elif target.scoring_system == "Beiter_hit_miss":
         s_bar = 1 - np.exp(-((((tar_dia / 2) + arw_rad) / sig_r) ** 2))
 
-    elif target.scoring_system == "Worcester":
+    elif target.scoring_system in ("Worcester", "IFAA_field_expert"):
         s_bar = 5 - sum(
             np.exp(-((((n * tar_dia / 10) + arw_rad) / sig_r) ** 2))
             for n in range(1, 6)
@@ -373,21 +474,20 @@ def arrow_score(
 
 def score_for_round(
     rnd: rounds.Round,
-    h: Union[float, np.ndarray],
+    handicap: Union[float, np.ndarray],
     hc_sys: str,
     hc_dat: HcParams,
     arw_d: Optional[float] = None,
     round_score_up: bool = True,
 ) -> Tuple[float, List[float]]:
     """
-    Subroutine to calculate the average arrow score for a given
-    target and handicap rating.
+    Calculate the average arrow score for a given target and handicap rating.
 
     Parameters
     ----------
     rnd : rounds.Round
         A Round class specifying the round being shot
-    h : ndarray or float
+    handicap : ndarray or float
         handicap value to calculate score for
     hc_sys : string
         identifier for the handicap system
@@ -406,22 +506,19 @@ def score_for_round(
     pass_score : list of float
         average score for each pass in the round
 
-    References
-    ----------
     """
-
-    pass_score = []
-    for Pass_i in rnd.passes:
-        pass_score.append(
-            Pass_i.n_arrows * arrow_score(Pass_i.target, h, hc_sys, hc_dat, arw_d=arw_d)
-        )
+    pass_score = [
+        pass_i.n_arrows
+        * arrow_score(pass_i.target, handicap, hc_sys, hc_dat, arw_d=arw_d)
+        for pass_i in rnd.passes
+    ]
 
     round_score = np.sum(pass_score, axis=0)
 
     if round_score_up:
         # Old AGB system uses plain rounding rather than ceil of other schemes
-        if hc_sys == "AGBold":
-            round_score = np.round(round_score)
+        if hc_sys in ("AGBold", "AA", "AA2"):
+            round_score = np.around(round_score)
         else:
             round_score = np.ceil(round_score)
 
diff --git a/archeryutils/handicaps/handicap_functions.py b/archeryutils/handicaps/handicap_functions.py
index e92d6ae..b8810b0 100644
--- a/archeryutils/handicaps/handicap_functions.py
+++ b/archeryutils/handicaps/handicap_functions.py
@@ -1,17 +1,21 @@
-# Author        : Jack Atkinson
-#
-# Contributors  : Jack Atkinson
-#
-# Date Created  : 2022-08-16
-# Last Modified : 2022-08-22 by Jack Atkinson
-#
-# Summary       : Code for doing things with archery handicap code
-#
+"""
+Code for doing things with archery handicap equations.
 
-import numpy as np
+Extended Summary
+----------------
+Code to add functionality to the basic handicap equations code
+in handicap_equations.py including inverse function and display.
+
+Routine Listings
+----------------
+print_handicap_table
+handicap_from_score
+
+"""
+from typing import Union, Optional, List
 import warnings
 from itertools import chain
-from typing import Union, Optional, List
+import numpy as np
 
 import archeryutils.handicaps.handicap_equations as hc_eq
 from archeryutils import rounds
@@ -33,7 +37,7 @@ def print_handicap_table(
     int_prec: bool = False,
 ) -> None:
     """
-    Subroutine to generate a handicap table
+    Generate a handicap table to screen and/or file.
 
     Parameters
     ----------
@@ -63,11 +67,7 @@ def print_handicap_table(
     Returns
     -------
     None
-
-    References
-    ----------
     """
-
     # Abbreviations to replace headings with in Handicap Tables to keep concise
     abbreviations = {
         "Compound": "C",
@@ -83,8 +83,16 @@ def print_handicap_table(
         "Ladies": "L",
     }
 
+    if not isinstance(hcs, np.ndarray):
+        if isinstance(hcs, list):
+            hcs = np.array(hcs)
+        elif isinstance(hcs, (float, int)):
+            hcs = np.array([hcs])
+        else:
+            raise TypeError("Expected float or ndarray for hcs.")
+
     table = np.empty([len(hcs), len(round_list) + 1])
-    table[:, 0] = hcs[:]
+    table[:, 0] = hcs.copy()
     for i, round_i in enumerate(round_list):
         table[:, i + 1], _ = hc_eq.score_for_round(
             round_i, hcs, hc_sys, hc_dat, arrow_d, round_score_up=round_scores_up
@@ -97,7 +105,7 @@ def print_handicap_table(
         # TODO: This assumes scores are running highest to lowest.
         #  AA and AA2 will only work if hcs passed in reverse order (large to small)
         for irow, row in enumerate(table[:-1, :]):
-            for jscore, score in enumerate(row):
+            for jscore in range(len(row)):
                 if table[irow, jscore] == table[irow + 1, jscore]:
                     if int_prec:
                         table[irow, jscore] = FILL
@@ -133,8 +141,7 @@ def abbreviate(name):
     def format_row(row):
         if int_prec:
             return "".join("".rjust(14) if x == FILL else f"{x:14d}" for x in row)
-        else:
-            return "".join("".rjust(14) if np.isnan(x) else f"{x:14.8f}" for x in row)
+        return "".join("".rjust(14) if np.isnan(x) else f"{x:14.8f}" for x in row)
 
     output_rows = [format_row(row) for row in table]
     output_str = "\n".join(chain([output_header], output_rows))
@@ -148,8 +155,6 @@ def format_row(row):
             f.write(output_str)
         print("Done.")
 
-    return
-
 
 def handicap_from_score(
     score: float,
@@ -160,7 +165,7 @@ def handicap_from_score(
     int_prec: bool = False,
 ) -> Union[int, float]:
     """
-    Subroutine to return the handicap of a given score on a given round
+    Calculate the handicap of a given score on a given round using root-finding.
 
     Parameters
     ----------
@@ -187,182 +192,181 @@ def handicap_from_score(
     ----------
     Brent's Method for Root Finding in Scipy
     - https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brentq.html
-    - https://github.com/scipy/scipy/blob/dde39b7cc7dc231cec6bf5d882c8a8b5f40e73ad/scipy/optimize/Zeros/brentq.c
+    - https://github.com/scipy/scipy/blob/dde39b7cc7dc231cec6bf5d882c8a8b5f40e73ad/
+      scipy/optimize/Zeros/brentq.c
     """
     max_score = rnd.max_score()
     if score > max_score:
         raise ValueError(
-            f"The score of {score} provided is greater that the maximum of {max_score} "
+            f"The score of {score} provided is greater than the maximum of {max_score} "
             f"for a {rnd.name}."
         )
-    elif score <= 0.0:
+    if score <= 0.0:
         raise ValueError(
             f"The score of {score} provided is less than or equal to zero so cannot "
             "have a handicap."
         )
 
-    elif score == max_score:
+    if score == max_score:
         # Deal with max score before root finding
-        # start high and drop down until no longer ceiling to max score
-        # (i.e. >= max_score - 1.0)
-        if hc_sys in ["AA", "AA2"]:
-            hc = 175
+        # start high and drop down until no longer rounding to max score
+        # (i.e. >= max_score - 1.0 for AGB, and >= max_score - 0.5 for AA, AA2, and AGBold)
+        if hc_sys in ("AA", "AA2"):
+            hc = 175.0
             dhc = -0.01
         else:
-            hc = -75
+            hc = -75.0
             dhc = 0.01
+
+        # Set rounding limit
+        if hc_sys in ("AA", "AA2", "AGBold"):
+            round_lim = 0.5
+        else:
+            round_lim = 1.0
+
         s_max, _ = hc_eq.score_for_round(
             rnd, hc, hc_sys, hc_dat, arw_d, round_score_up=False
         )
-        # Work down to where we would round up (ceil) to max score - ceiling approach
-        while s_max > max_score - 1.0:
-            hc += dhc
+        # Work down to where we would round or ceil to max score
+        while s_max > max_score - round_lim:
+            hc = hc + dhc
             s_max, _ = hc_eq.score_for_round(
                 rnd, hc, hc_sys, hc_dat, arw_d, round_score_up=False
             )
-        hc -= dhc  # Undo final iteration that overshoots
+        hc = hc - dhc  # Undo final iteration that overshoots
         if int_prec:
-            if hc_sys in ["AA", "AA2"]:
+            if hc_sys in ("AA", "AA2"):
                 hc = np.ceil(hc)
             else:
                 hc = np.floor(hc)
         else:
             warnings.warn(
                 "Handicap requested for maximum score without integer precision.\n"
-                "Value returned will be handicap required for score > max_score-1.\n"
+                "Value returned will be first handiucap that achieves this score.\n"
                 "This could cause issues if you are not working in integers.",
                 UserWarning,
             )
         return hc
 
+    # ROOT FINDING for general case (not max score)
+    def f_root(h, scr, rd, sys, hc_data, arw_dia):
+        val, _ = hc_eq.score_for_round(
+            rd, h, sys, hc_data, arw_dia, round_score_up=False
+        )
+        return val - scr
+
+    if hc_sys in ("AA", "AA2"):
+        x = [-250.0, 175.0]
     else:
-        # ROOT FINDING
-        def f_root(h, scr, rd, sys, hc_data, arw_dia):
-            val, _ = hc_eq.score_for_round(
-                rd, h, sys, hc_data, arw_dia, round_score_up=False
-            )
-            return val - scr
+        x = [-75.0, 300.0]
+
+    f = [
+        f_root(x[0], score, rnd, hc_sys, hc_dat, arw_d),
+        f_root(x[1], score, rnd, hc_sys, hc_dat, arw_d),
+    ]
+    xtol = 1.0e-16
+    rtol = 0.00
+    xblk = 0.0
+    fblk = 0.0
+    scur = 0.0
+    spre = 0.0
+    dpre = 0.0
+    dblk = 0.0
+    stry = 0.0
+
+    if abs(f[1]) <= f[0]:
+        xcur = x[1]
+        xpre = x[0]
+        fcur = f[1]
+        fpre = f[0]
+    else:
+        xpre = x[1]
+        xcur = x[0]
+        fpre = f[1]
+        fcur = f[0]
+
+    for _ in range(25):
+        if (fpre != 0.0) and (fcur != 0.0) and (np.sign(fpre) != np.sign(fcur)):
+            xblk = xpre
+            fblk = fpre
+            spre = xcur - xpre
+            scur = xcur - xpre
+        if abs(fblk) < abs(fcur):
+            # xpre = xcur
+            # xcur = xblk
+            # xblk = xpre
+            xpre, xcur, xblk = xcur, xblk, xcur
+
+            # fpre = fcur
+            # fcur = fblk
+            # fblk = fpre
+            fpre, fcur, fblk = fcur, fblk, fcur
+
+        delta = (xtol + rtol * abs(xcur)) / 2.0
+        sbis = (xblk - xcur) / 2.0
+
+        if (fcur == 0.0) or (abs(sbis) < delta):
+            hc = xcur
+            break
 
-        if hc_sys in ["AA", "AA2"]:
-            x = [-250, 175]
-        else:
-            x = [-75, 300]
-
-        f = [
-            f_root(x[0], score, rnd, hc_sys, hc_dat, arw_d),
-            f_root(x[1], score, rnd, hc_sys, hc_dat, arw_d),
-        ]
-        xtol = 1.0e-16
-        rtol = 0.00
-        xblk = 0.0
-        fblk = 0.0
-        scur = 0.0
-        spre = 0.0
-        dpre = 0.0
-        dblk = 0.0
-        stry = 0.0
-
-        if abs(f[1]) <= f[0]:
-            xcur = x[1]
-            xpre = x[0]
-            fcur = f[1]
-            fpre = f[0]
-        else:
-            xpre = x[1]
-            xcur = x[0]
-            fpre = f[1]
-            fcur = f[0]
-
-        for i in range(25):
-            if (fpre != 0.0) and (fcur != 0.0) and (np.sign(fpre) != np.sign(fcur)):
-                xblk = xpre
-                fblk = fpre
-                spre = xcur - xpre
-                scur = xcur - xpre
-            if abs(fblk) < abs(fcur):
-                xpre = xcur
-                xcur = xblk
-                xblk = xpre
-
-                fpre = fcur
-                fcur = fblk
-                fblk = fpre
-
-            delta = (xtol + rtol * abs(xcur)) / 2.0
-            sbis = (xblk - xcur) / 2.0
-
-            if (fcur == 0.0) or (abs(sbis) < delta):
-                hc = xcur
-                break
-
-            if (abs(spre) > delta) and (abs(fcur) < abs(fpre)):
-                if xpre == xblk:
-                    stry = -fcur * (xcur - xpre) / (fcur - xpre)
-                else:
-                    dpre = (fpre - fcur) / (xpre - xcur)
-                    dblk = (fblk - fcur) / (xblk - xcur)
-                    stry = -fcur * (fblk - fpre) / (fblk * dpre - fpre * dblk)
-
-                if 2 * abs(stry) < min(abs(spre), 3 * abs(sbis) - delta):
-                    # accept step
-                    spre = scur
-                    scur = stry
-                else:
-                    # bisect
-                    spre = sbis
-                    scur = sbis
+        if (abs(spre) > delta) and (abs(fcur) < abs(fpre)):
+            if xpre == xblk:
+                stry = -fcur * (xcur - xpre) / (fcur - xpre)
+            else:
+                dpre = (fpre - fcur) / (xpre - xcur)
+                dblk = (fblk - fcur) / (xblk - xcur)
+                stry = -fcur * (fblk - fpre) / (fblk * dpre - fpre * dblk)
+
+            if 2 * abs(stry) < min(abs(spre), 3 * abs(sbis) - delta):
+                # accept step
+                spre = scur
+                scur = stry
             else:
                 # bisect
                 spre = sbis
                 scur = sbis
-            xpre = xcur
-            fpre = fcur
-            if abs(scur) > delta:
-                xcur += scur
+        else:
+            # bisect
+            spre = sbis
+            scur = sbis
+        xpre = xcur
+        fpre = fcur
+        if abs(scur) > delta:
+            xcur += scur
+        else:
+            if sbis > 0:
+                xcur += delta
             else:
-                if sbis > 0:
-                    xcur += delta
-                else:
-                    xcur -= delta
+                xcur -= delta
 
-            fcur = f_root(xcur, score, rnd, hc_sys, hc_dat, arw_d)
-            hc = xcur
+        fcur = f_root(xcur, score, rnd, hc_sys, hc_dat, arw_d)
+        hc = xcur
 
-        # Force integer precision if required.
-        # NB: added complexity - not trivial as we require asymmetric rounding.
-        #                        hence the if <0 clause
-        if int_prec:
-            if np.sign(hc) < 0:
-                if hc_sys in ["AA", "AA2"]:
-                    hc = np.ceil(hc)
-                else:
-                    hc = np.floor(hc)
-            else:
-                if hc_sys in ["AA", "AA2"]:
-                    hc = np.floor(hc)
-                else:
-                    hc = np.ceil(hc)
+    # Force integer precision if required.
+    if int_prec:
+        if hc_sys in ("AA", "AA2"):
+            hc = np.floor(hc)
+        else:
+            hc = np.ceil(hc)
+
+        sc, _ = hc_eq.score_for_round(
+            rnd, hc, hc_sys, hc_dat, arw_d, round_score_up=True
+        )
 
+        # Check that you can't get the same score from a larger handicap when
+        # working in integers
+        min_h_flag = False
+        if hc_sys in ("AA", "AA2"):
+            hstep = -1.0
+        else:
+            hstep = 1.0
+        while not min_h_flag:
+            hc += hstep
             sc, _ = hc_eq.score_for_round(
                 rnd, hc, hc_sys, hc_dat, arw_d, round_score_up=True
             )
+            if sc < score:
+                hc -= hstep  # undo the iteration that caused the flag to raise
+                min_h_flag = True
 
-            # Check that you can't get the same score from a larger handicap when
-            # working in integers
-            min_h_flag = False
-            if hc_sys in ["AA", "AA2"]:
-                hstep = -1.0
-            else:
-                hstep = 1.0
-            while not min_h_flag:
-                hc += hstep
-                sc, _ = hc_eq.score_for_round(
-                    rnd, hc, hc_sys, hc_dat, arw_d, round_score_up=True
-                )
-                if sc < score:
-                    hc -= hstep  # undo the iteration that caused the flag to raise
-                    min_h_flag = True
-            return hc
-
-        else:
-            return hc
+    return hc
diff --git a/archeryutils/handicaps/hc_sys_params.json b/archeryutils/handicaps/hc_sys_params.json
index b71783e..52fc90d 100644
--- a/archeryutils/handicaps/hc_sys_params.json
+++ b/archeryutils/handicaps/hc_sys_params.json
@@ -1,28 +1,29 @@
 {
-  "AGB_datum" : 6.0,
-  "AGB_step" : 3.5,
-  "AGB_ang_0" : 5.0e-4,
-  "AGB_kd" : 0.00365,
+    "AGB_datum": 6.0,
+    "AGB_step": 3.5,
+    "AGB_ang_0": 5.0e-4,
+    "AGB_kd": 0.00365,
 
-  "AGBo_datum" : 12.9,
-  "AGBo_step" : 3.6,
-  "AGBo_ang_0" : 5.0e-4,
-  "AGBo_k1" : 1.429e-6,
-  "AGBo_k2" : 1.07,
-  "AGBo_k3" : 4.3,
-  "AGBo_p1" : 2.0,
-  "AGBo_arw_d" : 7.14e-3,
+    "AGBo_datum": 12.9,
+    "AGBo_step": 3.6,
+    "AGBo_ang_0": 5.0e-4,
+    "AGBo_k1": 1.429e-6,
+    "AGBo_k2": 1.07,
+    "AGBo_k3": 4.3,
+    "AGBo_p1": 2.0,
+    "AGBo_arw_d": 7.14e-3,
 
-  "AA_k0" : 2.37,
-  "AA_ks" : 0.027,
-  "AA_kd" : 0.004,
-
-  "AA2_k0" : 2.57,
-  "AA2_ks" : 0.027,
-  "AA2_f1" : 0.815,
-  "AA2_f2" : 0.185,
-  "AA2_d0" : 50.0,
-
-  "arrow_diameter_indoors" : 9.3e-3,
-  "arrow_diameter_outdoors" : 5.5e-3
+    "AA_k0": 2.37,
+    "AA_ks": 0.027,
+    "AA_kd": 0.004,
+    
+    "AA2_k0": 2.57,
+    "AA2_ks": 0.027,
+    "AA2_f1": 0.815,
+    "AA2_f2": 0.185,
+    "AA2_d0": 50.0,
+    
+    "AA_arw_d_out": 5.0e-3,
+    "arrow_diameter_indoors": 9.3e-3,
+    "arrow_diameter_outdoors": 5.5e-3
 }
diff --git a/archeryutils/load_rounds.py b/archeryutils/load_rounds.py
index 3162101..ed965c6 100644
--- a/archeryutils/load_rounds.py
+++ b/archeryutils/load_rounds.py
@@ -1,3 +1,4 @@
+"""Module to load round data from json files into DotDicts."""
 import json
 from pathlib import Path
 import warnings
@@ -7,8 +8,7 @@
 
 def read_json_to_round_dict(json_filelist):
     """
-    Subroutine to return round information read in from a json file as a dictionary of
-    rounds
+    Read round information from a json file into a dictionary of rounds.
 
     Parameters
     ----------
@@ -18,11 +18,8 @@ def read_json_to_round_dict(json_filelist):
     Returns
     -------
     round_dict : dict of str : rounds.Round
-
-    References
-    ----------
     """
-    if type(json_filelist) is not list:
+    if not isinstance(json_filelist, list):
         json_filelist = [json_filelist]
 
     round_data_files = Path(__file__).parent.joinpath("round_data_files")
@@ -31,11 +28,10 @@ def read_json_to_round_dict(json_filelist):
 
     for json_file in json_filelist:
         json_filepath = round_data_files.joinpath(json_file)
-        with open(json_filepath) as json_round_file:
+        with open(json_filepath, encoding="utf-8") as json_round_file:
             data = json.load(json_round_file)
 
         for round_i in data:
-
             # Assign location
             if "location" not in round_i:
                 warnings.warn(
@@ -44,7 +40,7 @@ def read_json_to_round_dict(json_filelist):
                 )
                 round_i["location"] = None
                 round_i["indoor"] = False
-            elif round_i["location"] in [
+            elif round_i["location"] in (
                 "i",
                 "I",
                 "indoors",
@@ -55,10 +51,10 @@ def read_json_to_round_dict(json_filelist):
                 "Indoor",
                 "In",
                 "Inside",
-            ]:
+            ):
                 round_i["indoor"] = True
                 round_i["location"] = "indoor"
-            elif round_i["location"] in [
+            elif round_i["location"] in (
                 "o",
                 "O",
                 "outdoors",
@@ -69,17 +65,17 @@ def read_json_to_round_dict(json_filelist):
                 "Outdoor",
                 "Out",
                 "Outside",
-            ]:
+            ):
                 round_i["indoor"] = False
                 round_i["location"] = "outdoor"
-            elif round_i["location"] in [
+            elif round_i["location"] in (
                 "f",
                 "F",
                 "field",
                 "Field",
                 "woods",
                 "Woods",
-            ]:
+            ):
                 round_i["indoor"] = False
                 round_i["location"] = "field"
             else:
@@ -108,18 +104,17 @@ def read_json_to_round_dict(json_filelist):
                 round_i["family"] = ""
 
             # Assign passes
-            passes = []
-            for pass_i in round_i["passes"]:
-                passes.append(
-                    Pass(
-                        pass_i["n_arrows"],
-                        pass_i["diameter"] / 100,
-                        pass_i["scoring"],
-                        pass_i["distance"],
-                        dist_unit=pass_i["dist_unit"],
-                        indoor=round_i["indoor"],
-                    )
+            passes = [
+                Pass(
+                    pass_i["n_arrows"],
+                    pass_i["diameter"] / 100,
+                    pass_i["scoring"],
+                    pass_i["distance"],
+                    dist_unit=pass_i["dist_unit"],
+                    indoor=round_i["indoor"],
                 )
+                for pass_i in round_i["passes"]
+            ]
 
             round_dict[round_i["codename"]] = Round(
                 round_i["name"],
@@ -134,29 +129,48 @@ def read_json_to_round_dict(json_filelist):
 
 class DotDict(dict):
     """
-    A subclass of dict to provide dot notation access to a dictionary
-
-    Attributes
-    ----------
-
-    Methods
-    -------
+    A subclass of dict to provide dot notation access to a dictionary.
 
     References
-    -------
+    ----------
     https://goodcode.io/articles/python-dict-object/
     """
 
     def __getattr__(self, name):
+        """
+        getter.
+
+        Parameters
+        ----------
+        name : str
+            name of attribute to look for in self
+        """
         if name in self:
             return self[name]
-        else:
-            raise AttributeError(self._attribute_err_msg(name))
+        raise AttributeError(self._attribute_err_msg(name))
 
     def __setattr__(self, name, value):
+        """
+        setter.
+
+        Parameters
+        ----------
+        name : str
+            name of attribute to look for in self
+        value : any
+            value to set for attribute matching name
+        """
         self[name] = value
 
     def __delattr__(self, name):
+        """
+        delete.
+
+        Parameters
+        ----------
+        name : str
+            name of attribute to delete from self
+        """
         if name in self:
             del self[name]
         else:
diff --git a/archeryutils/rounds.py b/archeryutils/rounds.py
index d14f11a..5ae854d 100644
--- a/archeryutils/rounds.py
+++ b/archeryutils/rounds.py
@@ -1,13 +1,4 @@
-# Author        : Jack Atkinson
-#
-# Contributors  : Jack Atkinson
-#
-# Date Created  : 2022-08-16
-# Last Modified : 2022-08-22 by Jack Atkinson
-#
-# Summary       : definition of classes to define rounds for archery applications
-#
-
+"""Classes to define a Pass and Round for archery applications."""
 import numpy as np
 
 from archeryutils.targets import Target
@@ -16,15 +7,25 @@
 
 class Pass:
     """
-    A class used to represent a Pass, a subunit of a Round
-      e.g. a single distance or half
+    A class used to represent a Pass.
+
+    This class represents a pass of arrows, i.e. a subunit of a Round.
+    e.g. a single distance or half
 
     Attributes
     ----------
-    target : Target
-        a Target class representing the target used
     n_arrows : int
-        the number of arrows shot at the target in this Pass
+        number of arrows in this pass
+    diameter : float
+        face diameter in [metres]
+    scoring_system : str
+        target face/scoring system type
+    distance : float
+        linear distance from archer to target in [metres]
+    dist_unit : str
+        The unit distance is measured in. default = 'metres'
+    indoor : bool
+        is round indoors for arrow diameter purposes? default = False
 
     Methods
     -------
@@ -41,67 +42,52 @@ def __init__(
         dist_unit="metres",
         indoor=False,
     ):
-        """
-        Parameters
-        ----------
-        n_arrows : int
-            number of arrows in this pass
-        diameter : float
-            face diameter in [metres]
-        scoring_system : str
-            target face/scoring system type
-        distance : float
-            linear distance from archer to target
-        dist_unit : str
-            The unit distance is measured in. default = 'metres'
-        indoor : bool
-            is round indoors for arrow diameter purposes? default = False
-        """
-
-        self.n_arrows = n_arrows
+        self.n_arrows = abs(n_arrows)
         self.target = Target(diameter, scoring_system, distance, dist_unit, indoor)
 
     @property
     def distance(self):
+        """Get distance."""
         return self.target.distance
 
     @property
     def native_dist_unit(self):
+        """Get native_dist_unit."""
         return self.target.native_dist_unit
 
     @property
     def diameter(self):
+        """Get diameter."""
         return self.target.diameter
 
     @property
     def scoring_system(self):
+        """Get scoring_system."""
         return self.target.scoring_system
 
     @property
     def indoor(self):
+        """Get indoor."""
         return self.target.indoor
 
     def max_score(self):
         """
-        max_score
-        returns the maximum numerical score possible on this pass (not counting x's)
-
-        Parameters
-        ----------
+        Return the maximum numerical score possible on this pass (not counting x's).
 
         Returns
-        ----------
+        -------
         max_score : float
             maximum score possible on this pass
         """
-
         return self.n_arrows * self.target.max_score()
 
 
 class Round:
     """
-    A class used to represent a Round
-    Made up of a number of Passes
+    Class representing a Round.
+
+    Describes an archer round made up of a number of Passes.
+    e.g. for different distances.
 
     Attributes
     ----------
@@ -109,6 +95,12 @@ class Round:
         Formal name of the round
     passes : list of Pass
         a list of Pass classes making up the round
+    location : str or None
+        string identifing where the round is shot
+    body : str or None
+        string identifing the governing body the round belongs to
+    family : str or None
+        string identifing the family the round belongs to (e.g. wa1440, western, etc.)
 
     Methods
     -------
@@ -119,76 +111,34 @@ class Round:
 
     """
 
-    def __init__(self, name, passes, location=None, body=None, family=None):
-        """
-        Parameters
-        ----------
-        name : str
-            Formal name of the round
-        passes : list of Pass
-            a list of Pass classes making up the round
-        location : str or None
-            string identifing where the round is shot
-        body : str or None
-            string identifing the governing body the round belongs to
-        family : str or None
-            string identifing the family the round belongs to (e.g. wa1440, western, etc.)
-
-        """
+    def __init__(
+        self,
+        name,
+        passes,
+        location=None,
+        body=None,
+        family=None,
+    ):
         self.name = name
         self.passes = passes
         self.location = location
         self.body = body
         self.family = family
 
-    def get_info(self):
-        """
-        method get_info()
-        Prints information about the round
-
-        Parameters
-        ----------
-
-        Returns
-        -------
-        """
-        print(f"A {self.name} consists of {len(self.passes)} passes:")
-        for pass_i in self.passes:
-            print(
-                "\t- {} arrows at a {} cm target at {} {}s.".format(
-                    pass_i.n_arrows,
-                    pass_i.diameter * 100.0,
-                    (
-                        pass_i.target.distance / YARD_TO_METRE
-                        if pass_i.native_dist_unit == "yard"
-                        else pass_i.distance
-                    ),
-                    pass_i.native_dist_unit,
-                )
-            )
-
-        return None
-
     def max_score(self):
         """
-        max_score
-        returns the maximum numerical score possible on this round (not counting x's)
-
-        Parameters
-        ----------
+        Return the maximum numerical score possible on this round (not counting x's).
 
         Returns
-        ----------
+        -------
         max_score : float
             maximum score possible on this round
         """
-
         return np.sum([pass_i.max_score() for pass_i in self.passes])
 
     def max_distance(self, unit=False):
         """
-        max_distance
-        returns the maximum distance shot on this round along with the unit (optional)
+        Return the maximum distance shot on this round along with the unit (optional).
 
         Parameters
         ----------
@@ -196,7 +146,7 @@ def max_distance(self, unit=False):
             Return unit as well as numerical value?
 
         Returns
-        ----------
+        -------
         max_dist : float
             maximum distance shot in this round
         (max_dist, unit) : tuple (float, str)
@@ -215,5 +165,18 @@ def max_distance(self, unit=False):
 
         if unit:
             return (max_dist, d_unit)
-        else:
-            return max_dist
+        return max_dist
+
+    def get_info(self):
+        """Print information about the Round."""
+        print(f"A {self.name} consists of {len(self.passes)} passes:")
+        for pass_i in self.passes:
+            if pass_i.native_dist_unit == "yard":
+                native_dist = pass_i.target.distance / YARD_TO_METRE
+            else:
+                native_dist = pass_i.distance
+            print(
+                f"\t- {pass_i.n_arrows} arrows "
+                f"at a {pass_i.diameter * 100.0:.1f} cm target "
+                f"at {native_dist:.1f} {pass_i.native_dist_unit}s."
+            )
diff --git a/archeryutils/targets.py b/archeryutils/targets.py
index f1e1401..3442864 100644
--- a/archeryutils/targets.py
+++ b/archeryutils/targets.py
@@ -1,32 +1,24 @@
-# Author        : Jack Atkinson
-#
-# Contributors  : Jack Atkinson
-#
-# Date Created  : 2022-08-16
-# Last Modified : 2022-08-18 by Jack Atkinson
-#
-# Summary       : definition of a target class for archery applications
-#
+"""Class to represent a Target for archery applications."""
 
 from archeryutils.constants import YARD_TO_METRE
 
 
 class Target:
     """
-    A class used to represent a target
+    Class to represent a target.
 
     Attributes
     ----------
     diameter : float
-        Target face diameter in [centimetres]
+        Target face diameter in [metres]
+    scoring_system : str
+        target face/scoring system type
     distance : float
-        Linear distance from archer to target
+        linear distance from archer to target
     native_dist_unit : str
         The native unit distance is measured in
-    scoring_system : str
-        the type of target face (scoring system) used
     indoor : bool
-        is this an indoor event (use large arrow diameter)
+        is round indoors for arrow diameter purposes? default = False
 
     Methods
     -------
@@ -38,30 +30,15 @@ def __init__(
         self,
         diameter,
         scoring_system,
-        distance=None,
-        native_dist_unit=None,
+        distance,
+        native_dist_unit="metre",
         indoor=False,
     ):
-        """
-        Parameters
-        ----------
-        diameter : float
-            face diameter in [metres]
-        scoring_system : str
-            target face/scoring system type
-        distance : float
-            linear distance from archer to target
-        native_dist_unit : str
-            The native unit distance is measured in
-        indoor : bool
-            is round indoors for arrow diameter purposes? default = False
-        """
         systems = [
             "5_zone",
             "10_zone",
             "10_zone_compound",
             "10_zone_6_ring",
-            "10_zone_6_ring_compound",
             "10_zone_5_ring",
             "10_zone_5_ring_compound",
             "WA_field",
@@ -74,11 +51,11 @@ def __init__(
 
         if scoring_system not in systems:
             raise ValueError(
-                "Invalid Target Face Type specified.\n"
-                "Please select from '{}'.".format("', '".join(systems))
+                f"""Invalid Target Face Type specified.\n"""
+                f"""Please select from '{"', '".join(systems)}'."""
             )
 
-        if native_dist_unit in [
+        if native_dist_unit in (
             "Yard",
             "yard",
             "Yards",
@@ -89,9 +66,9 @@ def __init__(
             "yd",
             "Yds",
             "yds",
-        ]:
+        ):
             native_dist_unit = "yard"
-        elif native_dist_unit in [
+        elif native_dist_unit in (
             "Metre",
             "metre",
             "Metres",
@@ -100,11 +77,11 @@ def __init__(
             "m",
             "Ms",
             "ms",
-        ]:
+        ):
             native_dist_unit = "metre"
         else:
             raise ValueError(
-                f"distance unit '{native_dist_unit}' not recognised. "
+                f"Distance unit '{native_dist_unit}' not recognised. "
                 "Select from 'yard' or 'metre'."
             )
 
@@ -118,40 +95,36 @@ def __init__(
 
     def max_score(self):
         """
-        max_score
-        returns the maximum numerical score possible on this target (i.e. not X)
-
-        Parameters
-        ----------
+        Return the maximum numerical score possible on this target (i.e. not X).
 
         Returns
-        ----------
+        -------
         max_score : float
             maximum score possible on this target face
         """
-        if self.scoring_system in ["5_zone"]:
+        if self.scoring_system in ("5_zone"):
             return 9.0
-        elif self.scoring_system in [
+        if self.scoring_system in (
             "10_zone",
             "10_zone_compound",
             "10_zone_6_ring",
             "10_zone_6_ring_compound",
             "10_zone_5_ring",
             "10_zone_5_ring_compound",
-        ]:
+        ):
             return 10.0
-        elif self.scoring_system in ["WA_field"]:
+        if self.scoring_system in ("WA_field"):
             return 6.0
-        elif self.scoring_system in [
+        if self.scoring_system in (
             "IFAA_field",
             "IFAA_field_expert",
             "Worcester",
             "Worcester_2_ring",
-        ]:
+        ):
             return 5.0
-        elif self.scoring_system in ["Beiter_hit_miss"]:
+        if self.scoring_system in ("Beiter_hit_miss"):
             return 1.0
-        else:
-            raise ValueError(
-                f"target face '{self.scoring_system}' has no specified maximum score"
-            )
+        # NB: Should be hard (but not impossible) to get here without catching earlier.
+        raise ValueError(
+            f"Target face '{self.scoring_system}' has no specified maximum score."
+        )
diff --git a/examples.py b/examples.py
index 94996c5..a933a87 100644
--- a/examples.py
+++ b/examples.py
@@ -104,6 +104,30 @@
         int_prec=True,
     )
 
+    hc_func.print_handicap_table(
+        np.arange(125.0, 9.0, -1.0),
+        "AA2",
+        [load_rounds.WA_outdoor.wa720_70, load_rounds.AGB_outdoor_metric.metric_122_50, load_rounds.AGB_outdoor_metric.metric_122_30, load_rounds.WA_outdoor.wa1440_90, load_rounds.AGB_outdoor_imperial.york],
+        hc_params,
+        printout=False,
+        round_scores_up=False,
+        clean_gaps=False,
+        filename="AA2_outdoor_metric_table.dat",
+        arrow_d=0.0,
+        int_prec=False,
+    )
+    hc_func.print_handicap_table(
+        np.arange(0.0, 151.0, 1.0),
+        "AGBold",
+        [load_rounds.WA_outdoor.wa720_70, load_rounds.AGB_outdoor_metric.metric_122_50, load_rounds.AGB_outdoor_metric.metric_122_30, load_rounds.WA_outdoor.wa1440_90],
+        hc_params,
+        printout=False,
+        round_scores_up=False,
+        clean_gaps=True,
+        filename="AGBold_outdoor_metric_table.dat",
+        int_prec=True,
+    )
+
     hc_func.print_handicap_table(
         np.arange(0.0, 151.0, 1.0),
         "AGB",
@@ -152,11 +176,6 @@
         int_prec=True,
     )
 
-
-
-
-
-
     # Print the continuous score that is comes from this handicap
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 51., "AGB", hc_params, round_score_up=False
@@ -165,7 +184,7 @@
         f"A handicap of 51. on a {load_rounds.AGB_outdoor_imperial.york.name} is a continuous score of {score_from_hc}."
     )
 
-        # Print the continuous score that is comes from this handicap
+    # Print the continuous score that is comes from this handicap
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 50.0, "AGB", hc_params, round_score_up=False
     )
@@ -173,7 +192,7 @@
         f"A handicap of 50.0 on a {load_rounds.AGB_outdoor_imperial.york.name} is a continuous score of {score_from_hc}."
     )
 
-        # Print the continuous score that is comes from this handicap
+    # Print the continuous score that is comes from this handicap
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 49., "AGB", hc_params, round_score_up=False
     )
@@ -189,7 +208,7 @@
         f"A handicap of 51 on a {load_rounds.AGB_outdoor_imperial.york.name} requires a minimum score of {score_from_hc}."
     )
 
-        # Print the minimum discrete score that is required to get this handicap
+    # Print the minimum discrete score that is required to get this handicap
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 50, "AGB", hc_params, round_score_up=True
     )
@@ -197,7 +216,7 @@
         f"A handicap of 50 on a {load_rounds.AGB_outdoor_imperial.york.name} requires a minimum score of {score_from_hc}."
     )
 
-        # Print the minimum discrete score that is required to get this handicap
+    # Print the minimum discrete score that is required to get this handicap
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 49, "AGB", hc_params, round_score_up=True
     )
@@ -245,7 +264,6 @@
         f"A score of 706 on a {load_rounds.AGB_outdoor_imperial.york.name} is a discrete handicap of {hc_from_score}."
     )
 
-
     score_from_hc = hc_eq.score_for_round(
         load_rounds.AGB_outdoor_imperial.york, 50., "AGB", hc_params, round_score_up=False
     )
@@ -265,9 +283,6 @@
         f"A handicap of 52. on a {load_rounds.AGB_outdoor_imperial.york.name} is a continuous score of {score_from_hc}."
     )
 
-
-
-
     # Print score for a certain handicap - discrete
     hc_from_score = hc_func.handicap_from_score(
         684, load_rounds.AGB_outdoor_imperial.york, "AGB", hc_params, int_prec=True
@@ -288,7 +303,6 @@
         f"A score of 682 on a {load_rounds.AGB_outdoor_imperial.york.name} is a discrete handicap of {hc_from_score}."
     )
 
-
     # Handicaps
     # Print the continuous score that is comes from this handicap
     score_from_hc = hc_eq.score_for_round(
@@ -313,29 +327,46 @@
         f"A handicap of 52 on a {load_rounds.AGB_outdoor_imperial.york.name} requires a minimum score of {score_from_hc}."
     )
 
-
     # Print the minimum discrete score that is required to get this handicap
-    class_from_score = class_func.calculate_AGB_outdoor_classification('york', 965, "recurve", "male", "50+")
+    class_from_score = class_func.calculate_AGB_outdoor_classification(
+        "york", 965, "recurve", "male", "50+"
+    )
     print(
         f"A score of 965 on a {load_rounds.AGB_outdoor_imperial.york.name} is class {class_from_score}."
     )
-    class_from_score = class_func.calculate_AGB_outdoor_classification('york', 964, "recurve", "male", "50+")
+    class_from_score = class_func.calculate_AGB_outdoor_classification(
+        "york", 964, "recurve", "male", "50+"
+    )
     print(
         f"A score of 964 on a {load_rounds.AGB_outdoor_imperial.york.name} is class {class_from_score}."
     )
-    class_from_score = class_func.calculate_AGB_outdoor_classification('western', 864, "recurve", "male", "Adult")
+    class_from_score = class_func.calculate_AGB_outdoor_classification(
+        "western", 864, "recurve", "male", "Adult"
+    )
     print(
         f"A score of 864 on a {load_rounds.AGB_outdoor_imperial.western.name} is class {class_from_score}."
     )
-    class_from_score = class_func.calculate_AGB_outdoor_classification('western_30', 864, "recurve", "male", "Adult")
+    class_from_score = class_func.calculate_AGB_outdoor_classification(
+        "western_30", 864, "recurve", "male", "Adult"
+    )
     print(
         f"A score of 864 on a {load_rounds.AGB_outdoor_imperial.western_30.name} is class {class_from_score}."
     )
 
-
     hc_from_score = hc_func.handicap_from_score(
         1295, load_rounds.AGB_outdoor_imperial.bristol_i, "AGB", hc_params, int_prec=True
     )
     print(
         f"A score of 1295 on a {load_rounds.AGB_outdoor_imperial.bristol_i.name} is a discrete handicap of {hc_from_score}."
     )
+    hc_func.print_handicap_table(
+        np.arange(125.0, 9.0, -1.0),
+        "AA2",
+        [load_rounds.WA_outdoor.wa720_70, load_rounds.AGB_outdoor_metric.metric_122_50, load_rounds.AGB_outdoor_metric.metric_122_30, load_rounds.WA_outdoor.wa1440_90, load_rounds.AGB_outdoor_imperial.york],
+        hc_params,
+        printout=False,
+        round_scores_up=False,
+        clean_gaps=False,
+        filename="AA2_outdoor_metric_table.dat",
+        int_prec=False,
+    )
diff --git a/pyproject.toml b/pyproject.toml
index 42003d6..63afce8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,8 +22,9 @@ classifiers = [
 ]
 dependencies = [
     "black>=22.12.0",
-    "flake8==6.0.0",
+    "mypy>=1.0.0",
     "numpy>=1.20.3",
+    "pytest>=7.2.0",
 ]
 
 [project.urls]
@@ -52,11 +53,5 @@ archeryutils = ["*.json", "round_data_files/*.json", "classifications/*.json"]
 #[options.extras_require]
 #tests = pytest
 
-[tool.flake8]
-# ignore = ['E231', 'E241']
-per-file-ignores = [
-    '__init__.py:F401',
-]
-max-line-length = 88
-count = true
-
+[tool.mypy]
+warn_unused_configs = true


Test patch:
diff --git a/archeryutils/handicaps/tests/test_handicaps.py b/archeryutils/handicaps/tests/test_handicaps.py
new file mode 100644
index 0000000..3efcb0c
--- /dev/null
+++ b/archeryutils/handicaps/tests/test_handicaps.py
@@ -0,0 +1,681 @@
+"""Tests for handicap equations and functions"""
+import numpy as np
+import pytest
+
+import archeryutils.handicaps.handicap_equations as hc_eq
+import archeryutils.handicaps.handicap_functions as hc_func
+from archeryutils.targets import Target
+from archeryutils.rounds import Round, Pass
+
+
+hc_params = hc_eq.HcParams()
+
+# Define rounds used in these functions
+york = Round(
+    "York",
+    [
+        Pass(72, 1.22, "5_zone", 100, "yard", False),
+        Pass(48, 1.22, "5_zone", 80, "yard", False),
+        Pass(24, 1.22, "5_zone", 60, "yard", False),
+    ],
+)
+hereford = Round(
+    "Hereford",
+    [
+        Pass(72, 1.22, "5_zone", 80, "yard", False),
+        Pass(48, 1.22, "5_zone", 60, "yard", False),
+        Pass(24, 1.22, "5_zone", 50, "yard", False),
+    ],
+)
+western = Round(
+    "Western",
+    [
+        Pass(48, 1.22, "5_zone", 60, "yard", False),
+        Pass(48, 1.22, "5_zone", 50, "yard", False),
+    ],
+)
+vegas300 = Round(
+    "Vegas 300",
+    [
+        Pass(30, 0.4, "10_zone", 20, "yard", True),
+    ],
+)
+wa1440_90 = Round(
+    "WA1440 90m",
+    [
+        Pass(36, 1.22, "10_zone", 90, "metre", False),
+        Pass(36, 1.22, "10_zone", 70, "metre", False),
+        Pass(36, 0.8, "10_zone", 50, "metre", False),
+        Pass(36, 0.8, "10_zone", 30, "metre", False),
+    ],
+)
+wa1440_70 = Round(
+    "WA1440 70m",
+    [
+        Pass(36, 1.22, "10_zone", 70, "metre", False),
+        Pass(36, 1.22, "10_zone", 60, "metre", False),
+        Pass(36, 0.8, "10_zone", 50, "metre", False),
+        Pass(36, 0.8, "10_zone", 30, "metre", False),
+    ],
+)
+wa720_70 = Round(
+    "WA 720 70m",
+    [
+        Pass(36, 1.22, "10_zone", 70, "metre", False),
+        Pass(36, 1.22, "10_zone", 70, "metre", False),
+    ],
+)
+metric122_30 = Round(
+    "Metric 122-30",
+    [
+        Pass(36, 1.22, "10_zone", 30, "metre", False),
+        Pass(36, 1.22, "10_zone", 30, "metre", False),
+    ],
+)
+
+
+class TestSigmaT:
+    """
+    Class to test the sigma_t() function of handicap_equations.
+
+    Uses output of the code when run at a particular point in time and then
+    'frozen' to make sure future developments do not introduce unexpected changes.
+    Deliberate changes to the schemes may affect these values and require changes.
+
+    Methods
+    -------
+    def test_invalid_system()
+        test if invalid handicap system raises error
+    test_float()
+        test if expected sigma_t returned from float
+    test_array()
+        test if expected sigma_t returned from array of floats
+    """
+
+    def test_invalid_system(self):
+        """
+        Check that sigma_t() returns error value for invalid system.
+        """
+        with pytest.raises(
+            ValueError,
+            match=(
+                "Invalid Handicap System specified.\n"
+                + "Please select from 'AGB', 'AGBold', 'AA', or 'AA2'."
+            ),
+        ):
+            hc_eq.sigma_t(
+                handicap=10.0,
+                hc_sys=None,
+                dist=10.0,
+                hc_dat=hc_params,
+            )
+
+    @pytest.mark.parametrize(
+        "handicap,system,distance,theta_expected",
+        # Check all systems, different distances, negative and large handicaps.
+        [
+            (25.46, "AGB", 100.0, 0.002125743670979009),
+            (25.46, "AGBold", 100.0, 0.002149455433015334),
+            (25.46, "AA", 100.0, 0.011349271879457612),
+            (25.46, "AA2", 100.0, 0.011011017526614786),
+            (-12.0, "AGB", 100.0, 0.0005859295368818659),
+            (-12.0, "AGBold", 100.0, 0.000520552194308095),
+            (-12.0, "AA", 100.0, 0.03120485183570297),
+            (-12.0, "AA2", 100.0, 0.03027482063411134),
+            (200.0, "AGB", 10.0, 0.6202029252075888),
+            (200.0, "AGBold", 10.0, 134.96059974543883),
+            (200.0, "AA", 10.0, 7.111717503148246e-05),
+            (200.0, "AA2", 10.0, 7.110517486764852e-05),
+        ],
+    )
+    def test_float(self, handicap, system, distance, theta_expected):
+        """
+        Check that sigma_t(handicap=float) returns expected value for a case.
+        """
+        theta = hc_eq.sigma_t(
+            handicap=handicap,
+            hc_sys=system,
+            dist=distance,
+            hc_dat=hc_params,
+        )
+
+        assert theta == theta_expected
+
+    def test_array(self):
+        """
+        Check that sigma_t(handicap=ndarray) returns expected value for a case.
+        """
+        handicap_array = np.array([25.46, -12.0, 200.0])
+        dist_array = np.array([100.0, 100.0, 10.0])
+        theta_expected_array = np.array(
+            [0.002125743670979009, 0.0005859295368818659, 0.6202029252075888]
+        )
+        theta_array = hc_eq.sigma_t(
+            handicap=handicap_array,
+            hc_sys="AGB",
+            dist=dist_array,
+            hc_dat=hc_params,
+        )
+
+        assert (theta_array == theta_expected_array).all()
+
+
+class TestSigmaR:
+    """
+    Class to test the sigma_r() function of handicap_equations.
+
+    Uses output of the code when run at a particular point in time and then
+    'frozen' to make sure future developments do not introduce unexpected changes.
+    Deliberate changes to the schemes may affect these values and require changes.
+
+    Methods
+    -------
+    def test_invalid_system()
+        test if invalid handicap system raises error
+    test_float()
+        test if expected sigma_r returned for from float
+    test_array()
+        test if expected sigma_r returned for from array of floats
+    """
+
+    def test_invalid_system(self):
+        """
+        Check that sigma_r() returns error value for invalid system.
+        """
+        with pytest.raises(
+            ValueError,
+            match=(
+                "Invalid Handicap System specified.\n"
+                + "Please select from 'AGB', 'AGBold', 'AA', or 'AA2'."
+            ),
+        ):
+            hc_eq.sigma_r(
+                handicap=10.0,
+                hc_sys=None,
+                dist=10.0,
+                hc_dat=hc_params,
+            )
+
+    @pytest.mark.parametrize(
+        "handicap,system,distance,sigma_r_expected",
+        # Check all systems, different distances, negative and large handicaps.
+        [
+            (25.46, "AGB", 100.0, 0.21257436709790092),
+            (25.46, "AGBold", 100.0, 0.2149455433015334),
+            (25.46, "AA", 100.0, 1.1349271879457612),
+            (25.46, "AA2", 100.0, 1.1011017526614786),
+            (-12.0, "AGB", 56.54, 0.02826893819014717),
+            (-12.0, "AGBold", 56.54, 0.029263504818409218),
+            (-12.0, "AA", 56.54, 1.482791802020098),
+            (-12.0, "AA2", 56.54, 1.4794590746458498),
+            (200.0, "AGB", 10.0, 6.202029252075888),
+            (200.0, "AGBold", 10.0, 1349.6059974543882),
+            (200.0, "AA", 10.0, 0.0007111717503148246),
+            (200.0, "AA2", 10.0, 0.0007110517486764853),
+        ],
+    )
+    def test_float(self, handicap, system, distance, sigma_r_expected):
+        """
+        Check that sigma_r(handicap=float) returns expected value for a case.
+        """
+        sigma_r = hc_eq.sigma_r(
+            handicap=handicap,
+            hc_sys=system,
+            dist=distance,
+            hc_dat=hc_params,
+        )
+
+        assert sigma_r == sigma_r_expected
+
+    def test_array(self):
+        """
+        Check that sigma_r(handicap=ndarray) returns expected value for a case.
+        """
+        handicap_array = np.array([25.46, -12.0, 200.0])
+        dist_array = np.array([100.0, 56.54, 10.0])
+        sigma_r_expected_array = np.array(
+            [0.21257436709790092, 0.02826893819014717, 6.202029252075888]
+        )
+        sigma_r_array = hc_eq.sigma_r(
+            handicap=handicap_array,
+            hc_sys="AGB",
+            dist=dist_array,
+            hc_dat=hc_params,
+        )
+
+        assert (sigma_r_array == sigma_r_expected_array).all()
+
+
+class TestArrowScore:
+    """
+    Class to test the arrow_score() function of handicap_equations.
+
+    Tests all of the different types of target faces.
+    Uses output of the code when run at a particular point in time and then
+    'frozen' to make sure future developments do not introduce unexpected changes.
+    Deliberate changes to the schemes may affect these values and require changes.
+
+
+    Methods
+    -------
+    test_invalid_scoring_system()
+        test if invalid target raises error
+    test_different_handicap_systems()
+        test if expected score returned for different systems
+    test_different_target_faces()
+        test if expected score returned for different faces
+    """
+
+    def test_invalid_scoring_system(self):
+        """
+        Check that arrow_score() returns error value for invalid system.
+        """
+        with pytest.raises(
+            ValueError,
+            match="No rule for calculating scoring for face type (.+).",
+        ):
+            target = Target(122.0, "5_zone", 100.0)
+            target.scoring_system = None
+
+            hc_eq.arrow_score(
+                target=target,
+                handicap=12.0,
+                hc_sys="AGB",
+                hc_dat=hc_params,
+                arw_d=None,
+            )
+
+    @pytest.mark.parametrize(
+        "hc_system,indoor,arrow_diameter,arrow_score_expected",
+        [
+            ("AGB", False, None, 9.134460979236048),
+            ("AGB", True, None, 9.20798112770351),
+            ("AGB", False, 7.2e-3, 9.167309048169756),
+            ("AGBold", False, None, 8.983801507994793),
+            ("AGBold", True, None, 8.983801507994793),
+            ("AGBold", False, 5.5e-3, 8.95254355127178),
+            ("AA", False, None, 1.8256148953722988),
+            ("AA", True, None, 1.9015069522219408),
+            ("AA", False, 7.2e-3, 1.8642888081318025),
+            ("AA2", False, None, 1.818143484577794),
+            ("AA2", True, None, 1.8937675318872254),
+            ("AA2", False, 7.2e-3, 1.8566804060966389),
+        ],
+    )
+    def test_different_handicap_systems(
+        self, hc_system, indoor, arrow_diameter, arrow_score_expected
+    ):
+        """
+        Check arrow scores returned for different handicap systems and arrow diameters.
+
+        """
+        arrow_score = hc_eq.arrow_score(
+            target=Target(0.40, "10_zone_5_ring_compound", 20.0, "metre", indoor),
+            handicap=20.0,
+            hc_sys=hc_system,
+            hc_dat=hc_params,
+            arw_d=arrow_diameter,
+        )
+
+        assert arrow_score == arrow_score_expected
+
+    @pytest.mark.parametrize(
+        "target_face,arrow_score_expected",
+        [
+            ("5_zone", 7.044047485373609),
+            ("10_zone", 7.5472101235522695),
+            ("10_zone_compound", 7.481017199706876),
+            ("10_zone_6_ring", 7.397557278755085),
+            ("10_zone_5_ring", 7.059965360625537),
+            ("10_zone_5_ring_compound", 6.9937724367801435),
+            ("WA_field", 4.807397627133902),
+            ("IFAA_field", 4.265744100115446),
+            ("IFAA_field_expert", 4.0219427627782665),
+            ("Beiter_hit_miss", 0.999838040182924),
+            ("Worcester", 4.0219427627782665),
+            ("Worcester_2_ring", 3.34641459746045),
+        ],
+    )
+    def test_different_target_faces(self, target_face, arrow_score_expected):
+        """
+        Check correct arrow scores returned for different target faces
+        """
+        arrow_score = hc_eq.arrow_score(
+            target=Target(0.80, target_face, 50.0, "metre", False),
+            handicap=38.0,
+            hc_sys="AGB",
+            hc_dat=hc_params,
+            arw_d=None,
+        )
+
+        assert arrow_score == arrow_score_expected
+
+
+class TestScoreForRound:
+    """
+    Class to test the score_for_round() function of handicap_equations.
+
+    Uses output of the code when run at a particular point in time and then
+    'frozen' to make sure future developments do not introduce unexpected changes.
+    Deliberate changes to the schemes may affect these values and require changes.
+
+    Methods
+    -------
+    test_float_round_score()
+        test if round_score returns expected results
+    test_rounded_round_score
+        test if round_score returns expected results when rounding
+    """
+
+    @pytest.mark.parametrize(
+        "hc_system,round_score_expected",
+        [
+            (
+                "AGB",
+                (
+                    243.38187034389472,
+                    [79.84565537600997, 76.64715034267604, 86.88906462520869],
+                ),
+            ),
+            (
+                "AGBold",
+                (
+                    242.76923846935358,
+                    [80.59511929560365, 76.16106748461186, 86.01305168913808],
+                ),
+            ),
+            (
+                "AA",
+                (
+                    36.36897790870545,
+                    [7.8139031556465, 6.287938401968809, 22.267136351090144],
+                ),
+            ),
+            (
+                "AA2",
+                (
+                    37.12605100927616,
+                    [8.266044100895407, 6.465486423674918, 22.394520484705836],
+                ),
+            ),
+        ],
+    )
+    def test_float_round_score(self, hc_system, round_score_expected):
+        """
+        Check appropriate expected round scores are returned not rounding.
+        """
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(10, 1.22, "10_zone", 100, "metre", False),
+                Pass(10, 0.80, "10_zone", 80, "metre", False),
+                Pass(10, 1.22, "5_zone", 60, "metre", False),
+            ],
+        )
+
+        assert (
+            hc_eq.score_for_round(
+                test_round, 20.0, hc_system, hc_eq.HcParams(), None, False
+            )
+            == round_score_expected
+        )
+
+    @pytest.mark.parametrize(
+        "hc_system,round_score_expected",
+        [
+            (
+                "AGB",
+                (244.0, [79.84565537600997, 76.64715034267604, 86.88906462520869]),
+            ),
+            (
+                "AGBold",
+                (243.0, [80.59511929560365, 76.16106748461186, 86.01305168913808]),
+            ),
+        ],
+    )
+    def test_rounded_round_score(self, hc_system, round_score_expected):
+        """
+        Check appropriate expected round scores are returned for rounding.
+
+        NB AGBold differs to other schemes in that it rounds rather than rounding up.
+        """
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(10, 1.22, "10_zone", 100, "metre", False),
+                Pass(10, 0.80, "10_zone", 80, "metre", False),
+                Pass(10, 1.22, "5_zone", 60, "metre", False),
+            ],
+        )
+
+        assert (
+            hc_eq.score_for_round(
+                test_round, 20.0, hc_system, hc_eq.HcParams(), None, True
+            )
+            == round_score_expected
+        )
+
+
+class TestHandicapFromScore:
+    """
+    Class to test the handicap_from_score() function of handicap_functions.
+
+    Test both float and integer values, and maximum score.
+    Where possible try and perform comparisons using values taken from literature,
+    not generated by this codebase.
+    - For Archery GB old use the published handicap tables.
+    - For Archery GB new use the published handicap tables or values from this code.
+    - For Archery Australia use Archery Scorepad- [ ] Classifications
+    - For Archery Australia 2 there are no published tables and issues exist with
+
+    Methods
+    -------
+    test_score_over_max()
+    test_score_of_zero()
+    test_score_below_zero()
+    test_maximum_score()
+    test_int_precision()
+    test_decimal()
+
+    References
+    ----------
+    Archery GB Old
+        Published handicap tables by David Lane
+        http://www.oldbasingarchers.co.uk/wp-content/uploads/2019/01/2011-Handicap-Booklet-Complete.pdf
+    Archery GB New
+        Published tables for outdoor rounds
+        https://archerygb.org/files/outdoor-handicap-tables-200123092252.pdf
+    Archery Australia
+        Archery Scorepad
+        https://www.archeryscorepad.com/ratings.php
+    Archery Australia 2
+        Currently no easily available data
+    """
+
+    def test_score_over_max(self):
+        """
+        Check that handicap_from_score() returns error value for too large score.
+        """
+        with pytest.raises(
+            ValueError,
+            match=(
+                "The score of (.+) provided is greater than"
+                + " the maximum of (.+) for a (.+)."
+            ),
+        ):
+            test_round = Round(
+                "TestRound",
+                [
+                    Pass(10, 1.22, "10_zone", 50, "metre", False),
+                    Pass(10, 0.80, "10_zone", 50, "metre", False),
+                ],
+            )
+
+            hc_func.handicap_from_score(9999, test_round, "AGB", hc_params)
+
+    def test_score_of_zero(self):
+        """
+        Check that handicap_from_score() returns error for zero score.
+        """
+        with pytest.raises(
+            ValueError,
+            match=(
+                "The score of 0 provided is less than or equal to"
+                + " zero so cannot have a handicap."
+            ),
+        ):
+            test_round = Round(
+                "TestRound",
+                [
+                    Pass(10, 1.22, "10_zone", 50, "metre", False),
+                    Pass(10, 0.80, "10_zone", 50, "metre", False),
+                ],
+            )
+
+            hc_func.handicap_from_score(0, test_round, "AGB", hc_params)
+
+    def test_score_below_zero(self):
+        """
+        Check that handicap_from_score() returns error for negative score.
+        """
+        with pytest.raises(
+            ValueError,
+            match=(
+                "The score of (.+) provided is less than or equal to"
+                + " zero so cannot have a handicap."
+            ),
+        ):
+            test_round = Round(
+                "TestRound",
+                [
+                    Pass(10, 1.22, "10_zone", 50, "metre", False),
+                    Pass(10, 0.80, "10_zone", 50, "metre", False),
+                ],
+            )
+
+            hc_func.handicap_from_score(-9999, test_round, "AGB", hc_params)
+
+    @pytest.mark.parametrize(
+        "hc_system,testround,max_score,handicap_expected",
+        [
+            ("AGB", metric122_30, 720, 11),
+            ("AA", metric122_30, 720, 107),
+            # ("AA2", metric122_30, 720, 107),
+            # ------------------------------
+            ("AGB", western, 864, 9),
+            ("AGBold", western, 864, 6),
+            # ------------------------------
+            ("AGB", vegas300, 300, 3),
+            ("AA", vegas300, 300, 119),
+            # ("AA2", vegas300, 300, 119),
+        ],
+    )
+    def test_maximum_score(self, hc_system, testround, max_score, handicap_expected):
+        """
+        Check correct handicap returned for max score.
+        """
+
+        handicap = hc_func.handicap_from_score(
+            max_score,
+            testround,
+            hc_system,
+            hc_params,
+            None,
+            True,
+        )
+
+        assert handicap == handicap_expected
+
+    @pytest.mark.parametrize(
+        "hc_system,testround,testscore,handicap_expected",
+        [
+            # ------------------------------
+            # Generic scores:
+            ("AGB", wa720_70, 700, 1),
+            ("AGBold", wa720_70, 700, 1),
+            ("AA", wa720_70, 700, 119),
+            # ("AA2", wa720_70, 700, 107),
+            ("AGB", wa720_70, 500, 44),
+            ("AGBold", wa720_70, 500, 40),
+            ("AA", wa720_70, 500, 64),
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Score on the lower bound of a band:
+            ("AGB", wa720_70, 283, 63),
+            ("AGBold", wa720_70, 286, 53),
+            ("AA", wa720_70, 280, 39),
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Score on upper bound of a band:
+            ("AGB", wa720_70, 486, 46),
+            ("AGBold", wa720_70, 488, 41),
+            ("AA", wa720_70, 491, 62),
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Scores that give negative AGB handicaps:
+            ("AGB", wa720_70, 710, -5),
+            ("AGBold", wa720_70, 710, -5),
+            # ("AA", wa720_70, 491, 62)
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Scores that give 0 AA handicaps:
+            ("AA", wa720_70, 52, 0),
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Multiple Distance Round:
+            ("AGB", wa1440_90, 850, 52),
+            ("AGBold", wa1440_90, 850, 46),
+            ("AA", wa1440_90, 850, 53),
+            # ("AA2", wa1440_90, 850, 53),
+        ],
+    )
+    def test_int_precision(self, hc_system, testround, testscore, handicap_expected):
+        """
+        Check correct handicap returned for various scores.
+        """
+        handicap = hc_func.handicap_from_score(
+            testscore,
+            testround,
+            hc_system,
+            hc_params,
+            None,
+            True,
+        )
+
+        assert handicap == handicap_expected
+
+    @pytest.mark.parametrize(
+        "hc_system,testround,testscore,handicap_expected",
+        [
+            # Generic scores:
+            ("AGB", wa720_70, 500, 43.47488098014149),
+            ("AGBold", wa720_70, 500, 39.05693137292724),
+            ("AA", wa720_70, 500, 64.19799339810962),
+            # ("AA2", wa720_70, 500, 107),
+            # ------------------------------
+            # Multiple Distance Round:
+            ("AGB", wa1440_90, 850, 51.77551461040738),
+            ("AGBold", wa1440_90, 850, 45.303733163646996),
+            ("AA", wa1440_90, 850, 53.545592683112666),
+            # ("AA2", wa1440_90, 850, 53),
+        ],
+    )
+    def test_decimal(self, hc_system, testround, testscore, handicap_expected):
+        """
+        Check correct handicap returned for various scores.
+
+        Uses output of the code when run at a particular point in time then
+        'frozen' to monitor future developments introducing unexpected changes.
+        Deliberate changes to the schemes may affect these values and require changes.
+
+        """
+        handicap = hc_func.handicap_from_score(
+            testscore,
+            testround,
+            hc_system,
+            hc_params,
+            None,
+            False,
+        )
+
+        assert handicap == handicap_expected
diff --git a/archeryutils/tests/__init__.py b/archeryutils/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/archeryutils/tests/test_rounds.py b/archeryutils/tests/test_rounds.py
new file mode 100644
index 0000000..c9eac87
--- /dev/null
+++ b/archeryutils/tests/test_rounds.py
@@ -0,0 +1,169 @@
+"""Tests for Pass and Round classes"""
+import pytest
+
+from archeryutils.rounds import Pass, Round
+
+
+class TestPass:
+    """
+    Class to test the Pass class.
+
+    Methods
+    -------
+    test_default_distance_unit()
+        test behaviour of default distance unit
+    test_default_location()
+        test behaviour of default location
+    test_negative_arrows()
+        test behaviour of negative arrow number
+    test_properties()
+        test setting of Pass properties
+    test_max_score()
+        test max score functionality of Pass
+    """
+
+    def test_default_distance_unit(self):
+        """
+        Check that Pass() returns distance in metres when units not specified.
+        """
+        test_pass = Pass(36, 1.22, "5_zone", 50)
+        assert test_pass.native_dist_unit == "metre"
+
+    def test_default_location(self):
+        """
+        Check that Pass() returns indoor=False when indoor not specified.
+        """
+        test_pass = Pass(36, 1.22, "5_zone", 50, "metre")
+        assert test_pass.indoor is False
+
+    def test_negative_arrows(self):
+        """
+        Check that Pass() uses abs(narrows).
+        """
+        test_pass = Pass(-36, 1.22, "5_zone", 50, "metre")
+        assert test_pass.n_arrows == 36
+
+    def test_properties(self):
+        """
+        Check that Pass properties are set correctly
+        """
+        test_pass = Pass(36, 1.22, "5_zone", 50, "metre", False)
+        assert test_pass.distance == 50.0
+        assert test_pass.native_dist_unit == "metre"
+        assert test_pass.diameter == 1.22
+        assert test_pass.scoring_system == "5_zone"
+        assert test_pass.indoor is False
+
+    @pytest.mark.parametrize(
+        "face_type,max_score_expected",
+        [
+            ("5_zone", 900),
+            ("10_zone", 1000),
+            ("WA_field", 600),
+            ("IFAA_field", 500),
+            ("Worcester_2_ring", 500),
+            ("Beiter_hit_miss", 100),
+        ],
+    )
+    def test_max_score(self, face_type, max_score_expected):
+        """
+        Check that Pass.max_score() method is functioning correctly
+        """
+        test_pass = Pass(100, 1.22, face_type, 50, "metre", False)
+        assert test_pass.max_score() == max_score_expected
+
+
+class TestRound:
+    """
+    Class to test the Round class.
+
+    Methods
+    -------
+    def test_max_score()
+        test max score functionality of Round
+    def test_max_distance()
+        test max distance functionality of Round
+    test_max_distance_out_of_order()
+        test max distance functionality of Round with unsorted Passes
+    def test_get_info()
+        test get_info functionality of Round
+    """
+
+    def test_max_score(self):
+        """
+        Check that max score is calculated correctly for a Round
+        """
+
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(100, 1.22, "5_zone", 50, "metre", False),
+                Pass(100, 1.22, "5_zone", 40, "metre", False),
+                Pass(100, 1.22, "5_zone", 30, "metre", False),
+            ],
+        )
+        assert test_round.max_score() == 2700
+
+    @pytest.mark.parametrize(
+        "unit,get_unit,max_dist_expected",
+        [
+            ("metres", True, (100, "metre")),
+            ("yards", True, (100, "yard")),
+            ("metres", False, 100),
+            ("yards", False, 100),
+        ],
+    )
+    def test_max_distance(self, unit, get_unit, max_dist_expected):
+        """
+        Check that max distance is calculated correctly for a Round.
+
+        Returns a tuple or float depending on input argument.
+        Always returns the distance in appropriate units regardless of whether unit
+        requested or not - i.e. should not convert yards to metres.
+        """
+
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(10, 1.22, "5_zone", 100, unit, False),
+                Pass(10, 1.22, "5_zone", 80, unit, False),
+                Pass(10, 1.22, "5_zone", 60, unit, False),
+            ],
+        )
+        assert test_round.max_distance(unit=get_unit) == max_dist_expected
+
+    def test_max_distance_out_of_order(self):
+        """
+        Check max distance correct for Round where Passes not in descending dist order.
+        """
+
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(10, 1.22, "5_zone", 80, "metre", False),
+                Pass(10, 1.22, "5_zone", 100, "metre", False),
+                Pass(10, 1.22, "5_zone", 60, "metre", False),
+            ],
+        )
+        assert test_round.max_distance() == 100
+
+    def test_get_info(self, capsys):
+        """
+        Check printing info works as expected.
+        """
+        test_round = Round(
+            "MyRound",
+            [
+                Pass(10, 1.22, "5_zone", 100, "metre", False),
+                Pass(20, 1.22, "5_zone", 80, "yards", False),
+                Pass(30, 0.80, "5_zone", 60, "metre", False),
+            ],
+        )
+        test_round.get_info()
+        captured = capsys.readouterr()
+        assert captured.out == (
+            "A MyRound consists of 3 passes:\n"
+            + "\t- 10 arrows at a 122.0 cm target at 100.0 metres.\n"
+            + "\t- 20 arrows at a 122.0 cm target at 80.0 yards.\n"
+            + "\t- 30 arrows at a 80.0 cm target at 60.0 metres.\n"
+        )
diff --git a/archeryutils/tests/test_targets.py b/archeryutils/tests/test_targets.py
new file mode 100644
index 0000000..42f4444
--- /dev/null
+++ b/archeryutils/tests/test_targets.py
@@ -0,0 +1,95 @@
+"""Tests for Target class"""
+import pytest
+
+from archeryutils.targets import Target
+
+
+class TestTarget:
+    """
+    Class to test the Target class.
+
+    Methods
+    -------
+    def test_invalid_system()
+        test if invalid target face type system raises an error
+    test_invalid_distance_unit()
+        test if invalid distance unit raises an error
+    """
+
+    def test_invalid_system(self):
+        """
+        Check that Target() returns error value for invalid system.
+        """
+        with pytest.raises(
+            ValueError,
+            match="Invalid Target Face Type specified.\nPlease select from '(.+)'.",
+        ):
+            Target(1.22, "InvalidScoringSystem", 50, "m", False)
+
+    def test_invalid_distance_unit(self):
+        """
+        Check that Target() returns error value for invalid distance units.
+        """
+        with pytest.raises(
+            ValueError,
+            match="Distance unit '(.+)' not recognised. Select from 'yard' or 'metre'.",
+        ):
+            Target(1.22, "5_zone", 50, "InvalidDistanceUnit", False)
+
+    def test_default_distance_unit(self):
+        """
+        Check that Target() returns distance in metres when units not specified.
+        """
+        target = Target(1.22, "5_zone", 50)
+        assert target.native_dist_unit == "metre"
+
+    def test_default_location(self):
+        """
+        Check that Target() returns indoor=False when indoor not specified.
+        """
+        target = Target(1.22, "5_zone", 50, "m")
+        assert target.indoor is False
+
+    def test_yard_to_m_conversion(self):
+        """
+        Check that Target() returns correct distance in metres when yards provided.
+        """
+        target = Target(1.22, "5_zone", 50, "yards")
+        assert target.distance == 50.0 * 0.9144
+
+    @pytest.mark.parametrize(
+        "face_type,max_score_expected",
+        [
+            ("5_zone", 9),
+            ("10_zone", 10),
+            ("10_zone_compound", 10),
+            ("10_zone_6_ring", 10),
+            ("10_zone_5_ring", 10),
+            ("10_zone_5_ring_compound", 10),
+            ("WA_field", 6),
+            ("IFAA_field", 5),
+            ("IFAA_field_expert", 5),
+            ("Worcester", 5),
+            ("Worcester_2_ring", 5),
+            ("Beiter_hit_miss", 1),
+        ],
+    )
+    def test_max_score(self, face_type, max_score_expected):
+        """
+        Check that Target() returns correct distance in metres when yards provided.
+        """
+        target = Target(1.22, face_type, 50, "metre", False)
+        assert target.max_score() == max_score_expected
+
+    def test_max_score_invalid_face_type(self):
+        """
+        Check that Target() returns correct distance in metres when yards provided.
+        """
+        with pytest.raises(
+            ValueError,
+            match="Target face '(.+)' has no specified maximum score.",
+        ):
+            target = Target(1.22, "5_zone", 50, "metre", False)
+            # Requires manual resetting of scoring system to get this error.
+            target.scoring_system = "InvalidScoringSystem"
+            target.max_score()