diff --git a/pyproject.toml b/pyproject.toml
index bb3257de95090401c88389695e18dff861752bc2..579538438beea2e75d64651da8499fe248c2d93b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,38 @@
 [build-system]
 requires = [
-    "setuptools>=42",
-    "wheel",
-    "setuptools_scm[toml]>3.4",
+    "setuptools>=60",
+    "setuptools-scm>=8.0",
 ]
 build-backend = "setuptools.build_meta"
 
+[project]
+dynamic = ["version"]
+name = "zolfa-nd2reader"
+authors = [
+    {name = "Ruben Verweij", email = "ruben@lighthacking.nl"},
+    {name = "Lorenzo Zolfanelli", email = "lorenzo.zolfanelli@espci.psl.eu"}
+]
+maintainers = [
+    {name = "Lorenzo Zolfanelli", email = "lorenzo.zolfanelli@espci.psl.eu"}
+]
+description = "A tool for reading ND2 files produced by NIS Elements"
+readme = "README.md"
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+    "Intended Audience :: Science/Research",
+    "Topic :: Scientific/Engineering :: Image Processing",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12"
+]
+
+dependencies = [
+    "numpy >= 1.14",
+    "xmltodict >= 0.9.2",
+    "PIMS >= 0.5.0"
+]
+
 [tool.setuptools_scm]
 write_to = "src/zolfa/nd2reader/version.py"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 291f4e5a8ce985a08e3deab00aaedbedccc13d9a..0000000000000000000000000000000000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,42 +0,0 @@
-[metadata]
-name = zolfa-nd2reader
-version = attr:zolfa.nd2reader.version
-description = A tool for reading ND2 files produced by NIS Elements
-description-file = README.md
-classifiers = 
-    Development Status :: 5 - Production/Stable
-    Intended Audience :: Science/Research
-    License :: Freely Distributable
-    License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
-    Operating System :: POSIX :: Linux
-    Programming Language :: Python :: 2.7
-    Programming Language :: Python :: 3.4
-    Topic :: Scientific/Engineering
-keywords =
-    nd2
-    nikon
-    microscopy
-    NIS Elements
-author = Ruben Verweij, Lorenzo Zolfanelli
-author_email = dev@zolfa.nl
-url = https://projects.lilik.it/zolfa/zolfa-nd2reader
-
-[options]
-packages = find_namespace:
-package_dir =
-    = src
-python_requires = >=3.6
-install_requires =
-    numpy>=1.14
-    six>=1.4
-    xmltodict>=0.9.2
-    PIMS>=0.5.0
-setup_requires =
-    setuptools_scm
-
-[options.packages.find]
-where = src
-include = zolfa.*
-
-[bdist_wheel]
-universal=1
\ No newline at end of file
diff --git a/src/zolfa/nd2reader/__init__.py b/src/zolfa/nd2reader/__init__.py
index 3b424e162f3706568d8be5223edbb2df48670cc9..9aa0d339234a8587ca6c84110999a38fe26c5824 100644
--- a/src/zolfa/nd2reader/__init__.py
+++ b/src/zolfa/nd2reader/__init__.py
@@ -2,10 +2,7 @@ from os import path
 from zolfa.nd2reader.reader import ND2Reader
 from zolfa.nd2reader.legacy import Nd2
 
-try:
-    import importlib.metadata as importlib_metadata
-except:
-    import importlib_metadata
+import importlib.metadata as importlib_metadata
 
 try:
     __version__ = importlib_metadata.version(__name__)
diff --git a/src/zolfa/nd2reader/artificial.py b/src/zolfa/nd2reader/artificial.py
index 05f460944333f7af860444a43cafaecac2a26fe9..8c72ff253c08fd5f130629ce9022832109fc9cd4 100644
--- a/src/zolfa/nd2reader/artificial.py
+++ b/src/zolfa/nd2reader/artificial.py
@@ -1,6 +1,5 @@
 """Functions to create artificial nd2 data for testing purposes
 """
-import six
 import numpy as np
 import struct
 from zolfa.nd2reader.common import check_or_make_dir
@@ -14,18 +13,18 @@ global_labels = ['image_attributes', 'image_text_info', 'image_metadata',
                  'acquisition_frames', 'lut_data', 'grabber_settings',
                  'custom_data', 'app_info', 'image_frame_0']
 
