diff --git a/badges/test_coverage.svg b/badges/test_coverage.svg
index 179c6a1788ecba3678a21ca99b50643f3ec81e57..6d68f472b0f880260c3f22f1fc057e768f16f432 100644
--- a/badges/test_coverage.svg
+++ b/badges/test_coverage.svg
@@ -15,7 +15,7 @@
     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
         <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
         <text x="31.5" y="14">coverage</text>
-        <text x="80" y="15" fill="#010101" fill-opacity=".3">63%</text>
-        <text x="80" y="14">63%</text>
+        <text x="80" y="15" fill="#010101" fill-opacity=".3">64%</text>
+        <text x="80" y="14">64%</text>
     </g>
 </svg>
diff --git a/binarycpython/tests/main.py b/binarycpython/tests/main.py
index e59ecd3b863d371ec163d4d230574b899fe293ce..da398439bc840f2e30c0f56d76022b2632a54545 100755
--- a/binarycpython/tests/main.py
+++ b/binarycpython/tests/main.py
@@ -43,6 +43,12 @@ from binarycpython.tests.test_dicts import (
 from binarycpython.tests.test_ensemble import (
     test_binaryc_json_serializer,
     test_handle_ensemble_string_to_json,
+    test_BinarycEncoder,
+    test_BinarycDecoder,
+    test_extract_ensemble_json_from_string,
+    test_load_ensemble,
+    test_ensemble_file_type,
+    test_open_ensemble
 )
 from binarycpython.tests.test_functions import (
     test_verbose_print,
diff --git a/binarycpython/tests/test_c_bindings.py b/binarycpython/tests/test_c_bindings.py
index 69200067075168f77e26f6e6654a9472bd4068ee..65ddd3252142ea891717704ff1ad8d416b91b247 100644
--- a/binarycpython/tests/test_c_bindings.py
+++ b/binarycpython/tests/test_c_bindings.py
@@ -19,7 +19,7 @@ from binarycpython.utils.functions import (
     Capturing,
 )
 from binarycpython.utils.ensemble import (
-    binarycDecoder,
+    BinarycDecoder,
     handle_ensemble_string_to_json,
     extract_ensemble_json_from_string,
 )
diff --git a/binarycpython/tests/test_ensemble.py b/binarycpython/tests/test_ensemble.py
index ec19b99cc70c812bfab069549f57424057d1486e..32128c72ff3fb364763c7ace470c2e3d54ee009b 100644
--- a/binarycpython/tests/test_ensemble.py
+++ b/binarycpython/tests/test_ensemble.py
@@ -3,19 +3,32 @@ Unit tests for the ensemble module
 """
 
 import os
+import sys
+import json
 import unittest
 
+import io
+from io import StringIO
+
 from binarycpython.utils.functions import (
     temp_dir,
     Capturing,
 )
 from binarycpython.utils.ensemble import (
     binaryc_json_serializer,
-    handle_ensemble_string_to_json
+    handle_ensemble_string_to_json,
+    BinarycEncoder,
+    BinarycDecoder,
+    extract_ensemble_json_from_string,
+    load_ensemble,
+    ensemble_file_type,
+    open_ensemble
+
 )
+from binarycpython.utils.grid import Population
 
 TMP_DIR = temp_dir("tests", "test_ensemble")
-
+TEST_VERBOSITY = 1
 
 class test_binaryc_json_serializer(unittest.TestCase):
     """
@@ -71,5 +84,381 @@ class test_handle_ensemble_string_to_json(unittest.TestCase):
         self.assertTrue(output_dict["ding"] == 10)
         self.assertTrue(output_dict["list_example"] == [1, 2, 3])
 
+
+class test_BinarycEncoder(unittest.TestCase):
+    """
+    Unittests for class BinarycEncoder
+    """
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        Test that the object is converted to strings
+        """
+
+        input_1 = {'a': BinarycEncoder}
+        output_1 = json.dumps(input_1, cls=BinarycEncoder)
+        self.assertTrue(isinstance(output_1, str))
+
+        dict_output_1 = json.loads(output_1)
+        self.assertTrue(isinstance(dict_output_1['a'], str))
+
+
+class test_BinarycDecoder(unittest.TestCase):
+    """
+    Unittests for class BinarycDecoder
+    """
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        Test that the object is converted to floats
+        """
+
+
+        input_1 = '{"a": "10.0"}'
+        output_1 = json.loads(input_1)
+        output_2 = json.loads(input_1, cls=BinarycDecoder)
+
+        self.assertTrue(isinstance(output_1["a"], str))
+        self.assertTrue(isinstance(output_2["a"], float))
+
+
+class test_extract_ensemble_json_from_string(unittest.TestCase):
+    """
+    Unittests for class extract_ensemble_json_from_string
+    """
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        Simple test without errors
+        """
+
+        input_1 = "ENSEMBLE_JSON {\"a\": 10}"
+        output_1 = extract_ensemble_json_from_string(input_1)
+
+        self.assertTrue(isinstance(output_1, dict))
+        self.assertEqual(output_1, {'a': 10})
+
+    def test_2(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_2(self):
+        """
+        Simple test with 2 lines
+        """
+
+        input_1 = "ENSEMBLE_JSON {\"a\": 10}\nENSEMBLE_JSON {\"b\": 20}"
+
+        capturedOutput = StringIO()          # Create StringIO object
+        sys.stdout = capturedOutput                   #  and redirect stdout.
+        _ = extract_ensemble_json_from_string(input_1)
+        sys.stdout = sys.__stdout__                   # Reset redirect.
+
+        self.assertTrue(capturedOutput.getvalue().startswith("Warning:"))
+
+    def test_3(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_3(self):
+        """
+        Simple test with empty input
+        """
+
+        input_1 = ""
+        output_1 = extract_ensemble_json_from_string(input_1)
+
+        self.assertTrue(isinstance(output_1, dict))
+        self.assertEqual(output_1, {})
+
+    def test_3(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def test_3(self):
+        """
+        Simple test with missing starting string
+        """
+
+        input_1 = " {\"a\": 10}"
+
+        #
+        capturedOutput = StringIO()          # Create StringIO object
+        sys.stdout = capturedOutput                   #  and redirect stdout.
+        _ = extract_ensemble_json_from_string(input_1)
+        sys.stdout = sys.__stdout__                   # Reset redirect.
+
+        self.assertTrue(capturedOutput.getvalue().startswith("Error:"))
+
+
+class test_load_ensemble(unittest.TestCase):
+    """
+    Unittests for class extract_ensemble_json_from_string
+    """
+
+    def __init__(self, *args, **kwargs):
+        """
+        init function
+        """
+        super(test_load_ensemble, self).__init__(*args, **kwargs)
+
+        # First
+        test_pop_1 = Population()
+        test_pop_1.set(
+            num_cores=2,
+            verbosity=TEST_VERBOSITY,
+            M_2=1,
+            orbital_period=100000,
+            ensemble=1,
+            ensemble_defer=1,
+            ensemble_filters_off=1,
+            ensemble_filter_STELLAR_TYPE_COUNTS=1,
+            ensemble_dt=1000,
+        )
+        test_pop_1.set(
+            data_dir=TMP_DIR,
+            combine_ensemble_with_thread_joining=True,
+        )
+
+        resolution = {"M_1": 10}
+
+        test_pop_1.add_grid_variable(
+            name="lnm1",
+            longname="Primary mass",
+            valuerange=[1, 100],
+            samplerfunc="self.const_linear(math.log(1), math.log(100), {})".format(
+                resolution["M_1"]
+            ),
+            precode="M_1=math.exp(lnm1)",
+            probdist="self.three_part_powerlaw(M_1, 0.1, 0.5, 1.0, 100, -1.3, -2.3, -2.3)*M_1",
+            dphasevol="dlnm1",
+            parameter_name="M_1",
+            condition="",  # Impose a condition on this grid variable. Mostly for a check for yourself
+        )
+
+        _ = test_pop_1.evolve()
+        ensemble_output_1 = test_pop_1.grid_ensemble_results
+
+        self.normal_ensemble_output_name = os.path.join(TMP_DIR, 'test_load_ensemble_ensemble_output.json')
+        self.bzip2_ensemble_output_name = os.path.join(TMP_DIR, 'test_load_ensemble_ensemble_output.json.bz2')
+        self.no_extension_ensemble_output_name = os.path.join(TMP_DIR, 'test_load_ensemble_ensemble_output')
+
+        # Write ensemble to json with normal write
+        test_pop_1.write_ensemble(self.normal_ensemble_output_name)
+
+        # Write ensemble to bzip
+        test_pop_1.write_ensemble(self.bzip2_ensemble_output_name)
+
+        # Write ensemble without extension
+        with open(self.no_extension_ensemble_output_name, 'w') as f:
+            f.write(json.dumps(ensemble_output_1))
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        Simple test to load ensemble with normal filetype
+        """
+
+        # load data
+        loaded_data_1 = load_ensemble(self.normal_ensemble_output_name)
+
+        self.assertTrue(isinstance(loaded_data_1, dict))
+
+    def test_2(self):
+        with Capturing() as _:
+            self._test_2()
+
+    def _test_2(self):
+        """
+        Simple test to load ensemble with msgpack type
+        """
+
+        # load data
+        loaded_data_1 = load_ensemble(self.bzip2_ensemble_output_name)
+
+        self.assertTrue(isinstance(loaded_data_1, dict))
+
+
+    def test_3(self):
+        with Capturing() as _:
+            self._test_3()
+
+    def _test_3(self):
+        """
+        Simple test to load ensemble with timing output
+        """
+
+
+        #
+        capturedOutput = StringIO()          # Create StringIO object
+        sys.stdout = capturedOutput                   #  and redirect stdout.
+        _ = load_ensemble(self.normal_ensemble_output_name, timing=True)
+        sys.stdout = sys.__stdout__                   # Reset redirect.
+
+        self.assertTrue("Took" in capturedOutput.getvalue())
+
+    def test_4(self):
+        with Capturing() as _:
+            self._test_4()
+
+    def _test_4(self):
+        """
+        Simple test to load ensemble conveting to floats and timing
+        """
+
+        #
+        capturedOutput = StringIO()          # Create StringIO object
+        sys.stdout = capturedOutput                   #  and redirect stdout.
+        loaded_data_1 = load_ensemble(self.normal_ensemble_output_name, timing=True, convert_float_keys=True)
+        sys.stdout = sys.__stdout__                   # Reset redirect.
+
+        self.assertTrue("Took" in capturedOutput.getvalue())
+
+
+class test_ensemble_file_type(unittest.TestCase):
+    """
+    Unittests for class ensemble_file_type
+    """
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        filetype tests
+        """
+
+        # Json test
+        input_1 = "/tmp/test.json"
+        output_1 = ensemble_file_type(input_1)
+
+        self.assertEqual(output_1, "JSON")
+
+        # Msgpack
+        input_2 = "/tmp/test.msgpack"
+        output_2 = ensemble_file_type(input_2)
+
+        self.assertEqual(output_2, "msgpack")
+
+        # None
+        input_3 = "/tmp/test"
+        output_3 = ensemble_file_type(input_3)
+
+        self.assertIsNone(output_3)
+
+
+class test_open_ensemble(unittest.TestCase):
+    """
+    Unittests for class open_ensemble:
+    """
+
+    def __init__(self, *args, **kwargs):
+        """
+        init function
+        """
+        super(test_open_ensemble, self).__init__(*args, **kwargs)
+
+        # First
+        test_pop_1 = Population()
+        test_pop_1.set(
+            num_cores=2,
+            verbosity=TEST_VERBOSITY,
+            M_2=1,
+            orbital_period=100000,
+            ensemble=1,
+            ensemble_defer=1,
+            ensemble_filters_off=1,
+            ensemble_filter_STELLAR_TYPE_COUNTS=1,
+            ensemble_dt=1000,
+        )
+        test_pop_1.set(
+            data_dir=TMP_DIR,
+            combine_ensemble_with_thread_joining=True,
+        )
+
+        resolution = {"M_1": 10}
+
+        test_pop_1.add_grid_variable(
+            name="lnm1",
+            longname="Primary mass",
+            valuerange=[1, 100],
+            samplerfunc="self.const_linear(math.log(1), math.log(100), {})".format(
+                resolution["M_1"]
+            ),
+            precode="M_1=math.exp(lnm1)",
+            probdist="self.three_part_powerlaw(M_1, 0.1, 0.5, 1.0, 100, -1.3, -2.3, -2.3)*M_1",
+            dphasevol="dlnm1",
+            parameter_name="M_1",
+            condition="",  # Impose a condition on this grid variable. Mostly for a check for yourself
+        )
+
+        _ = test_pop_1.evolve()
+        ensemble_output_1 = test_pop_1.grid_ensemble_results
+
+        self.normal_ensemble_output_name = os.path.join(TMP_DIR, 'test_open_ensemble_ensemble_output.json')
+        self.bzip2_ensemble_output_name = os.path.join(TMP_DIR, 'test_open_ensemble_ensemble_output.json.bz2')
+        self.gzip_ensemble_output_name = os.path.join(TMP_DIR, 'test_open_ensemble_ensemble_output.json.gz')
+        # self.msgpack_ensemble_output_name = os.path.join(TMP_DIR, 'test_open_ensemble_ensemble_output.msgpack')
+
+        # Write ensemble to json with normal write
+        test_pop_1.write_ensemble(self.normal_ensemble_output_name)
+
+        # Write ensemble to bzip
+        test_pop_1.write_ensemble(self.bzip2_ensemble_output_name)
+
+        # gzip
+        test_pop_1.write_ensemble(self.gzip_ensemble_output_name)
+
+        # # Msgpack
+        # test_pop_1.write_ensemble(self.msgpack_ensemble_output_name)
+
+    def test_1(self):
+        with Capturing() as _:
+            self._test_1()
+
+    def _test_1(self):
+        """
+        filetype tests
+        """
+
+        self.msgpack_ensemble_output_name = os.path.join(TMP_DIR, 'test_open_ensemble_ensemble_output.msgpack.gz')
+
+        #
+        handle_1 = open_ensemble(self.normal_ensemble_output_name)
+        self.assertTrue(isinstance(handle_1, io._io.TextIOWrapper))
+        handle_1.close()
+
+        #
+        handle_2 = open_ensemble(self.bzip2_ensemble_output_name)
+        self.assertTrue(isinstance(handle_2, io._io.TextIOWrapper))
+        handle_2.close()
+
+        #
+        handle_3 = open_ensemble(self.gzip_ensemble_output_name)
+        self.assertTrue(isinstance(handle_3, io._io.TextIOWrapper))
+        handle_3.close()
+
+        # # TODO: implement this again
+        # handle_4 = open_ensemble(self.msgpack_ensemble_output_name)
+        # self.assertTrue(isinstance(handle_4, io._io.TextIOWrapper))
+        # handle_4.close()
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/binarycpython/tests/test_grid.py b/binarycpython/tests/test_grid.py
index 1e146ec34fad370a69ef9ed6135e604756940e5a..37e303d112595bb035667a8c05b4153a80c9a8b8 100644
--- a/binarycpython/tests/test_grid.py
+++ b/binarycpython/tests/test_grid.py
@@ -6,7 +6,6 @@ TODO: exit
 TODO: _set_nprocesses
 TODO: _pre_run_setup
 TODO: clean
-TODO: evolve
 TODO: _evolve_population
 TODO: _system_queue_filler
 TODO: _evolve_population_grid
@@ -660,9 +659,6 @@ class test_resultdict(unittest.TestCase):
             test_case_dict, dict(example_pop.grid_results["example"]["mass"])
         )
 
-
-
-
 class test_grid_evolve(unittest.TestCase):
     """
     Unittests for function Population.evolve()
@@ -955,7 +951,6 @@ Printf("TEST_CUSTOM_LOGGING_1 %30.12e %g %g %g %g\\n",
         )
         test_pop.set(
             data_dir=TMP_DIR,
-            ensemble_output_name="ensemble_output.json",
             combine_ensemble_with_thread_joining=False,
         )
 
@@ -1027,7 +1022,6 @@ Printf("TEST_CUSTOM_LOGGING_1 %30.12e %g %g %g %g\\n",
         test_pop.set(
             data_dir=TMP_DIR,
             combine_ensemble_with_thread_joining=True,
-            ensemble_output_name="ensemble_output.json",
         )
 
         resolution = {"M_1": 10}
@@ -1084,7 +1078,6 @@ Printf("TEST_CUSTOM_LOGGING_1 %30.12e %g %g %g %g\\n",
         test_pop_1.set(
             data_dir=TMP_DIR,
             combine_ensemble_with_thread_joining=True,
-            ensemble_output_name="ensemble_output.json",
         )
 
         resolution = {"M_1": 10}
@@ -1121,7 +1114,6 @@ Printf("TEST_CUSTOM_LOGGING_1 %30.12e %g %g %g %g\\n",
         )
         test_pop_2.set(
             data_dir=TMP_DIR,
-            ensemble_output_name="ensemble_output.json",
             combine_ensemble_with_thread_joining=False,
         )
 
diff --git a/binarycpython/utils/ensemble.py b/binarycpython/utils/ensemble.py
index 7573fc6b58ba1d1c52d3a78df62d4ad4b204e38c..1808ed7c31595b021a4c43fb5d1b377af3d26674 100644
--- a/binarycpython/utils/ensemble.py
+++ b/binarycpython/utils/ensemble.py
@@ -197,11 +197,11 @@ def load_ensemble(
         # data = recursive_change_key_to_float(data) # 61s
         data = keys_to_floats(data)  # 6.94s
 
-    if timing:
-        print(
-            "\n\nTook {} s to convert floats\n\n".format(time.time() - tstart),
-            flush=True,
-        )
+        if timing:
+            print(
+                "\n\nTook {} s to convert floats\n\n".format(time.time() - tstart),
+                flush=True,
+            )
 
     # return data
     return data
@@ -264,10 +264,10 @@ def handle_ensemble_string_to_json(raw_output):
         raw_output: raw output of the ensemble dump by binary_c
 
     Returns:
-        json.loads(raw_output, cls=binarycDecoder)
+        json.loads(raw_output, cls=BinarycDecoder)
 
     """
-    return json.loads(raw_output, cls=binarycDecoder)
+    return json.loads(raw_output, cls=BinarycDecoder)
 
 
 def binaryc_json_serializer(obj: Any) -> Any:
@@ -289,7 +289,7 @@ def binaryc_json_serializer(obj: Any) -> Any:
     return obj
 
 
-class binarycDecoder(json.JSONDecoder):
+class BinarycDecoder(json.JSONDecoder):
     """
     Custom decoder to transform the numbers that are strings to actual floats
     """
@@ -327,7 +327,7 @@ class binarycDecoder(json.JSONDecoder):
             return o
 
 
-class BinaryCEncoder(json.JSONEncoder):
+class BinarycEncoder(json.JSONEncoder):
     """
     Encoding class function to attempt to convert things to strings.
     """
@@ -336,6 +336,7 @@ class BinaryCEncoder(json.JSONEncoder):
         """
         Converting function. Well, could be more precise. look at the JSON module
         """
+
         try:
             str_repr = str(o)
         except TypeError:
diff --git a/binarycpython/utils/population_extensions/dataIO.py b/binarycpython/utils/population_extensions/dataIO.py
index bd1da06bc59924dfe5d4ad2907d267eaffd6ce47..0723da759559885ee7c17fe68390e28f0eae2090 100644
--- a/binarycpython/utils/population_extensions/dataIO.py
+++ b/binarycpython/utils/population_extensions/dataIO.py
@@ -20,6 +20,7 @@ import compress_pickle
 
 from binarycpython.utils.ensemble import (
     ensemble_file_type,
+    ensemble_compression
 )
 from binarycpython.utils.dicts import (
     merge_dicts,
@@ -358,8 +359,8 @@ class dataIO:
             )
             self.exit(code=1)
         else:
-            f = self.open(output_file, "wt", encoding=encoding)
             if file_type == "JSON":
+                f = self.open(output_file, "wt", encoding=encoding)
                 # JSON output
                 f.write(
                     json.dumps(
@@ -370,8 +371,11 @@ class dataIO:
                     )
                 )
             elif file_type == "msgpack":
+                f = self.open(output_file, "w") # TODO: i think something is going wrong here. not sure but doing msgpack and .gz e.g gives an error about str input rather than bytes. i think this is because the self.open does not take into account that the msgpack stream requires different properties.
+
                 # msgpack output
                 msgpack.dump(data, f)
+
             f.close()
 
         print(