diff --git a/AUTHORS b/AUTHORS
index 05077a678..d8ebb76b0 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -39,4 +39,5 @@ Contributors are:
 -Ben Thayer <ben _at_ benthayer.com>
 -Dries Kennes <admin _at_ dries007.net>
 -Pratik Anurag <panurag247365 _at_ gmail.com>
+-Harmon <harmon.public _at_ gmail.com>
 Portions derived from other open source works and are clearly marked.
diff --git a/git/cmd.py b/git/cmd.py
index 08e25af52..e87a3b800 100644
--- a/git/cmd.py
+++ b/git/cmd.py
@@ -21,12 +21,8 @@
 from textwrap import dedent
 
 from git.compat import (
-    string_types,
     defenc,
     force_bytes,
-    PY3,
-    # just to satisfy flake8 on py3
-    unicode,
     safe_decode,
     is_posix,
     is_win,
@@ -43,11 +39,6 @@
     stream_copy,
 )
 
-try:
-    PermissionError
-except NameError:  # Python < 3.3
-    PermissionError = OSError
-
 execute_kwargs = {'istream', 'with_extended_output',
                   'with_exceptions', 'as_process', 'stdout_as_string',
                   'output_stream', 'with_stdout', 'kill_after_timeout',
@@ -918,18 +909,12 @@ def transform_kwargs(self, split_single_char_options=True, **kwargs):
     @classmethod
     def __unpack_args(cls, arg_list):
         if not isinstance(arg_list, (list, tuple)):
-            # This is just required for unicode conversion, as subprocess can't handle it
-            # However, in any other case, passing strings (usually utf-8 encoded) is totally fine
-            if not PY3 and isinstance(arg_list, unicode):
-                return [arg_list.encode(defenc)]
             return [str(arg_list)]
 
         outlist = []
         for arg in arg_list:
             if isinstance(arg_list, (list, tuple)):
                 outlist.extend(cls.__unpack_args(arg))
-            elif not PY3 and isinstance(arg_list, unicode):
-                outlist.append(arg_list.encode(defenc))
             # END recursion
             else:
                 outlist.append(str(arg))
@@ -1047,7 +1032,7 @@ def _prepare_ref(self, ref):
         if isinstance(ref, bytes):
             # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text
             refstr = ref.decode('ascii')
-        elif not isinstance(ref, string_types):
+        elif not isinstance(ref, str):
             refstr = str(ref)               # could be ref-object
 
         if not refstr.endswith("\n"):
diff --git a/git/compat.py b/git/compat.py
index e88ca9b56..de8a238ba 100644
--- a/git/compat.py
+++ b/git/compat.py
@@ -10,66 +10,23 @@
 import locale
 import os
 import sys
-import codecs
 
 
-from gitdb.utils.compat import (
-    xrange,
-    MAXSIZE,    # @UnusedImport
-    izip,       # @UnusedImport
-)
 from gitdb.utils.encoding import (
-    string_types,    # @UnusedImport
-    text_type,       # @UnusedImport
     force_bytes,     # @UnusedImport
     force_text       # @UnusedImport
 )
 
 
-PY3 = sys.version_info[0] >= 3
 is_win = (os.name == 'nt')
 is_posix = (os.name == 'posix')
 is_darwin = (os.name == 'darwin')
-if hasattr(sys, 'getfilesystemencoding'):
-    defenc = sys.getfilesystemencoding()
-if defenc is None:
-    defenc = sys.getdefaultencoding()
-
-if PY3:
-    import io
-    FileType = io.IOBase
-
-    def byte_ord(b):
-        return b
-
-    def bchr(n):
-        return bytes([n])
-
-    def mviter(d):
-        return d.values()
-
-    range = xrange  # @ReservedAssignment
-    unicode = str
-    binary_type = bytes
-else:
-    FileType = file  # @UndefinedVariable on PY3
-    # usually, this is just ascii, which might not enough for our encoding needs
-    # Unless it's set specifically, we override it to be utf-8
-    if defenc == 'ascii':
-        defenc = 'utf-8'
-    byte_ord = ord
-    bchr = chr
-    unicode = unicode
-    binary_type = str
-    range = xrange  # @ReservedAssignment
-
-    def mviter(d):
-        return d.itervalues()
+defenc = sys.getfilesystemencoding()
 
 
 def safe_decode(s):
     """Safely decodes a binary string to unicode"""
-    if isinstance(s, unicode):
+    if isinstance(s, str):
         return s
     elif isinstance(s, bytes):
         return s.decode(defenc, 'surrogateescape')
@@ -79,7 +36,7 @@ def safe_decode(s):
 
 def safe_encode(s):
     """Safely decodes a binary string to unicode"""
-    if isinstance(s, unicode):
+    if isinstance(s, str):
         return s.encode(defenc)
     elif isinstance(s, bytes):
         return s
@@ -89,7 +46,7 @@ def safe_encode(s):
 
 def win_encode(s):
     """Encode unicodes for process arguments on Windows."""
-    if isinstance(s, unicode):
+    if isinstance(s, str):
         return s.encode(locale.getpreferredencoding(False))
     elif isinstance(s, bytes):
         return s
@@ -106,208 +63,5 @@ class metaclass(meta):
         def __new__(cls, name, nbases, d):
             if nbases is None:
                 return type.__new__(cls, name, (), d)
-            # There may be clients who rely on this attribute to be set to a reasonable value, which is why
-            # we set the __metaclass__ attribute explicitly
-            if not PY3 and '___metaclass__' not in d:
-                d['__metaclass__'] = meta
             return meta(name, bases, d)
     return metaclass(meta.__name__ + 'Helper', None, {})
-
-
-## From https://docs.python.org/3.3/howto/pyporting.html
-class UnicodeMixin(object):
-
-    """Mixin class to handle defining the proper __str__/__unicode__
-    methods in Python 2 or 3."""
-
-    if PY3:
-        def __str__(self):
-            return self.__unicode__()
-    else:  # Python 2
-        def __str__(self):
-            return self.__unicode__().encode(defenc)
-            
-            
-"""
-This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error
-handler of Python 3.
-Source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/misc
-"""
-
-# This code is released under the Python license and the BSD 2-clause license
-
-
-FS_ERRORS = 'surrogateescape'
-
-#     # -- Python 2/3 compatibility -------------------------------------
-#     FS_ERRORS = 'my_surrogateescape'
-
-def u(text):
-    if PY3:
-        return text
-    return text.decode('unicode_escape')
-
-def b(data):
-    if PY3:
-        return data.encode('latin1')
-    return data
-
-if PY3:
-    _unichr = chr
-    bytes_chr = lambda code: bytes((code,))
-else:
-    _unichr = unichr
-    bytes_chr = chr
-
-def surrogateescape_handler(exc):
-    """
-    Pure Python implementation of the PEP 383: the "surrogateescape" error
-    handler of Python 3. Undecodable bytes will be replaced by a Unicode
-    character U+DCxx on decoding, and these are translated into the
-    original bytes on encoding.
-    """
-    mystring = exc.object[exc.start:exc.end]
-
-    try:
-        if isinstance(exc, UnicodeDecodeError):
-            # mystring is a byte-string in this case
-            decoded = replace_surrogate_decode(mystring)
-        elif isinstance(exc, UnicodeEncodeError):
-            # In the case of u'\udcc3'.encode('ascii',
-            # 'this_surrogateescape_handler'), both Python 2.x and 3.x raise an
-            # exception anyway after this function is called, even though I think
-            # it's doing what it should. It seems that the strict encoder is called
-            # to encode the unicode string that this function returns ...
-            decoded = replace_surrogate_encode(mystring, exc)
-        else:
-            raise exc
-    except NotASurrogateError:
-        raise exc
-    return (decoded, exc.end)
-
-
-class NotASurrogateError(Exception):
-    pass
-
-
-def replace_surrogate_encode(mystring, exc):
-    """
-    Returns a (unicode) string, not the more logical bytes, because the codecs
-    register_error functionality expects this.
-    """
-    decoded = []
-    for ch in mystring:
-        # if PY3:
-        #     code = ch
-        # else:
-        code = ord(ch)
-
-        # The following magic comes from Py3.3's Python/codecs.c file:
-        if not 0xD800 <= code <= 0xDCFF:
-            # Not a surrogate. Fail with the original exception.
-            raise exc
-        # mybytes = [0xe0 | (code >> 12),
-        #            0x80 | ((code >> 6) & 0x3f),
-        #            0x80 | (code & 0x3f)]
-        # Is this a good idea?
-        if 0xDC00 <= code <= 0xDC7F:
-            decoded.append(_unichr(code - 0xDC00))
-        elif code <= 0xDCFF:
-            decoded.append(_unichr(code - 0xDC00))
-        else:
-            raise NotASurrogateError
-    return str().join(decoded)
-
-
-def replace_surrogate_decode(mybytes):
-    """
-    Returns a (unicode) string
-    """
-    decoded = []
-    for ch in mybytes:
-        # We may be parsing newbytes (in which case ch is an int) or a native
-        # str on Py2
-        if isinstance(ch, int):
-            code = ch
-        else:
-            code = ord(ch)
-        if 0x80 <= code <= 0xFF:
-            decoded.append(_unichr(0xDC00 + code))
-        elif code <= 0x7F:
-            decoded.append(_unichr(code))
-        else:
-            # # It may be a bad byte
-            # # Try swallowing it.
-            # continue
-            # print("RAISE!")
-            raise NotASurrogateError
-    return str().join(decoded)
-
-
-def encodefilename(fn):
-    if FS_ENCODING == 'ascii':
-        # ASCII encoder of Python 2 expects that the error handler returns a
-        # Unicode string encodable to ASCII, whereas our surrogateescape error
-        # handler has to return bytes in 0x80-0xFF range.
-        encoded = []
-        for index, ch in enumerate(fn):
-            code = ord(ch)
-            if code < 128:
-                ch = bytes_chr(code)
-            elif 0xDC80 <= code <= 0xDCFF:
-                ch = bytes_chr(code - 0xDC00)
-            else:
-                raise UnicodeEncodeError(FS_ENCODING,
-                    fn, index, index+1,
-                    'ordinal not in range(128)')
-            encoded.append(ch)
-        return bytes().join(encoded)
-    elif FS_ENCODING == 'utf-8':
-        # UTF-8 encoder of Python 2 encodes surrogates, so U+DC80-U+DCFF
-        # doesn't go through our error handler
-        encoded = []
-        for index, ch in enumerate(fn):
-            code = ord(ch)
-            if 0xD800 <= code <= 0xDFFF:
-                if 0xDC80 <= code <= 0xDCFF:
-                    ch = bytes_chr(code - 0xDC00)
-                    encoded.append(ch)
-                else:
-                    raise UnicodeEncodeError(
-                        FS_ENCODING,
-                        fn, index, index+1, 'surrogates not allowed')
-            else:
-                ch_utf8 = ch.encode('utf-8')
-                encoded.append(ch_utf8)
-        return bytes().join(encoded)
-    return fn.encode(FS_ENCODING, FS_ERRORS)
-
-def decodefilename(fn):
-    return fn.decode(FS_ENCODING, FS_ERRORS)
-
-FS_ENCODING = 'ascii'; fn = b('[abc\xff]'); encoded = u('[abc\udcff]')
-# FS_ENCODING = 'cp932'; fn = b('[abc\x81\x00]'); encoded = u('[abc\udc81\x00]')
-# FS_ENCODING = 'UTF-8'; fn = b('[abc\xff]'); encoded = u('[abc\udcff]')
-
-
-# normalize the filesystem encoding name.
-# For example, we expect "utf-8", not "UTF8".
-FS_ENCODING = codecs.lookup(FS_ENCODING).name
-
-
-def register_surrogateescape():
-    """
-    Registers the surrogateescape error handler on Python 2 (only)
-    """
-    if PY3:
-        return
-    try:
-        codecs.lookup_error(FS_ERRORS)
-    except LookupError:
-        codecs.register_error(FS_ERRORS, surrogateescape_handler)
-
-
-try:
-    b"100644 \x9f\0aaa".decode(defenc, "surrogateescape")
-except Exception:
-    register_surrogateescape()
diff --git a/git/config.py b/git/config.py
index 762069c75..43f854f21 100644
--- a/git/config.py
+++ b/git/config.py
@@ -9,30 +9,23 @@
 import abc
 from functools import wraps
 import inspect
+from io import IOBase
 import logging
 import os
 import re
 from collections import OrderedDict
 
 from git.compat import (
-    string_types,
-    FileType,
     defenc,
     force_text,
     with_metaclass,
-    PY3,
     is_win,
 )
 from git.util import LockFile
 
 import os.path as osp
 
-
-try:
-    import ConfigParser as cp
-except ImportError:
-    # PY3
-    import configparser as cp
+import configparser as cp
 
 
 __all__ = ('GitConfigParser', 'SectionConstraint')
@@ -303,7 +296,7 @@ def _acquire_lock(self):
                 # END single file check
 
                 file_or_files = self._file_or_files
-                if not isinstance(self._file_or_files, string_types):
+                if not isinstance(self._file_or_files, str):
                     file_or_files = self._file_or_files.name
                 # END get filename from handle/stream
                 # initialize lock base - we want to write
@@ -372,9 +365,7 @@ def string_decode(v):
                 v = v[:-1]
             # end cut trailing escapes to prevent decode error
 
-            if PY3:
-                return v.encode(defenc).decode('unicode_escape')
-            return v.decode('string_escape')
+            return v.encode(defenc).decode('unicode_escape')
             # end
         # end
 
@@ -581,7 +572,7 @@ def write(self):
         fp = self._file_or_files
 
         # we have a physical file on disk, so get a lock
-        is_file_lock = isinstance(fp, string_types + (FileType, ))
+        is_file_lock = isinstance(fp, (str, IOBase))
         if is_file_lock:
             self._lock._obtain_lock()
         if not hasattr(fp, "seek"):
@@ -673,7 +664,7 @@ def _string_to_value(self, valuestr):
         if vl == 'true':
             return True
 
-        if not isinstance(valuestr, string_types):
+        if not isinstance(valuestr, str):
             raise TypeError(
                 "Invalid value type: only int, long, float and str are allowed",
                 valuestr)
diff --git a/git/diff.py b/git/diff.py
index 7a06f3a1b..567e3e70c 100644
--- a/git/diff.py
+++ b/git/diff.py
@@ -6,13 +6,9 @@
 import re
 
 from git.cmd import handle_process_output
-from git.compat import (
-    defenc,
-    PY3
-)
+from git.compat import defenc
 from git.util import finalize_process, hex_to_bin
 
-from .compat import binary_type
 from .objects.blob import Blob
 from .objects.util import mode_str_to_int
 
@@ -28,10 +24,7 @@
 def _octal_repl(matchobj):
     value = matchobj.group(1)
     value = int(value, 8)
-    if PY3:
-        value = bytes(bytearray((value,)))
-    else:
-        value = chr(value)
+    value = bytes(bytearray((value,)))
     return value
 
 
@@ -268,8 +261,8 @@ def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
         self.a_mode = a_mode
         self.b_mode = b_mode
 
-        assert a_rawpath is None or isinstance(a_rawpath, binary_type)
-        assert b_rawpath is None or isinstance(b_rawpath, binary_type)
+        assert a_rawpath is None or isinstance(a_rawpath, bytes)
+        assert b_rawpath is None or isinstance(b_rawpath, bytes)
         self.a_rawpath = a_rawpath
         self.b_rawpath = b_rawpath
 
@@ -302,8 +295,8 @@ def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
         self.copied_file = copied_file
 
         # be clear and use None instead of empty strings
-        assert raw_rename_from is None or isinstance(raw_rename_from, binary_type)
-        assert raw_rename_to is None or isinstance(raw_rename_to, binary_type)
+        assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
+        assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
         self.raw_rename_from = raw_rename_from or None
         self.raw_rename_to = raw_rename_to or None
 
@@ -370,8 +363,6 @@ def __str__(self):
         # Python2 silliness: have to assure we convert our likely to be unicode object to a string with the
         # right encoding. Otherwise it tries to convert it using ascii, which may fail ungracefully
         res = h + msg
-        if not PY3:
-            res = res.encode(defenc)
         # end
         return res
 
diff --git a/git/exc.py b/git/exc.py
index 1c4d50056..f75ec5c99 100644
--- a/git/exc.py
+++ b/git/exc.py
@@ -6,7 +6,7 @@
 """ Module containing all exceptions thrown throughout the git package, """
 
 from gitdb.exc import *     # NOQA @UnusedWildImport skipcq: PYL-W0401, PYL-W0614
-from git.compat import UnicodeMixin, safe_decode, string_types
+from git.compat import safe_decode
 
 
 class GitError(Exception):
@@ -25,7 +25,7 @@ class NoSuchPathError(GitError, OSError):
     """ Thrown if a path could not be access by the system. """
 
 
-class CommandError(UnicodeMixin, GitError):
+class CommandError(GitError):
     """Base class for exceptions thrown at every stage of `Popen()` execution.
 
     :param command:
@@ -50,7 +50,7 @@ def __init__(self, command, status=None, stderr=None, stdout=None):
                     status = u'exit code(%s)' % int(status)
                 except (ValueError, TypeError):
                     s = safe_decode(str(status))
-                    status = u"'%s'" % s if isinstance(status, string_types) else s
+                    status = u"'%s'" % s if isinstance(status, str) else s
 
         self._cmd = safe_decode(command[0])
         self._cmdline = u' '.join(safe_decode(i) for i in command)
@@ -58,7 +58,7 @@ def __init__(self, command, status=None, stderr=None, stdout=None):
         self.stdout = stdout and u"\n  stdout: '%s'" % safe_decode(stdout) or ''
         self.stderr = stderr and u"\n  stderr: '%s'" % safe_decode(stderr) or ''
 
-    def __unicode__(self):
+    def __str__(self):
         return (self._msg + "\n  cmdline: %s%s%s") % (
             self._cmd, self._cause, self._cmdline, self.stdout, self.stderr)
 
diff --git a/git/index/base.py b/git/index/base.py
index b8c9d5e66..8ff0f9824 100644
--- a/git/index/base.py
+++ b/git/index/base.py
@@ -11,12 +11,8 @@
 import tempfile
 
 from git.compat import (
-    izip,
-    xrange,
-    string_types,
     force_bytes,
     defenc,
-    mviter,
 )
 from git.exc import (
     GitCommandError,
@@ -272,8 +268,8 @@ def new(cls, repo, *tree_sha):
 
         inst = cls(repo)
         # convert to entries dict
-        entries = dict(izip(((e.path, e.stage) for e in base_entries),
-                            (IndexEntry.from_base(e) for e in base_entries)))
+        entries = dict(zip(((e.path, e.stage) for e in base_entries),
+                           (IndexEntry.from_base(e) for e in base_entries)))
 
         inst.entries = entries
         return inst
@@ -442,7 +438,7 @@ def iter_blobs(self, predicate=lambda t: True):
             Function(t) returning True if tuple(stage, Blob) should be yielded by the
             iterator. A default filter, the BlobFilter, allows you to yield blobs
             only if they match a given list of paths. """
-        for entry in mviter(self.entries):
+        for entry in self.entries.values():
             blob = entry.to_blob(self.repo)
             blob.size = entry.size
             output = (entry.stage, blob)
@@ -467,7 +463,7 @@ def unmerged_blobs(self):
         for stage, blob in self.iter_blobs(is_unmerged_blob):
             path_map.setdefault(blob.path, []).append((stage, blob))
         # END for each unmerged blob
-        for l in mviter(path_map):
+        for l in path_map.values():
             l.sort()
         return path_map
 
@@ -574,7 +570,7 @@ def _preprocess_add_items(self, items):
             items = [items]
 
         for item in items:
-            if isinstance(item, string_types):
+            if isinstance(item, str):
                 paths.append(self._to_relative_path(item))
             elif isinstance(item, (Blob, Submodule)):
                 entries.append(BaseIndexEntry.from_blob(item))
@@ -811,7 +807,7 @@ def _items_to_rela_paths(self, items):
         for item in items:
             if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
                 paths.append(self._to_relative_path(item.path))
-            elif isinstance(item, string_types):
+            elif isinstance(item, str):
                 paths.append(self._to_relative_path(item))
             else:
                 raise TypeError("Invalid item type: %r" % item)
@@ -913,7 +909,7 @@ def move(self, items, skip_errors=False, **kwargs):
 
         # parse result - first 0:n/2 lines are 'checking ', the remaining ones
         # are the 'renaming' ones which we parse
-        for ln in xrange(int(len(mvlines) / 2), len(mvlines)):
+        for ln in range(int(len(mvlines) / 2), len(mvlines)):
             tokens = mvlines[ln].split(' to ')
             assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
 
@@ -1086,11 +1082,11 @@ def handle_stderr(proc, iter_checked_out_files):
             proc = self.repo.git.checkout_index(*args, **kwargs)
             proc.wait()
             fprogress(None, True, None)
-            rval_iter = (e.path for e in mviter(self.entries))
+            rval_iter = (e.path for e in self.entries.values())
             handle_stderr(proc, rval_iter)
             return rval_iter
         else:
-            if isinstance(paths, string_types):
+            if isinstance(paths, str):
                 paths = [paths]
 
             # make sure we have our entries loaded before we start checkout_index
@@ -1117,7 +1113,7 @@ def handle_stderr(proc, iter_checked_out_files):
                     folder = co_path
                     if not folder.endswith('/'):
                         folder += '/'
-                    for entry in mviter(self.entries):
+                    for entry in self.entries.values():
                         if entry.path.startswith(folder):
                             p = entry.path
                             self._write_path_to_stdin(proc, p, p, make_exc,
@@ -1227,7 +1223,7 @@ def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwar
         # index against anything but None is a reverse diff with the respective
         # item. Handle existing -R flags properly. Transform strings to the object
         # so that we can call diff on it
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             other = self.repo.rev_parse(other)
         # END object conversion
 
diff --git a/git/index/fun.py b/git/index/fun.py
index 5906a358b..c6337909a 100644
--- a/git/index/fun.py
+++ b/git/index/fun.py
@@ -15,12 +15,10 @@
 
 from git.cmd import PROC_CREATIONFLAGS, handle_process_output
 from git.compat import (
-    PY3,
     defenc,
     force_text,
     force_bytes,
     is_posix,
-    safe_encode,
     safe_decode,
 )
 from git.exc import (
@@ -73,7 +71,7 @@ def run_commit_hook(name, index, *args):
         return
 
     env = os.environ.copy()
-    env['GIT_INDEX_FILE'] = safe_decode(index.path) if PY3 else safe_encode(index.path)
+    env['GIT_INDEX_FILE'] = safe_decode(index.path)
     env['GIT_EDITOR'] = ':'
     try:
         cmd = subprocess.Popen([hp] + list(args),
diff --git a/git/objects/commit.py b/git/objects/commit.py
index f7201d90e..8a84dd69b 100644
--- a/git/objects/commit.py
+++ b/git/objects/commit.py
@@ -24,7 +24,6 @@
     parse_actor_and_date,
     from_timestamp,
 )
-from git.compat import text_type
 
 from time import (
     time,
@@ -436,7 +435,7 @@ def _serialize(self, stream):
         write(b"\n")
 
         # write plain bytes, be sure its encoded according to our encoding
-        if isinstance(self.message, text_type):
+        if isinstance(self.message, str):
             write(self.message.encode(self.encoding))
         else:
             write(self.message)
diff --git a/git/objects/fun.py b/git/objects/fun.py
index dc879fd2d..9b36712e1 100644
--- a/git/objects/fun.py
+++ b/git/objects/fun.py
@@ -1,12 +1,8 @@
 """Module with functions which are supposed to be as fast as possible"""
 from stat import S_ISDIR
 from git.compat import (
-    byte_ord,
     safe_decode,
-    defenc,
-    xrange,
-    text_type,
-    bchr
+    defenc
 )
 
 __all__ = ('tree_to_stream', 'tree_entries_from_data', 'traverse_trees_recursive',
@@ -22,12 +18,12 @@ def tree_to_stream(entries, write):
 
     for binsha, mode, name in entries:
         mode_str = b''
-        for i in xrange(6):
-            mode_str = bchr(((mode >> (i * 3)) & bit_mask) + ord_zero) + mode_str
+        for i in range(6):
+            mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str
         # END for each 8 octal value
 
         # git slices away the first octal if its zero
-        if byte_ord(mode_str[0]) == ord_zero:
+        if mode_str[0] == ord_zero:
             mode_str = mode_str[1:]
         # END save a byte
 
@@ -36,7 +32,7 @@ def tree_to_stream(entries, write):
         # hence we must convert to an utf8 string for it to work properly.
         # According to my tests, this is exactly what git does, that is it just
         # takes the input literally, which appears to be utf8 on linux.
-        if isinstance(name, text_type):
+        if isinstance(name, str):
             name = name.encode(defenc)
         write(b''.join((mode_str, b' ', name, b'\0', binsha)))
     # END for each item
@@ -57,10 +53,10 @@ def tree_entries_from_data(data):
         # read mode
         # Some git versions truncate the leading 0, some don't
         # The type will be extracted from the mode later
-        while byte_ord(data[i]) != space_ord:
+        while data[i] != space_ord:
             # move existing mode integer up one level being 3 bits
             # and add the actual ordinal value of the character
-            mode = (mode << 3) + (byte_ord(data[i]) - ord_zero)
+            mode = (mode << 3) + (data[i] - ord_zero)
             i += 1
         # END while reading mode
 
@@ -70,7 +66,7 @@ def tree_entries_from_data(data):
         # parse name, it is NULL separated
 
         ns = i
-        while byte_ord(data[i]) != 0:
+        while data[i] != 0:
             i += 1
         # END while not reached NULL
 
diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py
index 04ca02218..97973a575 100644
--- a/git/objects/submodule/base.py
+++ b/git/objects/submodule/base.py
@@ -9,7 +9,6 @@
 import git
 from git.cmd import Git
 from git.compat import (
-    string_types,
     defenc,
     is_win,
 )
@@ -110,7 +109,7 @@ def __init__(self, repo, binsha, mode=None, path=None, name=None, parent_commit=
         if url is not None:
             self._url = url
         if branch_path is not None:
-            assert isinstance(branch_path, string_types)
+            assert isinstance(branch_path, str)
             self._branch_path = branch_path
         if name is not None:
             self._name = name
diff --git a/git/objects/tree.py b/git/objects/tree.py
index d6134e308..469e5395d 100644
--- a/git/objects/tree.py
+++ b/git/objects/tree.py
@@ -11,17 +11,13 @@
 from .base import IndexObject
 from .blob import Blob
 from .submodule.base import Submodule
-from git.compat import string_types
 
 from .fun import (
     tree_entries_from_data,
     tree_to_stream
 )
 
-from git.compat import PY3
-
-if PY3:
-    cmp = lambda a, b: (a > b) - (a < b)
+cmp = lambda a, b: (a > b) - (a < b)
 
 __all__ = ("TreeModifier", "Tree")
 
@@ -293,7 +289,7 @@ def __getitem__(self, item):
             info = self._cache[item]
             return self._map_id_to_type[info[1] >> 12](self.repo, info[0], info[1], join_path(self.path, info[2]))
 
-        if isinstance(item, string_types):
+        if isinstance(item, str):
             # compatibility
             return self.join(item)
         # END index is basestring
diff --git a/git/refs/log.py b/git/refs/log.py
index 432232ac7..965b26c79 100644
--- a/git/refs/log.py
+++ b/git/refs/log.py
@@ -1,12 +1,7 @@
 import re
 import time
 
-from git.compat import (
-    PY3,
-    xrange,
-    string_types,
-    defenc
-)
+from git.compat import defenc
 from git.objects.util import (
     parse_date,
     Serializable,
@@ -36,12 +31,7 @@ class RefLogEntry(tuple):
 
     def __repr__(self):
         """Representation of ourselves in git reflog format"""
-        res = self.format()
-        if PY3:
-            return res
-        # repr must return a string, which it will auto-encode from unicode using the default encoding.
-        # This usually fails, so we encode ourselves
-        return res.encode(defenc)
+        return self.format()
 
     def format(self):
         """:return: a string suitable to be placed in a reflog file"""
@@ -192,7 +182,7 @@ def iter_entries(cls, stream):
         :param stream: file-like object containing the revlog in its native format
             or basestring instance pointing to a file to read"""
         new_entry = RefLogEntry.from_line
-        if isinstance(stream, string_types):
+        if isinstance(stream, str):
             stream = file_contents_ro_filepath(stream)
         # END handle stream type
         while True:
@@ -220,7 +210,7 @@ def entry_at(cls, filepath, index):
             if index < 0:
                 return RefLogEntry.from_line(fp.readlines()[index].strip())
             # read until index is reached
-            for i in xrange(index + 1):
+            for i in range(index + 1):
                 line = fp.readline()
                 if not line:
                     break
diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py
index 766037c15..4784197c3 100644
--- a/git/refs/symbolic.py
+++ b/git/refs/symbolic.py
@@ -1,9 +1,6 @@
 import os
 
-from git.compat import (
-    string_types,
-    defenc
-)
+from git.compat import defenc
 from git.objects import Object, Commit
 from git.util import (
     join_path,
@@ -300,7 +297,7 @@ def set_reference(self, ref, logmsg=None):
         elif isinstance(ref, Object):
             obj = ref
             write_value = ref.hexsha
-        elif isinstance(ref, string_types):
+        elif isinstance(ref, str):
             try:
                 obj = self.repo.rev_parse(ref + "^{}")    # optionally deref tags
                 write_value = obj.hexsha
diff --git a/git/repo/base.py b/git/repo/base.py
index 05c55eddb..bca44a72a 100644
--- a/git/repo/base.py
+++ b/git/repo/base.py
@@ -4,7 +4,6 @@
 # This module is part of GitPython and is released under
 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
 
-from builtins import str
 from collections import namedtuple
 import logging
 import os
@@ -16,11 +15,8 @@
     handle_process_output
 )
 from git.compat import (
-    text_type,
     defenc,
-    PY3,
     safe_decode,
-    range,
     is_win,
 )
 from git.config import GitConfigParser
@@ -479,7 +475,7 @@ def commit(self, rev=None):
         :return: ``git.Commit``"""
         if rev is None:
             return self.head.commit
-        return self.rev_parse(text_type(rev) + "^0")
+        return self.rev_parse(str(rev) + "^0")
 
     def iter_trees(self, *args, **kwargs):
         """:return: Iterator yielding Tree objects
@@ -501,7 +497,7 @@ def tree(self, rev=None):
             operations might have unexpected results."""
         if rev is None:
             return self.head.commit.tree
-        return self.rev_parse(text_type(rev) + "^{tree}")
+        return self.rev_parse(str(rev) + "^{tree}")
 
     def iter_commits(self, rev=None, paths='', **kwargs):
         """A list of Commit objects representing the history of a given ref/commit
@@ -693,11 +689,8 @@ def _get_untracked_files(self, *args, **kwargs):
             # Special characters are escaped
             if filename[0] == filename[-1] == '"':
                 filename = filename[1:-1]
-                if PY3:
-                    # WHATEVER ... it's a mess, but works for me
-                    filename = filename.encode('ascii').decode('unicode_escape').encode('latin1').decode(defenc)
-                else:
-                    filename = filename.decode('string_escape').decode(defenc)
+                # WHATEVER ... it's a mess, but works for me
+                filename = filename.encode('ascii').decode('unicode_escape').encode('latin1').decode(defenc)
             untracked_files.append(filename)
         finalize_process(proc)
         return untracked_files
diff --git a/git/repo/fun.py b/git/repo/fun.py
index 5a47fff37..784a70bf3 100644
--- a/git/repo/fun.py
+++ b/git/repo/fun.py
@@ -3,7 +3,6 @@
 import stat
 from string import digits
 
-from git.compat import xrange
 from git.exc import WorkTreeRepositoryUnsupported
 from git.objects import Object
 from git.refs import SymbolicReference
@@ -307,7 +306,7 @@ def rev_parse(repo, rev):
         try:
             if token == "~":
                 obj = to_commit(obj)
-                for _ in xrange(num):
+                for _ in range(num):
                     obj = obj.parents[0]
                 # END for each history item to walk
             elif token == "^":
diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py
index 1c06010f4..9418a9f80 100644
--- a/git/test/lib/helper.py
+++ b/git/test/lib/helper.py
@@ -11,13 +11,12 @@
 import io
 import logging
 import os
-import sys
 import tempfile
 import textwrap
 import time
 import unittest
 
-from git.compat import string_types, is_win
+from git.compat import is_win
 from git.util import rmtree, cwd
 import gitdb
 
@@ -117,7 +116,7 @@ def with_rw_repo(working_tree_ref, bare=False):
     To make working with relative paths easier, the cwd will be set to the working
     dir of the repository.
     """
-    assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
+    assert isinstance(working_tree_ref, str), "Decorator requires ref name for working tree checkout"
 
     def argument_passer(func):
         @wraps(func)
@@ -248,7 +247,7 @@ def case(self, rw_repo, rw_daemon_repo)
     """
     from git import Git, Remote  # To avoid circular deps.
 
-    assert isinstance(working_tree_ref, string_types), "Decorator requires ref name for working tree checkout"
+    assert isinstance(working_tree_ref, str), "Decorator requires ref name for working tree checkout"
 
     def argument_passer(func):
 
@@ -344,11 +343,6 @@ class TestBase(TestCase):
       of the project history ( to assure tests don't fail for others ).
     """
 
-    # On py3, unittest has assertRaisesRegex
-    # On py27, we use unittest, which names it differently:
-    if sys.version_info[0:2] == (2, 7):
-        assertRaisesRegex = TestCase.assertRaisesRegexp
-
     def _small_repo_url(self):
         """:return" a path to a small, clonable repository"""
         from git.cmd import Git
diff --git a/git/test/performance/test_commit.py b/git/test/performance/test_commit.py
index 322d3c9fc..659f320dc 100644
--- a/git/test/performance/test_commit.py
+++ b/git/test/performance/test_commit.py
@@ -11,7 +11,6 @@
 from .lib import TestBigRepoRW
 from git import Commit
 from gitdb import IStream
-from git.compat import xrange
 from git.test.test_commit import assert_commit_serialization
 
 
@@ -90,7 +89,7 @@ def test_commit_serialization(self):
 
         nc = 5000
         st = time()
-        for i in xrange(nc):
+        for i in range(nc):
             cm = Commit(rwrepo, Commit.NULL_BIN_SHA, hc.tree,
                         hc.author, hc.authored_date, hc.author_tz_offset,
                         hc.committer, hc.committed_date, hc.committer_tz_offset,
diff --git a/git/test/test_commit.py b/git/test/test_commit.py
index 96a03b20d..e41e80bbf 100644
--- a/git/test/test_commit.py
+++ b/git/test/test_commit.py
@@ -17,10 +17,6 @@
     Actor,
 )
 from git import Repo
-from git.compat import (
-    string_types,
-    text_type
-)
 from git.objects.util import tzoffset, utc
 from git.repo.fun import touch
 from git.test.lib import (
@@ -145,7 +141,7 @@ def test_unicode_actor(self):
         self.assertEqual(len(name), 9)
         special = Actor._from_string(u"%s <something@this.com>" % name)
         self.assertEqual(special.name, name)
-        assert isinstance(special.name, text_type)
+        assert isinstance(special.name, str)
 
     def test_traversal(self):
         start = self.rorepo.commit("a4d06724202afccd2b5c54f81bcf2bf26dea7fff")
@@ -276,7 +272,7 @@ def test_iter_parents(self):
 
     def test_name_rev(self):
         name_rev = self.rorepo.head.commit.name_rev
-        assert isinstance(name_rev, string_types)
+        assert isinstance(name_rev, str)
 
     @with_rw_repo('HEAD', bare=True)
     def test_serialization(self, rwrepo):
@@ -289,8 +285,8 @@ def test_serialization_unicode_support(self):
         # create a commit with unicode in the message, and the author's name
         # Verify its serialization and deserialization
         cmt = self.rorepo.commit('0.1.6')
-        assert isinstance(cmt.message, text_type)     # it automatically decodes it as such
-        assert isinstance(cmt.author.name, text_type)  # same here
+        assert isinstance(cmt.message, str)     # it automatically decodes it as such
+        assert isinstance(cmt.author.name, str)  # same here
 
         cmt.message = u"üäêèß"
         self.assertEqual(len(cmt.message), 5)
diff --git a/git/test/test_config.py b/git/test/test_config.py
index 83e510be4..ce7a2cde2 100644
--- a/git/test/test_config.py
+++ b/git/test/test_config.py
@@ -10,7 +10,6 @@
 from git import (
     GitConfigParser
 )
-from git.compat import string_types
 from git.config import _OMD, cp
 from git.test.lib import (
     TestCase,
@@ -157,7 +156,7 @@ def test_base(self):
                 num_options += 1
                 val = r_config.get(section, option)
                 val_typed = r_config.get_value(section, option)
-                assert isinstance(val_typed, (bool, int, float, ) + string_types)
+                assert isinstance(val_typed, (bool, int, float, str))
                 assert val
                 assert "\n" not in option
                 assert "\n" not in val
diff --git a/git/test/test_fun.py b/git/test/test_fun.py
index 314fb734a..612c4c5de 100644
--- a/git/test/test_fun.py
+++ b/git/test/test_fun.py
@@ -2,10 +2,9 @@
 from stat import S_IFDIR, S_IFREG, S_IFLNK
 from os import stat
 import os.path as osp
-from unittest import skipIf, SkipTest
+from unittest import SkipTest
 
 from git import Git
-from git.compat import PY3
 from git.index import IndexFile
 from git.index.fun import (
     aggressive_tree_merge
@@ -282,12 +281,6 @@ def test_linked_worktree_traversal(self, rw_dir):
         statbuf = stat(gitdir)
         assert_true(statbuf.st_mode & S_IFDIR)
 
-    @skipIf(PY3, 'odd types returned ... maybe figure it out one day')
-    def test_tree_entries_from_data_with_failing_name_decode_py2(self):
-        r = tree_entries_from_data(b'100644 \x9f\0aaa')
-        assert r == [('aaa', 33188, u'\udc9f')], r
-
-    @skipIf(not PY3, 'odd types returned ... maybe figure it out one day')
     def test_tree_entries_from_data_with_failing_name_decode_py3(self):
         r = tree_entries_from_data(b'100644 \x9f\0aaa')
         assert r == [(b'aaa', 33188, '\udc9f')], r
diff --git a/git/test/test_git.py b/git/test/test_git.py
index 357d9edb3..e6bc19d1d 100644
--- a/git/test/test_git.py
+++ b/git/test/test_git.py
@@ -17,7 +17,7 @@
     Repo,
     cmd
 )
-from git.compat import PY3, is_darwin
+from git.compat import is_darwin
 from git.test.lib import (
     TestBase,
     patch,
@@ -61,18 +61,12 @@ def test_call_process_calls_execute(self, git):
 
     def test_call_unpack_args_unicode(self):
         args = Git._Git__unpack_args(u'Unicode€™')
-        if PY3:
-            mangled_value = 'Unicode\u20ac\u2122'
-        else:
-            mangled_value = 'Unicode\xe2\x82\xac\xe2\x84\xa2'
+        mangled_value = 'Unicode\u20ac\u2122'
         assert_equal(args, [mangled_value])
 
     def test_call_unpack_args(self):
         args = Git._Git__unpack_args(['git', 'log', '--', u'Unicode€™'])
-        if PY3:
-            mangled_value = 'Unicode\u20ac\u2122'
-        else:
-            mangled_value = 'Unicode\xe2\x82\xac\xe2\x84\xa2'
+        mangled_value = 'Unicode\u20ac\u2122'
         assert_equal(args, ['git', 'log', '--', mangled_value])
 
     @raises(GitCommandError)
diff --git a/git/test/test_index.py b/git/test/test_index.py
index 9b8c957e2..0a2309f93 100644
--- a/git/test/test_index.py
+++ b/git/test/test_index.py
@@ -25,7 +25,7 @@
     GitCommandError,
     CheckoutError,
 )
-from git.compat import string_types, is_win, PY3
+from git.compat import is_win
 from git.exc import (
     HookExecutionError,
     InvalidGitRepositoryError
@@ -388,7 +388,7 @@ def test_index_file_diffing(self, rw_repo):
             self.assertEqual(len(e.failed_files), 1)
             self.assertEqual(e.failed_files[0], osp.basename(test_file))
             self.assertEqual(len(e.failed_files), len(e.failed_reasons))
-            self.assertIsInstance(e.failed_reasons[0], string_types)
+            self.assertIsInstance(e.failed_reasons[0], str)
             self.assertEqual(len(e.valid_files), 0)
             with open(test_file, 'rb') as fd:
                 s = fd.read()
@@ -821,10 +821,6 @@ def test_index_bare_add(self, rw_bare_repo):
             asserted = True
         assert asserted, "Adding using a filename is not correctly asserted."
 
-    @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and not PY3, r"""
-        FIXME:  File "C:\projects\gitpython\git\util.py", line 125, in to_native_path_linux
-        return path.replace('\\', '/')
-        UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)""")
     @with_rw_directory
     def test_add_utf8P_path(self, rw_dir):
         # NOTE: fp is not a Unicode object in python 2 (which is the source of the problem)
diff --git a/git/test/test_remote.py b/git/test/test_remote.py
index 3ef474727..2194daecb 100644
--- a/git/test/test_remote.py
+++ b/git/test/test_remote.py
@@ -22,7 +22,6 @@
     GitCommandError
 )
 from git.cmd import Git
-from git.compat import string_types
 from git.test.lib import (
     TestBase,
     with_rw_repo,
@@ -116,7 +115,7 @@ def _do_test_fetch_result(self, results, remote):
         self.assertGreater(len(results), 0)
         self.assertIsInstance(results[0], FetchInfo)
         for info in results:
-            self.assertIsInstance(info.note, string_types)
+            self.assertIsInstance(info.note, str)
             if isinstance(info.ref, Reference):
                 self.assertTrue(info.flags)
             # END reference type flags handling
@@ -133,7 +132,7 @@ def _do_test_push_result(self, results, remote):
         self.assertIsInstance(results[0], PushInfo)
         for info in results:
             self.assertTrue(info.flags)
-            self.assertIsInstance(info.summary, string_types)
+            self.assertIsInstance(info.summary, str)
             if info.old_commit is not None:
                 self.assertIsInstance(info.old_commit, Commit)
             if info.flags & info.ERROR:
diff --git a/git/test/test_repo.py b/git/test/test_repo.py
index ef28c74ec..18b6f11ef 100644
--- a/git/test/test_repo.py
+++ b/git/test/test_repo.py
@@ -36,12 +36,6 @@
     BadName,
     GitCommandError
 )
-from git.compat import (
-    PY3,
-    is_win,
-    string_types,
-    win_encode,
-)
 from git.exc import (
     BadObject,
 )
@@ -60,7 +54,6 @@
 from git.test.lib import with_rw_directory
 from git.util import join_path_native, rmtree, rmfile, bin_to_hex
 
-import functools as fnt
 import os.path as osp
 
 
@@ -442,7 +435,7 @@ def test_should_display_blame_information(self, git):
         # test the 'lines per commit' entries
         tlist = b[0][1]
         assert_true(tlist)
-        assert_true(isinstance(tlist[0], string_types))
+        assert_true(isinstance(tlist[0], str))
         assert_true(len(tlist) < sum(len(t) for t in tlist))               # test for single-char bug
 
         # BINARY BLAME
@@ -503,10 +496,7 @@ def test_blame_complex_revision(self, git):
                 """)
     @with_rw_repo('HEAD', bare=False)
     def test_untracked_files(self, rwrepo):
-        for run, (repo_add, is_invoking_git) in enumerate((
-                (rwrepo.index.add, False),
-                (rwrepo.git.add, True),
-        )):
+        for run, repo_add in enumerate((rwrepo.index.add, rwrepo.git.add)):
             base = rwrepo.working_tree_dir
             files = (join_path_native(base, u"%i_test _myfile" % run),
                      join_path_native(base, "%i_test_other_file" % run),
@@ -526,11 +516,6 @@ def test_untracked_files(self, rwrepo):
                 num_test_untracked += join_path_native(base, utfile) in files
             self.assertEqual(len(files), num_test_untracked)
 
-            if is_win and not PY3 and is_invoking_git:
-                ## On Windows, shell needed when passing unicode cmd-args.
-                #
-                repo_add = fnt.partial(repo_add, shell=True)
-                untracked_files = [win_encode(f) for f in untracked_files]
             repo_add(untracked_files)
             self.assertEqual(len(rwrepo.untracked_files), (num_recently_untracked - len(files)))
         # end for each run
diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py
index 94028d834..0d306edc3 100644
--- a/git/test/test_submodule.py
+++ b/git/test/test_submodule.py
@@ -8,7 +8,7 @@
 
 import git
 from git.cmd import Git
-from git.compat import string_types, is_win
+from git.compat import is_win
 from git.exc import (
     InvalidGitRepositoryError,
     RepositoryDirtyError
@@ -79,7 +79,7 @@ def _do_base_tests(self, rwrepo):
         self.failUnlessRaises(InvalidGitRepositoryError, getattr, sm, 'branch')
 
         # branch_path works, as its just a string
-        assert isinstance(sm.branch_path, string_types)
+        assert isinstance(sm.branch_path, str)
 
         # some commits earlier we still have a submodule, but its at a different commit
         smold = next(Submodule.iter_items(rwrepo, self.k_subm_changed))
diff --git a/git/test/test_util.py b/git/test/test_util.py
index a4d9d7adc..5faeeacb3 100644
--- a/git/test/test_util.py
+++ b/git/test/test_util.py
@@ -13,7 +13,7 @@
 import ddt
 
 from git.cmd import dashify
-from git.compat import string_types, is_win
+from git.compat import is_win
 from git.objects.util import (
     altz_to_utctz_str,
     utctz_to_altz,
@@ -187,7 +187,7 @@ def assert_rval(rval, veri_time, offset=0):
 
             # now that we are here, test our conversion functions as well
             utctz = altz_to_utctz_str(offset)
-            self.assertIsInstance(utctz, string_types)
+            self.assertIsInstance(utctz, str)
             self.assertEqual(utctz_to_altz(verify_utctz(utctz)), offset)
         # END assert rval utility
 
diff --git a/git/util.py b/git/util.py
index 974657e6f..cf0ba8d5e 100644
--- a/git/util.py
+++ b/git/util.py
@@ -13,6 +13,7 @@
 import re
 import shutil
 import stat
+from sys import maxsize
 import time
 from unittest import SkipTest
 
@@ -31,11 +32,6 @@
 from git.compat import is_win
 import os.path as osp
 
-from .compat import (
-    MAXSIZE,
-    defenc,
-    PY3
-)
 from .exc import InvalidGitRepositoryError
 
 
@@ -592,9 +588,6 @@ def _main_actor(cls, env_name, env_email, config_reader=None):
                                           ('email', env_email, cls.conf_email, default_email)):
             try:
                 val = os.environ[evar]
-                if not PY3:
-                    val = val.decode(defenc)
-                # end assure we don't get 'invalid strings'
                 setattr(actor, attr, val)
             except KeyError:
                 if config_reader is not None:
@@ -787,7 +780,7 @@ class BlockingLockFile(LockFile):
         can never be obtained."""
     __slots__ = ("_check_interval", "_max_block_time")
 
-    def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=MAXSIZE):
+    def __init__(self, file_path, check_interval_s=0.3, max_block_time_s=maxsize):
         """Configure the instance
 
         :param check_interval_s:
@@ -940,8 +933,3 @@ def iter_items(cls, repo, *args, **kwargs):
 class NullHandler(logging.Handler):
     def emit(self, record):
         pass
-
-
-# In Python 2.6, there is no NullHandler yet. Let's monkey-patch it for a workaround.
-if not hasattr(logging, 'NullHandler'):
-    logging.NullHandler = NullHandler
diff --git a/requirements.txt b/requirements.txt
index 63d5ddfe7..5eb87ac69 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-gitdb2 (>=2.0.0)
+gitdb2>=2.0.0
diff --git a/setup.py b/setup.py
index 0497792de..488d348ea 100755
--- a/setup.py
+++ b/setup.py
@@ -78,7 +78,7 @@ def _stamp_version(filename):
     py_modules=['git.' + f[:-3] for f in os.listdir('./git') if f.endswith('.py')],
     package_data={'git.test': ['fixtures/*']},
     package_dir={'git': 'git'},
-    python_requires='>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
+    python_requires='>=3.4',
     install_requires=requirements,
     tests_require=requirements + test_requirements,
     zip_safe=False,