-global_file_labels = ["ImageAttributesLV!", "ImageTextInfoLV!",
-                      "ImageMetadataLV!", "ImageMetadataSeqLV|0!",
-                      "ImageCalibrationLV|0!", "CustomData|X!", "CustomData|Y!",
-                      "CustomData|Z!", "CustomData|RoiMetadata_v1!",
-                      "CustomData|PFS_STATUS!", "CustomData|PFS_OFFSET!",
-                      "CustomData|GUIDStore!", "CustomData|CustomDescriptionV1_0!",
-                      "CustomData|Camera_ExposureTime1!", "CustomData|CameraTemp1!",
-                      "CustomData|AcqTimesCache!", "CustomData|AcqTimes2Cache!",
-                      "CustomData|AcqFramesCache!", "CustomDataVar|LUTDataV1_0!",
-                      "CustomDataVar|GrabberCameraSettingsV1_0!",
-                      "CustomDataVar|CustomDataV2_0!", "CustomDataVar|AppInfo_V1_0!",
-                      "ImageDataSeq|0!"]
+global_file_labels = [b"ImageAttributesLV!", b"ImageTextInfoLV!",
+                      b"ImageMetadataLV!", b"ImageMetadataSeqLV|0!",
+                      b"ImageCalibrationLV|0!", b"CustomData|X!", b"CustomData|Y!",
+                      b"CustomData|Z!", b"CustomData|RoiMetadata_v1!",
+                      b"CustomData|PFS_STATUS!", b"CustomData|PFS_OFFSET!",
+                      b"CustomData|GUIDStore!", b"CustomData|CustomDescriptionV1_0!",
+                      b"CustomData|Camera_ExposureTime1!", b"CustomData|CameraTemp1!",
+                      b"CustomData|AcqTimesCache!", b"CustomData|AcqTimes2Cache!",
+                      b"CustomData|AcqFramesCache!", b"CustomDataVar|LUTDataV1_0!",
+                      b"CustomDataVar|GrabberCameraSettingsV1_0!",
+                      b"CustomDataVar|CustomDataV2_0!", b"CustomDataVar|AppInfo_V1_0!",
+                      b"ImageDataSeq|0!"]
 
 
 class ArtificialND2(object):
@@ -107,7 +106,7 @@ class ArtificialND2(object):
         self.raw_text += struct.pack("Q", location)
 
     def _get_version_string(self):
-        return six.b('ND2 FILE SIGNATURE CHUNK NAME01!Ver%s.%s' % self.version)
+        return b'ND2 FILE SIGNATURE CHUNK NAME01!Ver%d.%d' % self.version
 
     def _get_version_byte_length(self):
         return 16 + len(self._get_version_string())
@@ -125,7 +124,7 @@ class ArtificialND2(object):
             tuple: (binary data, dictionary data)
 
         """
-        raw_text = six.b('')
+        raw_text = b''
         labels = global_labels
         file_labels = global_file_labels
 
@@ -137,19 +136,19 @@ class ArtificialND2(object):
         version_length = self._get_version_byte_length()
 
         # calculate data length
-        label_length = np.sum([len(six.b(l)) + 16 for l in file_labels])
+        label_length = np.sum([len(l) + 16 for l in file_labels])
 
         # write label map
         cur_pos = version_length + label_length
         for label, file_label, data in zip(labels, file_labels, file_data):
-            raw_text += six.b(file_label)
+            raw_text += file_label
             data_length = len(data)
             raw_text += struct.pack('QQ', cur_pos, data_length)
             locations[label] = (cur_pos, data_length)
             cur_pos += data_length
 
         # write data
-        raw_text += six.b('').join(file_data)
+        raw_text += b''.join(file_data)
 
         return raw_text, locations, file_data_dict
 
@@ -170,7 +169,7 @@ class ArtificialND2(object):
             raw_data = struct.pack('I', data)
         elif isinstance(data, float):
             raw_data = struct.pack('d', data)
-        elif isinstance(data, str):
+        elif isinstance(data, bytes):
             raw_data = self._str_to_padded_bytes(data)
 
         return raw_data
@@ -180,14 +179,14 @@ class ArtificialND2(object):
             return self.data_types['metadata_item']
         elif isinstance(data, int):
             return self.data_types['unsigned_int']
-        elif isinstance(data, str):
+        elif isinstance(data, bytes):
             return self.data_types['string']
         else:
             return self.data_types['double']
 
     @staticmethod
     def _str_to_padded_bytes(data):
-        return six.b('').join([struct.pack('cx', six.b(s)) for s in data]) + struct.pack('xx')
+        return b''.join([struct.pack('Bx', s) for s in data]) + struct.pack('xx')
 
     def _pack_dict_with_metadata(self, data):
         raw_data = b''
@@ -217,44 +216,44 @@ class ArtificialND2(object):
 
     @staticmethod
     def _get_slx_img_attrib():
-        return {'uiWidth': 128,
-                'uiWidthBytes': 256,
-                'uiHeight': 128,
-                'uiComp': 1,
-                'uiBpcInMemory': 16,
-                'uiBpcSignificant': 12,
-                'uiSequenceCount': 70,
-                'uiTileWidth': 128,
-                'uiTileHeight': 128,
-                'eCompression': 2,
-                'dCompressionParam': -1.0,
-                'ePixelType': 1,
-                'uiVirtualComponents': 1
+        return {b'uiWidth': 128,
+                b'uiWidthBytes': 256,
+                b'uiHeight': 128,
+                b'uiComp': 1,
+                b'uiBpcInMemory': 16,
+                b'uiBpcSignificant': 12,
+                b'uiSequenceCount': 70,
+                b'uiTileWidth': 128,
+                b'uiTileHeight': 128,
+                b'eCompression': 2,
+                b'dCompressionParam': -1.0,
+                b'ePixelType': 1,
+                b'uiVirtualComponents': 1
                 }
 
     @staticmethod
     def _get_slx_picture_metadata():
-        return {'sPicturePlanes':
+        return {b'sPicturePlanes':
                 {
-                    'sPlaneNew': {
+                    b'sPlaneNew': {
                         # channels are numbered a0, a1, ..., aN
-                        'a0': {
-                            'sDescription': 'TRITC'
-                            }
+                        b'a0': {
+                            b'sDescription': b'TRITC',
                         }
                     }
-                }
+                },
+        }
 
     def _get_file_data(self, labels):
         file_data = [
-            {'SLxImageAttributes': self._get_slx_img_attrib()},  # ImageAttributesLV!",
+            {b'SLxImageAttributes': self._get_slx_img_attrib()},  # ImageAttributesLV!",
             7,  # ImageTextInfoLV!",
             7,  # ImageMetadataLV!",
-            {'SLxPictureMetadata': self._get_slx_picture_metadata()},  # ImageMetadataSeqLV|0!",
+            {b'SLxPictureMetadata': self._get_slx_picture_metadata()},  # ImageMetadataSeqLV|0!",
             7,  # ImageCalibrationLV|0!",
-            7,  # CustomData|X!",
-            7,  # CustomData|Y!",
-            7,  # CustomData|Z!",
+            7.,  # CustomData|X!",
+            7.,  # CustomData|Y!",
+            7.,  # CustomData|Z!",
             7,  # CustomData|RoiMetadata_v1!",
             7,  # CustomData|PFS_STATUS!",
             7,  # CustomData|PFS_OFFSET!",
diff --git a/src/zolfa/nd2reader/common.py b/src/zolfa/nd2reader/common.py
index c326bd83635d484b596cac649889e2eb142b187d..db99b52b914b423f5f3ca9f3a1c14ca2eb0fec79 100644
--- a/src/zolfa/nd2reader/common.py
+++ b/src/zolfa/nd2reader/common.py
@@ -2,7 +2,7 @@ import os
 import struct
 import array
 from datetime import datetime
-import six
+import io
 import re
 from zolfa.nd2reader.exceptions import InvalidVersionError
 
@@ -156,7 +156,7 @@ def _parse_string(data):
         """
     value = data.read(2)
     # the string ends at the first instance of \x00\x00
-    while not value.endswith(six.b("\x00\x00")):
+    while not value.endswith(b"\x00\x00"):
         next_data = data.read(2)
         if len(next_data) == 0:
             break
@@ -275,7 +275,7 @@ def read_metadata(data, count):
     if data is None:
         return None
 
-    data = six.BytesIO(data)
+    data = io.BytesIO(data)
     metadata = {}
 
     for _ in range(count):
@@ -336,7 +336,7 @@ def get_from_dict_if_exists(key, dictionary, convert_key_to_binary=True):
 
     """
     if convert_key_to_binary:
-        key = six.b(key)
+        key = key.encode('latin-1')
 
     if key not in dictionary:
         return None
diff --git a/src/zolfa/nd2reader/common_raw_metadata.py b/src/zolfa/nd2reader/common_raw_metadata.py
index 408339fa31ddd19c7fa1fc7265000e47d956cd89..21e7d04e939bd88bb0f9a51f613e9460f416c837 100644
--- a/src/zolfa/nd2reader/common_raw_metadata.py
+++ b/src/zolfa/nd2reader/common_raw_metadata.py
@@ -1,4 +1,3 @@
-import six
 import warnings
 
 from zolfa.nd2reader.common import get_from_dict_if_exists
@@ -11,10 +10,10 @@ def parse_if_not_none(to_check, callback):
 
 
 def parse_dimension_text_line(line):
-    if six.b("Dimensions:") in line:
-        entries = line.split(six.b("\r\n"))
+    if b"Dimensions:" in line:
+        entries = line.split(b"\r\n")
         for entry in entries:
-            if entry.startswith(six.b("Dimensions:")):
+            if entry.startswith(b"Dimensions:"):
                 return entry
     return None
 
@@ -41,22 +40,22 @@ def parse_roi_type(type_no):
 
 def get_loops_from_data(loop_data):
     # special ND experiment
-    if six.b('pPeriod') not in loop_data:
+    if b'pPeriod' not in loop_data:
         return []
 
-    if six.b('uiPeriodCount') in loop_data and loop_data[six.b('uiPeriodCount')] > 0:
+    if b'uiPeriodCount' in loop_data and loop_data[b'uiPeriodCount'] > 0:
         loops = []
-        for i, period in enumerate(loop_data[six.b('pPeriod')]):
+        for i, period in enumerate(loop_data[b'pPeriod']):
             # exclude invalid periods
-            if six.b('pPeriodValid') in loop_data:
+            if b'pPeriodValid' in loop_data:
                 try:
-                    if loop_data[six.b('pPeriodValid')][i] == 1:
-                        loops.append(loop_data[six.b('pPeriod')][period])
+                    if loop_data[b'pPeriodValid'][i] == 1:
+                        loops.append(loop_data[b'pPeriod'][period])
                 except IndexError:
                     continue
             else:
                 # we can't be sure, append all
-                loops.append(loop_data[six.b('pPeriod')][period])
+                loops.append(loop_data[b'pPeriod'][period])
 
     return [loop_data]
 
diff --git a/src/zolfa/nd2reader/label_map.py b/src/zolfa/nd2reader/label_map.py
index dcd2e9cfe6cd4b5cfd82d1bb8cf920dbbbde5a1f..f51d4be973df28f61089541292604bb911ce4c75 100644
--- a/src/zolfa/nd2reader/label_map.py
+++ b/src/zolfa/nd2reader/label_map.py
@@ -1,4 +1,3 @@
-import six
 import struct
 import re
 
@@ -31,7 +30,7 @@ class LabelMap(object):
             int: The location of the textual image information
 
         """
-        return self._get_location(six.b("ImageTextInfoLV!"))
+        return self._get_location(b"ImageTextInfoLV!")
 
     @property
     def image_metadata(self):
@@ -41,7 +40,7 @@ class LabelMap(object):
             int: The location of the image metadata
 
         """
-        return self._get_location(six.b("ImageMetadataLV!"))
+        return self._get_location(b"ImageMetadataLV!")
 
     @property
     def image_events(self):
@@ -51,7 +50,7 @@ class LabelMap(object):
             int: The location of the image events
 
         """
-        return self._get_location(six.b("ImageEventsLV!"))
+        return self._get_location(b"ImageEventsLV!")
 
     @property
     def image_metadata_sequence(self):
@@ -62,7 +61,7 @@ class LabelMap(object):
             int: The location of the image metadata sequence
 
         """
-        return self._get_location(six.b("ImageMetadataSeqLV|0!"))
+        return self._get_location(b"ImageMetadataSeqLV|0!")
 
     def get_image_data_location(self, index):
         """Get the location of the image data
@@ -72,7 +71,7 @@ class LabelMap(object):
 
         """
         if not self._image_data:
-            regex = re.compile(six.b("""ImageDataSeq\|(\d+)!"""))
+            regex = re.compile(b"""ImageDataSeq\|(\d+)!""")
             for match in regex.finditer(self._data):
                 if match:
                     location = self._parse_data_location(match.end())
@@ -87,7 +86,7 @@ class LabelMap(object):
             int: The location of the image calibration
 
         """
-        return self._get_location(six.b("ImageCalibrationLV|0!"))
+        return self._get_location(b"ImageCalibrationLV|0!")
 
     @property
     def image_attributes(self):
@@ -97,7 +96,7 @@ class LabelMap(object):
             int: The location of the image attributes
 
         """
-        return self._get_location(six.b("ImageAttributesLV!"))
+        return self._get_location(b"ImageAttributesLV!")
 
     @property
     def x_data(self):
@@ -107,7 +106,7 @@ class LabelMap(object):
             int: The location of the custom x data
 
         """
-        return self._get_location(six.b("CustomData|X!"))
+        return self._get_location(b"CustomData|X!")
 
     @property
     def y_data(self):
@@ -117,7 +116,7 @@ class LabelMap(object):
             int: The location of the custom y data
 
         """
-        return self._get_location(six.b("CustomData|Y!"))
+        return self._get_location(b"CustomData|Y!")
 
     @property
     def z_data(self):
@@ -127,7 +126,7 @@ class LabelMap(object):
             int: The location of the custom z data
 
         """
-        return self._get_location(six.b("CustomData|Z!"))
+        return self._get_location(b"CustomData|Z!")
 
     @property
     def roi_metadata(self):
@@ -137,7 +136,7 @@ class LabelMap(object):
             int: The location of the regions of interest (ROIs)
 
         """
-        return self._get_location(six.b("CustomData|RoiMetadata_v1!"))
+        return self._get_location(b"CustomData|RoiMetadata_v1!")
 
     @property
     def pfs_status(self):
@@ -147,7 +146,7 @@ class LabelMap(object):
             int: The location of the perfect focus system (PFS) status
 
         """
-        return self._get_location(six.b("CustomData|PFS_STATUS!"))
+        return self._get_location(b"CustomData|PFS_STATUS!")
 
     @property
     def pfs_offset(self):
@@ -157,7 +156,7 @@ class LabelMap(object):
             int: The location of the perfect focus system (PFS) offset
 
         """
-        return self._get_location(six.b("CustomData|PFS_OFFSET!"))
+        return self._get_location(b"CustomData|PFS_OFFSET!")
 
     @property
     def guid(self):
@@ -167,7 +166,7 @@ class LabelMap(object):
             int: The location of the image guid
 
         """
-        return self._get_location(six.b("CustomData|GUIDStore!"))
+        return self._get_location(b"CustomData|GUIDStore!")
 
     @property
     def description(self):
@@ -177,7 +176,7 @@ class LabelMap(object):
             int: The location of the image description
 
         """
-        return self._get_location(six.b("CustomData|CustomDescriptionV1_0!"))
+        return self._get_location(b"CustomData|CustomDescriptionV1_0!")
 
     @property
     def camera_exposure_time(self):
@@ -187,7 +186,7 @@ class LabelMap(object):
             int: The location of the camera exposure time
 
         """
-        return self._get_location(six.b("CustomData|Camera_ExposureTime1!"))
+        return self._get_location(b"CustomData|Camera_ExposureTime1!")
 
     @property
     def camera_temp(self):
@@ -197,7 +196,7 @@ class LabelMap(object):
             int: The location of the camera temperature
 
         """
-        return self._get_location(six.b("CustomData|CameraTemp1!"))
+        return self._get_location(b"CustomData|CameraTemp1!")
 
     @property
     def acquisition_times(self):
@@ -207,7 +206,7 @@ class LabelMap(object):
             int: The location of the acquisition times, block 1
 
         """
-        return self._get_location(six.b("CustomData|AcqTimesCache!"))
+        return self._get_location(b"CustomData|AcqTimesCache!")
 
     @property
     def acquisition_times_2(self):
@@ -217,7 +216,7 @@ class LabelMap(object):
             int: The location of the acquisition times, block 2
 
         """
-        return self._get_location(six.b("CustomData|AcqTimes2Cache!"))
+        return self._get_location(b"CustomData|AcqTimes2Cache!")
 
     @property
     def acquisition_frames(self):
@@ -227,7 +226,7 @@ class LabelMap(object):
             int: The location of the acquisition frames
 
         """
-        return self._get_location(six.b("CustomData|AcqFramesCache!"))
+        return self._get_location(b"CustomData|AcqFramesCache!")
 
     @property
     def lut_data(self):
@@ -237,7 +236,7 @@ class LabelMap(object):
             int: The location of the LUT data
 
         """
-        return self._get_location(six.b("CustomDataVar|LUTDataV1_0!"))
+        return self._get_location(b"CustomDataVar|LUTDataV1_0!")
 
     @property
     def grabber_settings(self):
@@ -247,7 +246,7 @@ class LabelMap(object):
             int: The location of the grabber settings
 
         """
-        return self._get_location(six.b("CustomDataVar|GrabberCameraSettingsV1_0!"))
+        return self._get_location(b"CustomDataVar|GrabberCameraSettingsV1_0!")
 
     @property
     def custom_data(self):
@@ -257,7 +256,7 @@ class LabelMap(object):
             int: The location of the custom user data
 
         """
-        return self._get_location(six.b("CustomDataVar|CustomDataV2_0!"))
+        return self._get_location(b"CustomDataVar|CustomDataV2_0!")
 
     @property
     def app_info(self):
@@ -267,4 +266,4 @@ class LabelMap(object):
             int: The location of the application info metadata
 
         """
-        return self._get_location(six.b("CustomDataVar|AppInfo_V1_0!"))
+        return self._get_location(b"CustomDataVar|AppInfo_V1_0!")
diff --git a/src/zolfa/nd2reader/parser.py b/src/zolfa/nd2reader/parser.py
index e3ceea3cb4ed578744160756393f8f04a1b5c73e..45fcde16ef6ac2550f1c8e2f0ca77aa283aff5cf 100644
--- a/src/zolfa/nd2reader/parser.py
+++ b/src/zolfa/nd2reader/parser.py
@@ -2,7 +2,6 @@
 import struct
 
 import array
-import six
 import warnings
 from pims.base_frames import Frame
 import numpy as np
@@ -18,8 +17,8 @@ class Parser(object):
 
     """
     CHUNK_HEADER = 0xabeceda
-    CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!")
-    CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
+    CHUNK_MAP_START = b"ND2 FILEMAP SIGNATURE NAME 0001!"
+    CHUNK_MAP_END = b"ND2 CHUNK MAP SIGNATURE 0000001!"
 
     supported_file_versions = {(3, None): True}
 
diff --git a/src/zolfa/nd2reader/raw_metadata.py b/src/zolfa/nd2reader/raw_metadata.py
index 9b2e8e38707a7b6ce0dd1ef1a770f0d30dfd7bad..7ef593dc0a1444d67ba970a33af5db194b3cf17b 100644
--- a/src/zolfa/nd2reader/raw_metadata.py
+++ b/src/zolfa/nd2reader/raw_metadata.py
@@ -1,6 +1,5 @@
 import re
 import xmltodict
-import six
 import numpy as np
 import warnings
 
@@ -74,7 +73,7 @@ class RawMetadata(object):
 
     def _parse_width_or_height(self, key):
         try:
-            length = self.image_attributes[six.b('SLxImageAttributes')][six.b(key)]
+            length = self.image_attributes[b'SLxImageAttributes'][key.encode('latin-1')]
         except KeyError:
             length = None
 
@@ -88,13 +87,13 @@ class RawMetadata(object):
 
     def _parse_date(self):
         try:
-            return parse_date(self.image_text_info[six.b('SLxImageTextInfo')])
+            return parse_date(self.image_text_info[b'SLxImageTextInfo'])
         except KeyError:
             return None
 
     def _parse_calibration(self):
         try:
-            return self.image_calibration.get(six.b('SLxCalibration'), {}).get(six.b('dCalibration'))
+            return self.image_calibration.get(b'SLxCalibration', {}).get(b'dCalibration')
         except KeyError:
             return None
 
@@ -117,7 +116,7 @@ class RawMetadata(object):
             return []
 
         try:
-            metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
+            metadata = self.image_metadata_sequence[b'SLxPictureMetadata'][b'sPicturePlanes']
         except KeyError:
             return []
 
@@ -132,19 +131,19 @@ class RawMetadata(object):
         # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
         # we get the correct order.
         channels = []
-        for valid, (label, chan) in zip(validity, sorted(metadata[six.b('sPlaneNew')].items())):
+        for valid, (label, chan) in zip(validity, sorted(metadata[b'sPlaneNew'].items())):
             if not valid:
                 continue
-            if chan[six.b('sDescription')] is not None:
-                channels.append(chan[six.b('sDescription')].decode("utf8"))
+            if chan[b'sDescription'] is not None:
+                channels.append(chan[b'sDescription'].decode("utf8"))
             else:
                 channels.append('Unknown')
         return channels
 
     def _get_channel_validity_list(self, metadata):
         try:
-            validity = self.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][
-                six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
+            validity = self.image_metadata[b'SLxExperiment'][b'ppNextLevelEx'][b''][0][
+                b'ppNextLevelEx'][b''][0][b'pItemValid']
         except (KeyError, TypeError):
             # If none of the channels have been deleted, there is no validity list, so we just make one
             validity = [True for _ in metadata]
@@ -220,7 +219,7 @@ class RawMetadata(object):
             return []
 
         try:
-            metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')]
+            metadata = self.image_metadata_sequence[b'SLxPictureMetadata']
         except KeyError:
             return []
 
@@ -234,7 +233,7 @@ class RawMetadata(object):
             return []
 
         try:
-            metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')][b'sPicturePlanes']
+            metadata = self.image_metadata_sequence[b'SLxPictureMetadata'][b'sPicturePlanes']
         except KeyError:
             return []
 
@@ -261,12 +260,12 @@ class RawMetadata(object):
         is always there and in the same exact format, so we just parse that instead.
 
         """
-        dimension_text = six.b("")
+        dimension_text = b""
         if self.image_text_info is None:
             return dimension_text
 
         try:
-            textinfo = self.image_text_info[six.b('SLxImageTextInfo')].values()
+            textinfo = self.image_text_info[b'SLxImageTextInfo'].values()
         except KeyError:
             return dimension_text
 
@@ -282,8 +281,7 @@ class RawMetadata(object):
         if dimension_text is None:
             return []
 
-        if six.PY3:
-            dimension_text = dimension_text.decode("utf8")
+        dimension_text = dimension_text.decode("utf8")
 
         match = re.match(pattern, dimension_text)
         if not match:
@@ -301,7 +299,7 @@ class RawMetadata(object):
         if self.image_attributes is None:
             return 0
         try:
-            total_images = self.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
+            total_images = self.image_attributes[b'SLxImageAttributes'][b'uiSequenceCount']
         except KeyError:
             total_images = None
 
@@ -311,19 +309,19 @@ class RawMetadata(object):
         """Parse the raw ROI metadata.
 
         """
-        if self.roi_metadata is None or not six.b('RoiMetadata_v1') in self.roi_metadata:
+        if self.roi_metadata is None or not b'RoiMetadata_v1' in self.roi_metadata:
             return
 
-        raw_roi_data = self.roi_metadata[six.b('RoiMetadata_v1')]
+        raw_roi_data = self.roi_metadata[b'RoiMetadata_v1']
 
-        if not six.b('m_vectGlobal_Size') in raw_roi_data:
+        if not b'm_vectGlobal_Size' in raw_roi_data:
             return
 
-        number_of_rois = raw_roi_data[six.b('m_vectGlobal_Size')]
+        number_of_rois = raw_roi_data[b'm_vectGlobal_Size']
 
         roi_objects = []
         for i in range(number_of_rois):
-            current_roi = raw_roi_data[six.b('m_vectGlobal_%d' % i)]
+            current_roi = raw_roi_data[b'm_vectGlobal_%d' % i]
             roi_objects.append(self._parse_roi(current_roi))
 
         self._metadata_parsed['rois'] = roi_objects
@@ -340,17 +338,17 @@ class RawMetadata(object):
             dict: the parsed ROI metadata
 
         """
-        number_of_timepoints = raw_roi_dict[six.b('m_vectAnimParams_Size')]
+        number_of_timepoints = raw_roi_dict[b'm_vectAnimParams_Size']
 
         roi_dict = {
             "timepoints": [],
             "positions": [],
             "sizes": [],
-            "shape": parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]),
-            "type": parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')])
+            "shape": parse_roi_shape(raw_roi_dict[b'm_sInfo'][b'm_uiShapeType']),
+            "type": parse_roi_type(raw_roi_dict[b'm_sInfo'][b'm_uiInterpType'])
         }
         for i in range(number_of_timepoints):
-            roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[six.b('m_vectAnimParams_%d' % i)])
+            roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[b'm_vectAnimParams_%d' % i])
 
         # convert to NumPy arrays
         roi_dict["timepoints"] = np.array(roi_dict["timepoints"], dtype=np.float)
@@ -371,23 +369,23 @@ class RawMetadata(object):
             dict: the parsed metadata
 
         """
-        roi_dict["timepoints"].append(animation_dict[six.b('m_dTimeMs')])
+        roi_dict["timepoints"].append(animation_dict[b'm_dTimeMs'])
 
         image_width = self._metadata_parsed["width"] * self._metadata_parsed["pixel_microns"]
         image_height = self._metadata_parsed["height"] * self._metadata_parsed["pixel_microns"]
 
         # positions are taken from the center of the image as a fraction of the half width/height of the image
-        position = np.array((0.5 * image_width * (1 + animation_dict[six.b('m_dCenterX')]),
-                             0.5 * image_height * (1 + animation_dict[six.b('m_dCenterY')]),
-                             animation_dict[six.b('m_dCenterZ')]))
+        position = np.array((0.5 * image_width * (1 + animation_dict[b'm_dCenterX']),
+                             0.5 * image_height * (1 + animation_dict[b'm_dCenterY']),
+                             animation_dict[b'm_dCenterZ']))
         roi_dict["positions"].append(position)
 
-        size_dict = animation_dict[six.b('m_sBoxShape')]
+        size_dict = animation_dict[b'm_sBoxShape']
 
         # sizes are fractions of the half width/height of the image
-        roi_dict["sizes"].append((size_dict[six.b('m_dSizeX')] * 0.25 * image_width,
-                                  size_dict[six.b('m_dSizeY')] * 0.25 * image_height,
-                                  size_dict[six.b('m_dSizeZ')]))
+        roi_dict["sizes"].append((size_dict[b'm_dSizeX'] * 0.25 * image_width,
+                                  size_dict[b'm_dSizeY'] * 0.25 * image_height,
+                                  size_dict[b'm_dSizeZ']))
         return roi_dict
 
     def _parse_experiment_metadata(self):
@@ -399,16 +397,16 @@ class RawMetadata(object):
             'loops': []
         }
 
-        if self.image_metadata is None or six.b('SLxExperiment') not in self.image_metadata:
+        if self.image_metadata is None or b'SLxExperiment' not in self.image_metadata:
             return
 
-        raw_data = self.image_metadata[six.b('SLxExperiment')]
+        raw_data = self.image_metadata[b'SLxExperiment']
 
-        if six.b('wsApplicationDesc') in raw_data:
-            self._metadata_parsed['experiment']['description'] = raw_data[six.b('wsApplicationDesc')].decode('utf8')
+        if b'wsApplicationDesc' in raw_data:
+            self._metadata_parsed['experiment']['description'] = raw_data[b'wsApplicationDesc'].decode('utf8')
 
-        if six.b('uLoopPars') in raw_data:
-            self._metadata_parsed['experiment']['loops'] = self._parse_loop_data(raw_data[six.b('uLoopPars')])
+        if b'uLoopPars' in raw_data:
+            self._metadata_parsed['experiment']['loops'] = self._parse_loop_data(raw_data[b'uLoopPars'])
 
     def _parse_loop_data(self, loop_data):
         """Parse the experimental loop data
@@ -507,19 +505,19 @@ class RawMetadata(object):
 
         events = read_metadata(read_chunk(self._fh, self._label_map.image_events), 1)
 
-        if events is None or six.b('RLxExperimentRecord') not in events:
+        if events is None or b'RLxExperimentRecord' not in events:
             return
 
-        events = events[six.b('RLxExperimentRecord')][six.b('pEvents')]
+        events = events[b'RLxExperimentRecord'][b'pEvents']
 
         if len(events) == 0:
             return
 
-        for event in events[six.b('')]:
+        for event in events[b'']:
             event_info = {
-                'index': event[six.b('I')],
-                'time': event[six.b('T')],
-                'type': event[six.b('M')],
+                'index': event[b'I'],
+                'time': event[b'T'],
+                'type': event[b'M'],
             }
             if event_info['type'] in event_names.keys():
                 event_info['name'] = event_names[event_info['type']]
diff --git a/tests/test_artificial.py b/tests/test_artificial.py
index 8595d3d676e9289c7510a3ad18fad1bdebef8db5..ab9de287cfd177a79effc369e47d4a03a6a21053 100644
--- a/tests/test_artificial.py
+++ b/tests/test_artificial.py
@@ -1,6 +1,5 @@
 import unittest
 from os import path
-import six
 import struct
 
 from zolfa.nd2reader.artificial import ArtificialND2
diff --git a/tests/test_common.py b/tests/test_common.py
index 27c548bb191c92ac44bfa6d0a69237fdd66c648c..b594cb16272d95f704e51b0d239f2faa920468b6 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -2,7 +2,7 @@ import unittest
 from os import path
 
 import array
-import six
+import io
 import struct
 
 from zolfa.nd2reader.artificial import ArtificialND2
@@ -48,20 +48,20 @@ class TestCommon(unittest.TestCase):
     def test_parse_date_24(self):
         date_format = "%m/%d/%Y  %H:%M:%S"
         date = '02/13/2016  23:43:37'
-        textinfo = {six.b('TextInfoItem9'): six.b(date)}
+        textinfo = {b'TextInfoItem9': date.encode('latin-1')}
         result = parse_date(textinfo)
         self.assertEqual(result.strftime(date_format), date)
 
     def test_parse_date_12(self):
         date_format = "%m/%d/%Y  %I:%M:%S %p"
         date = '02/13/2016  11:43:37 PM'
-        textinfo = {six.b('TextInfoItem9'): six.b(date)}
+        textinfo = {b'TextInfoItem9': date.encode('latin-1')}
         result = parse_date(textinfo)
         self.assertEqual(result.strftime(date_format), date)
 
     def test_parse_date_exception(self):
         date = 'i am no date'
-        textinfo = {six.b('TextInfoItem9'): six.b(date)}
+        textinfo = {b'TextInfoItem9': date.encode('latin-1')}
         result = parse_date(textinfo)
         self.assertIsNone(result)
 
@@ -82,7 +82,7 @@ class TestCommon(unittest.TestCase):
 
     @staticmethod
     def _prepare_bin_stream(binary_format, *value):
-        file = six.BytesIO()
+        file = io.BytesIO()
         data = struct.pack(binary_format, *value)
         file.write(data)
         file.seek(0)
@@ -101,10 +101,10 @@ class TestCommon(unittest.TestCase):
         file = self._prepare_bin_stream("d", 47.9)
         self.assertEqual(_parse_double(file), 47.9)
 
-        test_string = 'colloid'
-        file = self._prepare_bin_stream("%ds" % len(test_string), six.b(test_string))
+        test_string = b'colloid'
+        file = self._prepare_bin_stream("%ds" % len(test_string), test_string)
         parsed = _parse_string(file)
-        self.assertEqual(parsed, six.b(test_string))
+        self.assertEqual(parsed, test_string)
 
         test_data = [1, 2, 3, 4, 5]
         file = self._prepare_bin_stream("Q" + ''.join(['B'] * len(test_data)), len(test_data), *test_data)
@@ -113,7 +113,7 @@ class TestCommon(unittest.TestCase):
 
     def test_get_from_dict_if_exists(self):
         test_dict = {
-            six.b('existing'): 'test',
+            b'existing': 'test',
             'string': 'test2'
         }
 
@@ -127,7 +127,7 @@ class TestCommon(unittest.TestCase):
             chunk_location = artificial.locations['image_attributes'][0]
 
             chunk_read = read_chunk(fh, chunk_location)
-            real_data = six.BytesIO(artificial.raw_text)
+            real_data = io.BytesIO(artificial.raw_text)
 
             real_data.seek(chunk_location)
 
diff --git a/tests/test_raw_metadata.py b/tests/test_raw_metadata.py
index 8f560921a3cf1e0a7957a2711964c1f521dd1f30..0eddbf6cb51a384410e9d56989f05a0921802d96 100644
--- a/tests/test_raw_metadata.py
+++ b/tests/test_raw_metadata.py
@@ -1,5 +1,4 @@
 import unittest
-import six
 
 from zolfa.nd2reader.artificial import ArtificialND2
 from zolfa.nd2reader.label_map import LabelMap
@@ -26,9 +25,9 @@ class TestRawMetadata(unittest.TestCase):
         self.assertIsNone(parse_roi_type(-1))
 
     def test_parse_dimension_text(self):
-        line = six.b('Metadata:\r\nDimensions: T(443) x \xce\xbb(1)\r\nCamera Name: Andor Zyla VSC-01537')
-        self.assertEqual(parse_dimension_text_line(line), six.b('Dimensions: T(443) x \xce\xbb(1)'))
-        self.assertIsNone(parse_dimension_text_line(six.b('Dim: nothing')))
+        line = b'Metadata:\r\nDimensions: T(443) x \xce\xbb(1)\r\nCamera Name: Andor Zyla VSC-01537'
+        self.assertEqual(parse_dimension_text_line(line), b'Dimensions: T(443) x \xce\xbb(1)')
+        self.assertIsNone(parse_dimension_text_line(b'Dim: nothing'))
 
     def test_parse_z_levels(self):
         # smokescreen test to check if the fallback to z_coordinates is working
@@ -62,7 +61,7 @@ class TestRawMetadata(unittest.TestCase):
 
     def _assert_dicts_equal(self, parsed_dict, original_dict):
         for attribute in original_dict.keys():
-            parsed_key = six.b(attribute)
+            parsed_key = attribute
             self.assertIn(parsed_key, parsed_dict.keys())
 
             if isinstance(parsed_dict[parsed_key], dict):
diff --git a/tests/test_reader.py b/tests/test_reader.py
index d01032d08d1c961e2ca69b330c16ef39e4f28860..7a0d5c8c7c68932c0cc86f53b429df5a94a1c920 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -17,9 +17,9 @@ class TestReader(unittest.TestCase):
         self.assertTrue('nd2' in ND2Reader.class_exts())
 
     def cmp_two_readers(self, r1, r2):
-        attributes = r1.data['image_attributes']['SLxImageAttributes']
-        self.assertEqual(r2.metadata['width'], attributes['uiWidth'])
-        self.assertEqual(r2.metadata['height'], attributes['uiHeight'])
+        attributes = r1.data['image_attributes'][b'SLxImageAttributes']
+        self.assertEqual(r2.metadata['width'], attributes[b'uiWidth'])
+        self.assertEqual(r2.metadata['height'], attributes[b'uiHeight'])
 
         self.assertEqual(r2.metadata['width'], r2.sizes['x'])
         self.assertEqual(r2.metadata['height'], r2.sizes['y'])