Drop Python 2.7 (#936)

* Drop Python 2.7

The tooling situation around Python 2 has deteriorate to such a degree that
upholding compatibility is not tenable anymore for a volunteer-run project.

Signed-off-by: Hynek Schlawack <hs@ox.cx>

* Add newsfragment

* Run Python 3.5 under coverage to make up for Python 2.7

* Wait for py35 in parallel

* Remove fullmatch kludge

* Remove Python 2-specific code

* Revert empty slot test

Also disable pyupgrade on that file.

Signed-off-by: Hynek Schlawack <hs@ox.cx>

* We DO run under 3.5

* Remove __qualname__ workarounds

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update changelog.d/936.breaking.rst

Co-authored-by: Tin Tvrtković <tinchester@gmail.com>

* Compare methods using is

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tin Tvrtković <tinchester@gmail.com>
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6c9e25c..e081f9f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -71,7 +71,7 @@
 
 - To run the test suite, all you need is a recent [*tox*].
   It will ensure the test suite runs with all dependencies against all Python versions just as it will in our [CI].
-  If you lack some Python versions, you can can always limit the environments like `tox -e py27,py38`, or make it a non-failure using `tox --skip-missing-interpreters`.
+  If you lack some Python versions, you can can always limit the environments like `tox -e py38,py39`, or make it a non-failure using `tox --skip-missing-interpreters`.
 
   In that case you should look into [*asdf*](https://asdf-vm.com) or [*pyenv*](https://github.com/pyenv/pyenv), which make it very easy to install many different Python versions in parallel.
 - Write [good test docstrings](https://jml.io/pages/test-docstrings.html).
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a68af6b..f62e0a6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,7 +23,7 @@
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-2.7", "pypy-3.7", "pypy-3.8"]
+        python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"]
 
     steps:
       - uses: actions/checkout@v3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b5c6509..688bdf0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,12 +8,20 @@
     hooks:
       - id: black
 
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v2.31.1
+    hooks:
+      - id: pyupgrade
+        args: [--py3-plus, --keep-percent-format]
+        exclude: "tests/test_slots.py"
+
   - repo: https://github.com/PyCQA/isort
     rev: 5.10.1
     hooks:
       - id: isort
         additional_dependencies: [toml]
         files: \.py$
+        language_version: python3.10  # needed for match
 
   - repo: https://github.com/PyCQA/flake8
     rev: 4.0.1
diff --git a/README.rst b/README.rst
index c2e5cf3..cfb7a92 100644
--- a/README.rst
+++ b/README.rst
@@ -117,7 +117,8 @@
 its documentation lives at `Read the Docs <https://www.attrs.org/>`_,
 the code on `GitHub <https://github.com/python-attrs/attrs>`_,
 and the latest release on `PyPI <https://pypi.org/project/attrs/>`_.
-It’s rigorously tested on Python 2.7, 3.5+, and PyPy.
+It’s rigorously tested on Python 3.5+ and PyPy.
+The last version with Python 2.7 support is `21.4.0 <https://pypi.org/project/attrs/21.4.0/>`_.
 
 We collect information on **third-party extensions** in our `wiki <https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs>`_.
 Feel free to browse and add your own!
diff --git a/changelog.d/936.breaking.rst b/changelog.d/936.breaking.rst
new file mode 100644
index 0000000..1a30d12
--- /dev/null
+++ b/changelog.d/936.breaking.rst
@@ -0,0 +1,6 @@
+Python 2.7 is not supported anymore.
+
+Dealing with Python 2.7 tooling has become too difficult for a volunteer-run project.
+
+We have supported Python 2 more than 2 years after it was officially discontinued and feel that we have paid our dues.
+All version up to 21.4.0 from December 2021 remain fully functional, of course.
diff --git a/conftest.py b/conftest.py
index 0d539a1..33cc6a6 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 from hypothesis import HealthCheck, settings
 
diff --git a/docs/index.rst b/docs/index.rst
index ff65a67..de82a2d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -89,7 +89,6 @@
    :maxdepth: 1
 
    license
-   python-2
    changelog
 
 
diff --git a/docs/python-2.rst b/docs/python-2.rst
deleted file mode 100644
index 863c6b0..0000000
--- a/docs/python-2.rst
+++ /dev/null
@@ -1,25 +0,0 @@
-Python 2 Statement
-==================
-
-While ``attrs`` has always been a Python 3-first package, we the maintainers are aware that Python 2 has not magically disappeared in 2020.
-We are also aware that ``attrs`` is an important building block in many people's systems and livelihoods.
-
-As such, we do **not** have any immediate plans to drop Python 2 support in ``attrs``.
-We intend to support is as long as it will be technically feasible for us.
-
-Feasibility in this case means:
-
-1. Possibility to run the tests on our development computers,
-2. and **free** CI options.
-
-This can mean that we will have to run our tests on PyPy, whose maintainers have unequivocally declared that they do not intend to stop the development and maintenance of their Python 2-compatible line at all.
-And this can mean that at some point, a sponsor will have to step up and pay for bespoke CI setups.
-
-**However**: there is no promise of new features coming to ``attrs`` running under Python 2.
-It is up to our discretion alone, to decide whether the introduced complexity or awkwardness are worth it, or whether we choose to make a feature available on modern platforms only.
-
-
-Summary
--------
-
-We will do our best to support existing users, but nobody is entitled to the latest and greatest features on a platform that is officially end of life.
diff --git a/docs/why.rst b/docs/why.rst
index 8489959..db6282d 100644
--- a/docs/why.rst
+++ b/docs/why.rst
@@ -20,7 +20,7 @@
   There is a long list of features that were sacrificed for the sake of simplicity and while the most obvious ones are validators, converters, :ref:`equality customization <custom-comparison>`, or :doc:`extensibility <extending>` in general, it permeates throughout all APIs.
 
   On the other hand, Data Classes currently do not offer any significant feature that ``attrs`` doesn't already have.
-- ``attrs`` supports all mainstream Python versions, including CPython 2.7 and PyPy.
+- ``attrs`` supports all mainstream Python versions including PyPy.
 - ``attrs`` doesn't force type annotations on you if you don't like them.
 - But since it **also** supports typing, it's the best way to embrace type hints *gradually*, too.
 - While Data Classes are implementing features from ``attrs`` every now and then, their presence is dependent on the Python version, not the package version.
diff --git a/setup.py b/setup.py
index 0b8caff..59ebc60 100644
--- a/setup.py
+++ b/setup.py
@@ -32,8 +32,6 @@
     "License :: OSI Approved :: MIT License",
     "Operating System :: OS Independent",
     "Programming Language :: Python",
-    "Programming Language :: Python :: 2",
-    "Programming Language :: Python :: 2.7",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.5",
     "Programming Language :: Python :: 3.6",
@@ -56,7 +54,6 @@
         "hypothesis",
         "pympler",
         "pytest>=4.3.0",  # 4.3.0 dropped last use of `convert`
-        "six",
     ],
 }
 if (
@@ -143,7 +140,7 @@
         long_description_content_type="text/x-rst",
         packages=PACKAGES,
         package_dir={"": "src"},
-        python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+        python_requires=">=3.5",
         zip_safe=False,
         classifiers=CLASSIFIERS,
         install_requires=INSTALL_REQUIRES,
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index 65c94cd..b475913 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import sys
 
diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py
index 6cffa4d..0060222 100644
--- a/src/attr/_cmp.py
+++ b/src/attr/_cmp.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import functools
 
diff --git a/src/attr/_compat.py b/src/attr/_compat.py
index 7f11733..b0d6908 100644
--- a/src/attr/_compat.py
+++ b/src/attr/_compat.py
@@ -1,15 +1,16 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
+import inspect
 import platform
 import sys
 import threading
 import types
 import warnings
 
+from collections.abc import Mapping, Sequence  # noqa
 
-PY2 = sys.version_info[0] == 2
+
 PYPY = platform.python_implementation() == "PyPy"
 PY36 = sys.version_info[:2] >= (3, 6)
 HAS_F_STRINGS = PY36
@@ -24,180 +25,76 @@
     ordered_dict = OrderedDict
 
 
-if PY2:
-    from collections import Mapping, Sequence
+def just_warn(*args, **kw):
+    """
+    We only warn on Python 3 because we are not aware of any concrete
+    consequences of not setting the cell on Python 2.
+    """
+    warnings.warn(
+        "Running interpreter doesn't sufficiently support code object "
+        "introspection.  Some features like bare super() or accessing "
+        "__class__ will not work with slotted classes.",
+        RuntimeWarning,
+        stacklevel=2,
+    )
 
-    from UserDict import IterableUserDict
 
-    # We 'bundle' isclass instead of using inspect as importing inspect is
-    # fairly expensive (order of 10-15 ms for a modern machine in 2016)
-    def isclass(klass):
-        return isinstance(klass, (type, types.ClassType))
+def isclass(klass):
+    return isinstance(klass, type)
 
-    def new_class(name, bases, kwds, exec_body):
+
+TYPE = "class"
+
+
+def iteritems(d):
+    return d.items()
+
+
+new_class = types.new_class
+
+
+def metadata_proxy(d):
+    return types.MappingProxyType(dict(d))
+
+
+class _AnnotationExtractor:
+    """
+    Extract type annotations from a callable, returning None whenever there
+    is none.
+    """
+
+    __slots__ = ["sig"]
+
+    def __init__(self, callable):
+        try:
+            self.sig = inspect.signature(callable)
+        except (ValueError, TypeError):  # inspect failed
+            self.sig = None
+
+    def get_first_param_type(self):
         """
-        A minimal stub of types.new_class that we need for make_class.
+        Return the type annotation of the first argument if it's not empty.
         """
-        ns = {}
-        exec_body(ns)
-
-        return type(name, bases, ns)
-
-    # TYPE is used in exceptions, repr(int) is different on Python 2 and 3.
-    TYPE = "type"
-
-    def iteritems(d):
-        return d.iteritems()
-
-    # Python 2 is bereft of a read-only dict proxy, so we make one!
-    class ReadOnlyDict(IterableUserDict):
-        """
-        Best-effort read-only dict wrapper.
-        """
-
-        def __setitem__(self, key, val):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise TypeError(
-                "'mappingproxy' object does not support item assignment"
-            )
-
-        def update(self, _):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise AttributeError(
-                "'mappingproxy' object has no attribute 'update'"
-            )
-
-        def __delitem__(self, _):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise TypeError(
-                "'mappingproxy' object does not support item deletion"
-            )
-
-        def clear(self):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise AttributeError(
-                "'mappingproxy' object has no attribute 'clear'"
-            )
-
-        def pop(self, key, default=None):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise AttributeError(
-                "'mappingproxy' object has no attribute 'pop'"
-            )
-
-        def popitem(self):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise AttributeError(
-                "'mappingproxy' object has no attribute 'popitem'"
-            )
-
-        def setdefault(self, key, default=None):
-            # We gently pretend we're a Python 3 mappingproxy.
-            raise AttributeError(
-                "'mappingproxy' object has no attribute 'setdefault'"
-            )
-
-        def __repr__(self):
-            # Override to be identical to the Python 3 version.
-            return "mappingproxy(" + repr(self.data) + ")"
-
-    def metadata_proxy(d):
-        res = ReadOnlyDict()
-        res.data.update(d)  # We blocked update, so we have to do it like this.
-        return res
-
-    def just_warn(*args, **kw):  # pragma: no cover
-        """
-        We only warn on Python 3 because we are not aware of any concrete
-        consequences of not setting the cell on Python 2.
-        """
-
-    class _AnnotationExtractor:
-        """
-        Always return None, allows to keep ``if PY2``s from code.
-        """
-
-        __slots__ = ["sig"]
-        sig = None
-
-        def __init__(self, callable):
-            pass
-
-        def get_first_param_type(self):
+        if not self.sig:
             return None
 
-        def get_return_type(self):
-            return None
+        params = list(self.sig.parameters.values())
+        if params and params[0].annotation is not inspect.Parameter.empty:
+            return params[0].annotation
 
-else:  # Python 3 and later.
-    import inspect
+        return None
 
-    from collections.abc import Mapping, Sequence  # noqa
-
-    def just_warn(*args, **kw):
+    def get_return_type(self):
         """
-        We only warn on Python 3 because we are not aware of any concrete
-        consequences of not setting the cell on Python 2.
+        Return the return type if it's not empty.
         """
-        warnings.warn(
-            "Running interpreter doesn't sufficiently support code object "
-            "introspection.  Some features like bare super() or accessing "
-            "__class__ will not work with slotted classes.",
-            RuntimeWarning,
-            stacklevel=2,
-        )
+        if (
+            self.sig
+            and self.sig.return_annotation is not inspect.Signature.empty
+        ):
+            return self.sig.return_annotation
 
-    def isclass(klass):
-        return isinstance(klass, type)
-
-    TYPE = "class"
-
-    def iteritems(d):
-        return d.items()
-
-    new_class = types.new_class
-
-    def metadata_proxy(d):
-        return types.MappingProxyType(dict(d))
-
-    class _AnnotationExtractor:
-        """
-        Extract type annotations from a callable, returning None whenever there
-        is none.
-        """
-
-        __slots__ = ["sig"]
-
-        def __init__(self, callable):
-            try:
-                self.sig = inspect.signature(callable)
-            except (ValueError, TypeError):  # inspect failed
-                self.sig = None
-
-        def get_first_param_type(self):
-            """
-            Return the type annotation of the first argument if it's not empty.
-            """
-            if not self.sig:
-                return None
-
-            params = list(self.sig.parameters.values())
-            if params and params[0].annotation is not inspect.Parameter.empty:
-                return params[0].annotation
-
-            return None
-
-        def get_return_type(self):
-            """
-            Return the return type if it's not empty.
-            """
-            if (
-                self.sig
-                and self.sig.return_annotation is not inspect.Signature.empty
-            ):
-                return self.sig.return_annotation
-
-            return None
+        return None
 
 
 def make_set_closure_cell():
@@ -229,10 +126,7 @@
     try:
         # Extract the code object and make sure our assumptions about
         # the closure behavior are correct.
-        if PY2:
-            co = set_first_cellvar_to.func_code
-        else:
-            co = set_first_cellvar_to.__code__
+        co = set_first_cellvar_to.__code__
         if co.co_cellvars != ("x",) or co.co_freevars != ():
             raise AssertionError  # pragma: no cover
 
@@ -247,8 +141,7 @@
             )
         else:
             args = [co.co_argcount]
-            if not PY2:
-                args.append(co.co_kwonlyargcount)
+            args.append(co.co_kwonlyargcount)
             args.extend(
                 [
                     co.co_nlocals,
@@ -288,10 +181,7 @@
 
             return func
 
-        if PY2:
-            cell = make_func_with_cell().func_closure[0]
-        else:
-            cell = make_func_with_cell().__closure__[0]
+        cell = make_func_with_cell().__closure__[0]
         set_closure_cell(cell, 100)
         if cell.cell_contents != 100:
             raise AssertionError  # pragma: no cover
diff --git a/src/attr/_config.py b/src/attr/_config.py
index fc9be29..96d4200 100644
--- a/src/attr/_config.py
+++ b/src/attr/_config.py
@@ -1,7 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
-
 
 __all__ = ["set_run_validators", "get_run_validators"]
 
diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
index 4c90085..69a3cf6 100644
--- a/src/attr/_funcs.py
+++ b/src/attr/_funcs.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import copy
 
diff --git a/src/attr/_make.py b/src/attr/_make.py
index c1b6f99..0b974f3 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -1,10 +1,10 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import copy
 import linecache
 import sys
+import typing
 import warnings
 
 from operator import itemgetter
@@ -14,7 +14,6 @@
 from . import _compat, _config, setters
 from ._compat import (
     HAS_F_STRINGS,
-    PY2,
     PY310,
     PYPY,
     _AnnotationExtractor,
@@ -29,15 +28,10 @@
     DefaultAlreadySetError,
     FrozenInstanceError,
     NotAnAttrsClassError,
-    PythonTooOldError,
     UnannotatedAttributeError,
 )
 
 
-if not PY2:
-    import typing
-
-
 # This is used at least twice, so cache it here.
 _obj_setattr = object.__setattr__
 _init_converter_pat = "__attr_converter_%s"
@@ -64,7 +58,7 @@
 _ng_default_on_setattr = setters.pipe(setters.convert, setters.validate)
 
 
-class _Nothing(object):
+class _Nothing:
     """
     Sentinel class to indicate the lack of a value when ``None`` is ambiguous.
 
@@ -77,7 +71,7 @@
 
     def __new__(cls):
         if _Nothing._singleton is None:
-            _Nothing._singleton = super(_Nothing, cls).__new__(cls)
+            _Nothing._singleton = super().__new__(cls)
         return _Nothing._singleton
 
     def __repr__(self):
@@ -86,9 +80,6 @@
     def __bool__(self):
         return False
 
-    def __len__(self):
-        return 0  # __bool__ for Python 2
-
 
 NOTHING = _Nothing()
 """
@@ -108,17 +99,8 @@
     See GH #613 for more details.
     """
 
-    if PY2:
-        # For some reason `type(None)` isn't callable in Python 2, but we don't
-        # actually need a constructor for None objects, we just need any
-        # available function that returns None.
-        def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)):
-            return _none_constructor, _args
-
-    else:
-
-        def __reduce__(self, _none_constructor=type(None), _args=()):
-            return _none_constructor, _args
+    def __reduce__(self, _none_constructor=type(None), _args=()):
+        return _none_constructor, _args
 
 
 def attrib(
@@ -647,7 +629,7 @@
     raise FrozenInstanceError()
 
 
-class _ClassBuilder(object):
+class _ClassBuilder:
     """
     Iteratively build *one* class.
     """
@@ -701,7 +683,7 @@
         self._cls = cls
         self._cls_dict = dict(cls.__dict__) if slots else {}
         self._attrs = attrs
-        self._base_names = set(a.name for a in base_attrs)
+        self._base_names = {a.name for a in base_attrs}
         self._base_attr_map = base_map
         self._attr_names = tuple(a.name for a in attrs)
         self._slots = slots
@@ -879,9 +861,7 @@
             slot_names.append(_hash_cache_field)
         cd["__slots__"] = tuple(slot_names)
 
-        qualname = getattr(self._cls, "__qualname__", None)
-        if qualname is not None:
-            cd["__qualname__"] = qualname
+        cd["__qualname__"] = self._cls.__qualname__
 
         # Create new class based on old class and our methods.
         cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd)
@@ -1197,8 +1177,6 @@
     whose presence signal that the user has implemented it themselves.
 
     Return *default* if no reason for either for or against is found.
-
-    auto_detect must be False on Python 2.
     """
     if flag is True or flag is False:
         return flag
@@ -1495,11 +1473,6 @@
     .. versionchanged:: 21.1.0 *cmp* undeprecated
     .. versionadded:: 21.3.0 *match_args*
     """
-    if auto_detect and PY2:
-        raise PythonTooOldError(
-            "auto_detect only works on Python 3 and later."
-        )
-
     eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None)
     hash_ = hash  # work around the lack of nonlocal
 
@@ -1507,10 +1480,6 @@
         on_setattr = setters.pipe(*on_setattr)
 
     def wrap(cls):
-
-        if getattr(cls, "__class__", None) is None:
-            raise TypeError("attrs only works with new-style classes.")
-
         is_frozen = frozen or _has_frozen_base_class(cls)
         is_exc = auto_exc is True and issubclass(cls, BaseException)
         has_own_setattr = auto_detect and _has_own_attribute(
@@ -1634,34 +1603,19 @@
 """
 
 
-if PY2:
-
-    def _has_frozen_base_class(cls):
-        """
-        Check whether *cls* has a frozen ancestor by looking at its
-        __setattr__.
-        """
-        return (
-            getattr(cls.__setattr__, "__module__", None)
-            == _frozen_setattrs.__module__
-            and cls.__setattr__.__name__ == _frozen_setattrs.__name__
-        )
-
-else:
-
-    def _has_frozen_base_class(cls):
-        """
-        Check whether *cls* has a frozen ancestor by looking at its
-        __setattr__.
-        """
-        return cls.__setattr__ == _frozen_setattrs
+def _has_frozen_base_class(cls):
+    """
+    Check whether *cls* has a frozen ancestor by looking at its
+    __setattr__.
+    """
+    return cls.__setattr__ is _frozen_setattrs
 
 
 def _generate_unique_filename(cls, func_name):
     """
     Create a "filename" suitable for a function being generated.
     """
-    unique_filename = "<attrs generated {0} {1}.{2}>".format(
+    unique_filename = "<attrs generated {} {}.{}>".format(
         func_name,
         cls.__module__,
         getattr(cls, "__qualname__", cls.__name__),
@@ -1687,8 +1641,7 @@
     if not cache_hash:
         hash_def += "):"
     else:
-        if not PY2:
-            hash_def += ", *"
+        hash_def += ", *"
 
         hash_def += (
             ", _cache_wrapper="
@@ -1987,15 +1940,7 @@
                 return "..."
             real_cls = self.__class__
             if ns is None:
-                qualname = getattr(real_cls, "__qualname__", None)
-                if qualname is not None:  # pragma: no cover
-                    # This case only happens on Python 3.5 and 3.6. We exclude
-                    # it from coverage, because we don't want to slow down our
-                    # test suite by running them under coverage too for this
-                    # one line.
-                    class_name = qualname.rsplit(">.", 1)[-1]
-                else:
-                    class_name = real_cls.__name__
+                class_name = real_cls.__qualname__.rsplit(">.", 1)[-1]
             else:
                 class_name = ns + "." + real_cls.__name__
 
@@ -2086,7 +2031,7 @@
         raise NotAnAttrsClassError(
             "{cls!r} is not an attrs-decorated class.".format(cls=cls)
         )
-    return ordered_dict(((a.name, a) for a in attrs))
+    return ordered_dict((a.name, a) for a in attrs)
 
 
 def validate(inst):
@@ -2236,63 +2181,6 @@
     )
 
 
-if PY2:
-
-    def _unpack_kw_only_py2(attr_name, default=None):
-        """
-        Unpack *attr_name* from _kw_only dict.
-        """
-        if default is not None:
-            arg_default = ", %s" % default
-        else:
-            arg_default = ""
-        return "%s = _kw_only.pop('%s'%s)" % (
-            attr_name,
-            attr_name,
-            arg_default,
-        )
-
-    def _unpack_kw_only_lines_py2(kw_only_args):
-        """
-        Unpack all *kw_only_args* from _kw_only dict and handle errors.
-
-        Given a list of strings "{attr_name}" and "{attr_name}={default}"
-        generates list of lines of code that pop attrs from _kw_only dict and
-        raise TypeError similar to builtin if required attr is missing or
-        extra key is passed.
-
-        >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"])))
-        try:
-            a = _kw_only.pop('a')
-            b = _kw_only.pop('b', 42)
-        except KeyError as _key_error:
-            raise TypeError(
-                ...
-        if _kw_only:
-            raise TypeError(
-                ...
-        """
-        lines = ["try:"]
-        lines.extend(
-            "    " + _unpack_kw_only_py2(*arg.split("="))
-            for arg in kw_only_args
-        )
-        lines += """\
-except KeyError as _key_error:
-    raise TypeError(
-        '__init__() missing required keyword-only argument: %s' % _key_error
-    )
-if _kw_only:
-    raise TypeError(
-        '__init__() got an unexpected keyword argument %r'
-        % next(iter(_kw_only))
-    )
-""".split(
-            "\n"
-        )
-        return lines
-
-
 def _attrs_to_init_script(
     attrs,
     frozen,
@@ -2548,15 +2436,10 @@
 
     args = ", ".join(args)
     if kw_only_args:
-        if PY2:
-            lines = _unpack_kw_only_lines_py2(kw_only_args) + lines
-
-            args += "%s**_kw_only" % (", " if args else "",)  # leading comma
-        else:
-            args += "%s*, %s" % (
-                ", " if args else "",  # leading comma
-                ", ".join(kw_only_args),  # kw_only args
-            )
+        args += "%s*, %s" % (
+            ", " if args else "",  # leading comma
+            ", ".join(kw_only_args),  # kw_only args
+        )
     return (
         """\
 def {init_name}(self, {args}):
@@ -2571,7 +2454,7 @@
     )
 
 
-class Attribute(object):
+class Attribute:
     """
     *Read-only* representation of an attribute.
 
@@ -2793,7 +2676,7 @@
 )
 
 
-class _CountingAttr(object):
+class _CountingAttr:
     """
     Intermediate representation of attributes that uses a counter to preserve
     the order in which the attributes have been defined.
@@ -2936,7 +2819,7 @@
 _CountingAttr = _add_eq(_add_repr(_CountingAttr))
 
 
-class Factory(object):
+class Factory:
     """
     Stores a factory callable.
 
@@ -3022,7 +2905,7 @@
     if isinstance(attrs, dict):
         cls_dict = attrs
     elif isinstance(attrs, (list, tuple)):
-        cls_dict = dict((a, attrib()) for a in attrs)
+        cls_dict = {a: attrib() for a in attrs}
     else:
         raise TypeError("attrs argument must be a dict or a list.")
 
@@ -3071,7 +2954,7 @@
 
 
 @attrs(slots=True, hash=True)
-class _AndValidator(object):
+class _AndValidator:
     """
     Compose many validators to a single one.
     """
@@ -3126,10 +3009,9 @@
         return val
 
     if not converters:
-        if not PY2:
-            # If the converter list is empty, pipe_converter is the identity.
-            A = typing.TypeVar("A")
-            pipe_converter.__annotations__ = {"val": A, "return": A}
+        # If the converter list is empty, pipe_converter is the identity.
+        A = typing.TypeVar("A")
+        pipe_converter.__annotations__ = {"val": A, "return": A}
     else:
         # Get parameter type from first converter.
         t = _AnnotationExtractor(converters[0]).get_first_param_type()
diff --git a/src/attr/_version_info.py b/src/attr/_version_info.py
index cdaeec3..51a1312 100644
--- a/src/attr/_version_info.py
+++ b/src/attr/_version_info.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 from functools import total_ordering
 
@@ -10,7 +9,7 @@
 
 @total_ordering
 @attrs(eq=False, order=False, slots=True, frozen=True)
-class VersionInfo(object):
+class VersionInfo:
     """
     A version object that can be compared to tuple of length 1--4:
 
diff --git a/src/attr/converters.py b/src/attr/converters.py
index b591539..a73626c 100644
--- a/src/attr/converters.py
+++ b/src/attr/converters.py
@@ -4,16 +4,13 @@
 Commonly useful converters.
 """
 
-from __future__ import absolute_import, division, print_function
 
-from ._compat import PY2, _AnnotationExtractor
+import typing
+
+from ._compat import _AnnotationExtractor
 from ._make import NOTHING, Factory, pipe
 
 
-if not PY2:
-    import typing
-
-
 __all__ = [
     "default_if_none",
     "optional",
diff --git a/src/attr/exceptions.py b/src/attr/exceptions.py
index b2f1edc..5dc51e0 100644
--- a/src/attr/exceptions.py
+++ b/src/attr/exceptions.py
@@ -1,7 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
-
 
 class FrozenError(AttributeError):
     """
diff --git a/src/attr/filters.py b/src/attr/filters.py
index a1978a8..e7432e4 100644
--- a/src/attr/filters.py
+++ b/src/attr/filters.py
@@ -4,7 +4,6 @@
 Commonly useful filters for `attr.asdict`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 from ._compat import isclass
 from ._make import Attribute
diff --git a/src/attr/setters.py b/src/attr/setters.py
index b9f42a4..12ed675 100644
--- a/src/attr/setters.py
+++ b/src/attr/setters.py
@@ -4,7 +4,6 @@
 Commonly used hooks for on_setattr.
 """
 
-from __future__ import absolute_import, division, print_function
 
 from . import _config
 from .exceptions import FrozenAttributeError
diff --git a/src/attr/validators.py b/src/attr/validators.py
index 5f850cc..e1c01b4 100644
--- a/src/attr/validators.py
+++ b/src/attr/validators.py
@@ -4,7 +4,6 @@
 Commonly useful validators.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import operator
 import re
@@ -93,7 +92,7 @@
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _InstanceOfValidator(object):
+class _InstanceOfValidator:
     type = attrib()
 
     def __call__(self, inst, attr, value):
@@ -137,7 +136,7 @@
 
 
 @attrs(repr=False, frozen=True, slots=True)
-class _MatchesReValidator(object):
+class _MatchesReValidator:
     pattern = attrib()
     match_func = attrib()
 
@@ -179,8 +178,7 @@
     .. versionadded:: 19.2.0
     .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern.
     """
-    fullmatch = getattr(re, "fullmatch", None)
-    valid_funcs = (fullmatch, None, re.search, re.match)
+    valid_funcs = (re.fullmatch, None, re.search, re.match)
     if func not in valid_funcs:
         raise ValueError(
             "'func' must be one of {}.".format(
@@ -206,19 +204,14 @@
         match_func = pattern.match
     elif func is re.search:
         match_func = pattern.search
-    elif fullmatch:
+    else:
         match_func = pattern.fullmatch
-    else:  # Python 2 fullmatch emulation (https://bugs.python.org/issue16203)
-        pattern = re.compile(
-            r"(?:{})\Z".format(pattern.pattern), pattern.flags
-        )
-        match_func = pattern.match
 
     return _MatchesReValidator(pattern, match_func)
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _ProvidesValidator(object):
+class _ProvidesValidator:
     interface = attrib()
 
     def __call__(self, inst, attr, value):
@@ -260,7 +253,7 @@
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _OptionalValidator(object):
+class _OptionalValidator:
     validator = attrib()
 
     def __call__(self, inst, attr, value):
@@ -294,7 +287,7 @@
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _InValidator(object):
+class _InValidator:
     options = attrib()
 
     def __call__(self, inst, attr, value):
@@ -335,7 +328,7 @@
 
 
 @attrs(repr=False, slots=False, hash=True)
-class _IsCallableValidator(object):
+class _IsCallableValidator:
     def __call__(self, inst, attr, value):
         """
         We use a callable class to be able to change the ``__repr__``.
@@ -372,7 +365,7 @@
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _DeepIterable(object):
+class _DeepIterable:
     member_validator = attrib(validator=is_callable())
     iterable_validator = attrib(
         default=None, validator=optional(is_callable())
@@ -421,7 +414,7 @@
 
 
 @attrs(repr=False, slots=True, hash=True)
-class _DeepMapping(object):
+class _DeepMapping:
     key_validator = attrib(validator=is_callable())
     value_validator = attrib(validator=is_callable())
     mapping_validator = attrib(default=None, validator=optional(is_callable()))
@@ -460,7 +453,7 @@
 
 
 @attrs(repr=False, frozen=True, slots=True)
-class _NumberValidator(object):
+class _NumberValidator:
     bound = attrib()
     compare_op = attrib()
     compare_func = attrib()
@@ -534,7 +527,7 @@
 
 
 @attrs(repr=False, frozen=True, slots=True)
-class _MaxLengthValidator(object):
+class _MaxLengthValidator:
     max_length = attrib()
 
     def __call__(self, inst, attr, value):
@@ -565,7 +558,7 @@
 
 
 @attrs(repr=False, frozen=True, slots=True)
-class _MinLengthValidator(object):
+class _MinLengthValidator:
     min_length = attrib()
 
     def __call__(self, inst, attr, value):
diff --git a/tests/attr_import_star.py b/tests/attr_import_star.py
index eaec321..6365452 100644
--- a/tests/attr_import_star.py
+++ b/tests/attr_import_star.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import
 
 from attr import *  # noqa: F401,F403
 
diff --git a/tests/strategies.py b/tests/strategies.py
index 99f9f48..f630b22 100644
--- a/tests/strategies.py
+++ b/tests/strategies.py
@@ -28,8 +28,7 @@
     Some short strings (such as 'as') are keywords, so we skip them.
     """
     lc = string.ascii_lowercase
-    for c in lc:
-        yield c
+    yield from lc
     for outer in lc:
         for inner in lc:
             res = outer + inner
diff --git a/tests/test_3rd_party.py b/tests/test_3rd_party.py
index 8866d7f..0707b2c 100644
--- a/tests/test_3rd_party.py
+++ b/tests/test_3rd_party.py
@@ -14,7 +14,7 @@
 cloudpickle = pytest.importorskip("cloudpickle")
 
 
-class TestCloudpickleCompat(object):
+class TestCloudpickleCompat:
     """
     Tests for compatibility with ``cloudpickle``.
     """
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
index a201ebf..49c9b0d 100644
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -113,7 +113,7 @@
         i = C(42)
         assert "C(a=42, x=[], y=2, z=3, foo=None)" == repr(i)
 
-        attr_names = set(a.name for a in C.__attrs_attrs__)
+        attr_names = {a.name for a in C.__attrs_attrs__}
         assert "a" in attr_names  # just double check that the set works
         assert "cls_var" not in attr_names
 
diff --git a/tests/test_cmp.py b/tests/test_cmp.py
index ec2c687..b938387 100644
--- a/tests/test_cmp.py
+++ b/tests/test_cmp.py
@@ -4,12 +4,10 @@
 Tests for methods from `attrib._cmp`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import pytest
 
 from attr._cmp import cmp_using
-from attr._compat import PY2
 
 
 # Test parameters.
@@ -57,7 +55,7 @@
 cmp_ids = eq_ids + order_ids
 
 
-class TestEqOrder(object):
+class TestEqOrder:
     """
     Tests for eq and order related methods.
     """
@@ -92,7 +90,6 @@
     #########
     # lt
     #########
-    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
     @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
     def test_lt_unorderable(self, cls, requires_same_type):
         """
@@ -131,9 +128,8 @@
         if requires_same_type:
             # Unlike __eq__, NotImplemented will cause an exception to be
             # raised from __lt__.
-            if not PY2:
-                with pytest.raises(TypeError):
-                    cls(1) < cls(2.0)
+            with pytest.raises(TypeError):
+                cls(1) < cls(2.0)
         else:
             assert cls(1) < cls(2.0)
             assert not (cls(2) < cls(1.0))
@@ -141,7 +137,6 @@
     #########
     # le
     #########
-    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
     @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
     def test_le_unorderable(self, cls, requires_same_type):
         """
@@ -182,9 +177,8 @@
         if requires_same_type:
             # Unlike __eq__, NotImplemented will cause an exception to be
             # raised from __le__.
-            if not PY2:
-                with pytest.raises(TypeError):
-                    cls(1) <= cls(2.0)
+            with pytest.raises(TypeError):
+                cls(1) <= cls(2.0)
         else:
             assert cls(1) <= cls(2.0)
             assert cls(1) <= cls(1.0)
@@ -193,7 +187,6 @@
     #########
     # gt
     #########
-    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
     @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
     def test_gt_unorderable(self, cls, requires_same_type):
         """
@@ -232,9 +225,8 @@
         if requires_same_type:
             # Unlike __eq__, NotImplemented will cause an exception to be
             # raised from __gt__.
-            if not PY2:
-                with pytest.raises(TypeError):
-                    cls(2) > cls(1.0)
+            with pytest.raises(TypeError):
+                cls(2) > cls(1.0)
         else:
             assert cls(2) > cls(1.0)
             assert not (cls(1) > cls(2.0))
@@ -242,7 +234,6 @@
     #########
     # ge
     #########
-    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
     @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
     def test_ge_unorderable(self, cls, requires_same_type):
         """
@@ -283,16 +274,15 @@
         if requires_same_type:
             # Unlike __eq__, NotImplemented will cause an exception to be
             # raised from __ge__.
-            if not PY2:
-                with pytest.raises(TypeError):
-                    cls(2) >= cls(1.0)
+            with pytest.raises(TypeError):
+                cls(2) >= cls(1.0)
         else:
             assert cls(2) >= cls(2.0)
             assert cls(2) >= cls(1.0)
             assert not (cls(1) >= cls(2.0))
 
 
-class TestDundersUnnamedClass(object):
+class TestDundersUnnamedClass:
     """
     Tests for dunder attributes of unnamed classes.
     """
@@ -304,8 +294,7 @@
         Class name and qualified name should be well behaved.
         """
         assert self.cls.__name__ == "Comparable"
-        if not PY2:
-            assert self.cls.__qualname__ == "Comparable"
+        assert self.cls.__qualname__ == "Comparable"
 
     def test_eq(self):
         """
@@ -327,7 +316,7 @@
         assert method.__name__ == "__ne__"
 
 
-class TestTotalOrderingException(object):
+class TestTotalOrderingException:
     """
     Test for exceptions related to total ordering.
     """
@@ -345,7 +334,7 @@
         )
 
 
-class TestNotImplementedIsPropagated(object):
+class TestNotImplementedIsPropagated:
     """
     Test related to functions that return NotImplemented.
     """
@@ -361,7 +350,7 @@
         assert C(1) != C(1)
 
 
-class TestDundersPartialOrdering(object):
+class TestDundersPartialOrdering:
     """
     Tests for dunder attributes of classes with partial ordering.
     """
@@ -373,8 +362,7 @@
         Class name and qualified name should be well behaved.
         """
         assert self.cls.__name__ == "PartialOrderCSameType"
-        if not PY2:
-            assert self.cls.__qualname__ == "PartialOrderCSameType"
+        assert self.cls.__qualname__ == "PartialOrderCSameType"
 
     def test_eq(self):
         """
@@ -408,12 +396,9 @@
         __le__ docstring and qualified name should be well behaved.
         """
         method = self.cls.__le__
-        if PY2:
-            assert method.__doc__ == "x.__le__(y) <==> x<=y"
-        else:
-            assert method.__doc__.strip().startswith(
-                "Return a <= b.  Computed by @total_ordering from"
-            )
+        assert method.__doc__.strip().startswith(
+            "Return a <= b.  Computed by @total_ordering from"
+        )
         assert method.__name__ == "__le__"
 
     def test_gt(self):
@@ -421,12 +406,9 @@
         __gt__ docstring and qualified name should be well behaved.
         """
         method = self.cls.__gt__
-        if PY2:
-            assert method.__doc__ == "x.__gt__(y) <==> x>y"
-        else:
-            assert method.__doc__.strip().startswith(
-                "Return a > b.  Computed by @total_ordering from"
-            )
+        assert method.__doc__.strip().startswith(
+            "Return a > b.  Computed by @total_ordering from"
+        )
         assert method.__name__ == "__gt__"
 
     def test_ge(self):
@@ -434,16 +416,13 @@
         __ge__ docstring and qualified name should be well behaved.
         """
         method = self.cls.__ge__
-        if PY2:
-            assert method.__doc__ == "x.__ge__(y) <==> x>=y"
-        else:
-            assert method.__doc__.strip().startswith(
-                "Return a >= b.  Computed by @total_ordering from"
-            )
+        assert method.__doc__.strip().startswith(
+            "Return a >= b.  Computed by @total_ordering from"
+        )
         assert method.__name__ == "__ge__"
 
 
-class TestDundersFullOrdering(object):
+class TestDundersFullOrdering:
     """
     Tests for dunder attributes of classes with full ordering.
     """
@@ -455,8 +434,7 @@
         Class name and qualified name should be well behaved.
         """
         assert self.cls.__name__ == "FullOrderCSameType"
-        if not PY2:
-            assert self.cls.__qualname__ == "FullOrderCSameType"
+        assert self.cls.__qualname__ == "FullOrderCSameType"
 
     def test_eq(self):
         """
diff --git a/tests/test_config.py b/tests/test_config.py
index bbf6756..6c78fd2 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,14 +4,13 @@
 Tests for `attr._config`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import pytest
 
 from attr import _config
 
 
-class TestConfig(object):
+class TestConfig:
     def test_default(self):
         """
         Run validators by default.
diff --git a/tests/test_converters.py b/tests/test_converters.py
index 3213e3a..7607e55 100644
--- a/tests/test_converters.py
+++ b/tests/test_converters.py
@@ -4,7 +4,6 @@
 Tests for `attr.converters`.
 """
 
-from __future__ import absolute_import
 
 import pytest
 
@@ -14,7 +13,7 @@
 from attr.converters import default_if_none, optional, pipe, to_bool
 
 
-class TestOptional(object):
+class TestOptional:
     """
     Tests for `optional`.
     """
@@ -45,7 +44,7 @@
             c("not_an_int")
 
 
-class TestDefaultIfNone(object):
+class TestDefaultIfNone:
     def test_missing_default(self):
         """
         Raises TypeError if neither default nor factory have been passed.
@@ -101,7 +100,7 @@
         assert [] == c(None)
 
 
-class TestPipe(object):
+class TestPipe:
     def test_success(self):
         """
         Succeeds if all wrapped converters succeed.
@@ -130,7 +129,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a1 = attrib(default="True", converter=pipe(str, to_bool, bool))
             a2 = attrib(default=True, converter=[str, to_bool, bool])
 
@@ -146,7 +145,7 @@
         assert o is pipe()(o)
 
 
-class TestToBool(object):
+class TestToBool:
     def test_unhashable(self):
         """
         Fails if value is unhashable.
diff --git a/tests/test_dunders.py b/tests/test_dunders.py
index 186762e..03644f8 100644
--- a/tests/test_dunders.py
+++ b/tests/test_dunders.py
@@ -4,7 +4,6 @@
 Tests for dunder methods from `attrib._make`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import copy
 import pickle
@@ -40,25 +39,25 @@
 
 
 @attr.s(eq=True)
-class EqCallableC(object):
+class EqCallableC:
     a = attr.ib(eq=str.lower, order=False)
     b = attr.ib(eq=True)
 
 
 @attr.s(eq=True, slots=True)
-class EqCallableCSlots(object):
+class EqCallableCSlots:
     a = attr.ib(eq=str.lower, order=False)
     b = attr.ib(eq=True)
 
 
 @attr.s(order=True)
-class OrderCallableC(object):
+class OrderCallableC:
     a = attr.ib(eq=True, order=str.lower)
     b = attr.ib(order=True)
 
 
 @attr.s(order=True, slots=True)
-class OrderCallableCSlots(object):
+class OrderCallableCSlots:
     a = attr.ib(eq=True, order=str.lower)
     b = attr.ib(order=True)
 
@@ -102,14 +101,14 @@
     return cls
 
 
-class InitC(object):
+class InitC:
     __attrs_attrs__ = [simple_attr("a"), simple_attr("b")]
 
 
 InitC = _add_init(InitC, False)
 
 
-class TestEqOrder(object):
+class TestEqOrder:
     """
     Tests for eq and order related methods.
     """
@@ -168,7 +167,7 @@
         match.
         """
 
-        class NotEqC(object):
+        class NotEqC:
             a = 1
             b = 2
 
@@ -316,7 +315,7 @@
         assert NotImplemented == (cls(1, 2).__ge__(42))
 
 
-class TestAddRepr(object):
+class TestAddRepr:
     """
     Tests for `_add_repr`.
     """
@@ -349,7 +348,7 @@
             return "foo:" + str(value)
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib(repr=custom_repr)
 
         assert "C(a=foo:1)" == repr(C(1))
@@ -361,7 +360,7 @@
         """
 
         @attr.s
-        class Cycle(object):
+        class Cycle:
             value = attr.ib(default=7)
             cycle = attr.ib(default=None)
 
@@ -376,7 +375,7 @@
         """
 
         @attr.s
-        class LongCycle(object):
+        class LongCycle:
             value = attr.ib(default=14)
             cycle = attr.ib(default=None)
 
@@ -391,7 +390,7 @@
         repr does not strip underscores.
         """
 
-        class C(object):
+        class C:
             __attrs_attrs__ = [simple_attr("_x")]
 
         C = _add_repr(C)
@@ -440,21 +439,21 @@
 # these are for use in TestAddHash.test_cache_hash_serialization
 # they need to be out here so they can be un-pickled
 @attr.attrs(hash=True, cache_hash=False)
-class HashCacheSerializationTestUncached(object):
+class HashCacheSerializationTestUncached:
     foo_value = attr.ib()
 
 
 @attr.attrs(hash=True, cache_hash=True)
-class HashCacheSerializationTestCached(object):
+class HashCacheSerializationTestCached:
     foo_value = attr.ib()
 
 
 @attr.attrs(slots=True, hash=True, cache_hash=True)
-class HashCacheSerializationTestCachedSlots(object):
+class HashCacheSerializationTestCachedSlots:
     foo_value = attr.ib()
 
 
-class IncrementingHasher(object):
+class IncrementingHasher:
     def __init__(self):
         self.hash_value = 100
 
@@ -464,7 +463,7 @@
         return rv
 
 
-class TestAddHash(object):
+class TestAddHash:
     """
     Tests for `_add_hash`.
     """
@@ -661,7 +660,7 @@
             kwargs["hash"] = True
 
         @attr.s(**kwargs)
-        class C(object):
+        class C:
             x = attr.ib()
 
         a = C(IncrementingHasher())
@@ -711,7 +710,7 @@
         """
 
         @attr.s(frozen=frozen, cache_hash=True, hash=True)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __getstate__(self):
@@ -729,7 +728,7 @@
         return pickle.loads(pickle_str)
 
 
-class TestAddInit(object):
+class TestAddInit:
     """
     Tests for `_add_init`.
     """
@@ -802,7 +801,7 @@
         If a default value is present, it's used as fallback.
         """
 
-        class C(object):
+        class C:
             __attrs_attrs__ = [
                 simple_attr(name="a", default=2),
                 simple_attr(name="b", default="hallo"),
@@ -820,10 +819,10 @@
         If a default factory is present, it's used as fallback.
         """
 
-        class D(object):
+        class D:
             pass
 
-        class C(object):
+        class C:
             __attrs_attrs__ = [
                 simple_attr(name="a", default=Factory(list)),
                 simple_attr(name="b", default=Factory(D)),
@@ -898,7 +897,7 @@
         underscores.
         """
 
-        class C(object):
+        class C:
             __attrs_attrs__ = [simple_attr("_private")]
 
         C = _add_init(C, False)
@@ -906,7 +905,7 @@
         assert 42 == i._private
 
 
-class TestNothing(object):
+class TestNothing:
     """
     Tests for `_Nothing`.
     """
@@ -942,7 +941,7 @@
 
 
 @attr.s(hash=True, order=True)
-class C(object):
+class C:
     pass
 
 
@@ -951,7 +950,7 @@
 
 
 @attr.s(hash=True, order=True)
-class C(object):
+class C:
     pass
 
 
@@ -959,13 +958,13 @@
 
 
 @attr.s(hash=True, order=True)
-class C(object):
+class C:
     """A different class, to generate different methods."""
 
     a = attr.ib()
 
 
-class TestFilenames(object):
+class TestFilenames:
     def test_filenames(self):
         """
         The created dunder methods have a "consistent" filename.
diff --git a/tests/test_filters.py b/tests/test_filters.py
index d1ec24d..6945bd2 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -4,7 +4,6 @@
 Tests for `attr.filters`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import pytest
 
@@ -15,12 +14,12 @@
 
 
 @attr.s
-class C(object):
+class C:
     a = attr.ib()
     b = attr.ib()
 
 
-class TestSplitWhat(object):
+class TestSplitWhat:
     """
     Tests for `_split_what`.
     """
@@ -35,7 +34,7 @@
         ) == _split_what((str, fields(C).a, int))
 
 
-class TestInclude(object):
+class TestInclude:
     """
     Tests for `include`.
     """
@@ -73,7 +72,7 @@
         assert i(fields(C).a, value) is False
 
 
-class TestExclude(object):
+class TestExclude:
     """
     Tests for `exclude`.
     """
diff --git a/tests/test_funcs.py b/tests/test_funcs.py
index 4490ed8..40c5487 100644
--- a/tests/test_funcs.py
+++ b/tests/test_funcs.py
@@ -4,7 +4,6 @@
 Tests for `attr._funcs`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 from collections import OrderedDict
 
@@ -35,14 +34,14 @@
     import attr
 
     @attr.s
-    class C(object):
+    class C:
         x = attr.ib()
         y = attr.ib()
 
     return C
 
 
-class TestAsDict(object):
+class TestAsDict:
     """
     Tests for `asdict`.
     """
@@ -207,7 +206,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a = attr.ib()
 
         instance = A({(1,): 1})
@@ -225,7 +224,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a = attr.ib()
 
         instance = A({(1,): 1})
@@ -233,7 +232,7 @@
         assert {"a": {(1,): 1}} == attr.asdict(instance)
 
 
-class TestAsTuple(object):
+class TestAsTuple:
     """
     Tests for `astuple`.
     """
@@ -391,7 +390,7 @@
         assert (1, [1, 2, 3]) == d
 
 
-class TestHas(object):
+class TestHas:
     """
     Tests for `has`.
     """
@@ -408,7 +407,7 @@
         """
 
         @attr.s
-        class D(object):
+        class D:
             pass
 
         assert has(D)
@@ -420,7 +419,7 @@
         assert not has(object)
 
 
-class TestAssoc(object):
+class TestAssoc:
     """
     Tests for `assoc`.
     """
@@ -432,7 +431,7 @@
         """
 
         @attr.s(slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             pass
 
         i1 = C()
@@ -494,7 +493,7 @@
         """
 
         @attr.s(frozen=True)
-        class C(object):
+        class C:
             x = attr.ib()
             y = attr.ib()
 
@@ -507,7 +506,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib()
 
         with pytest.warns(DeprecationWarning) as wi:
@@ -516,7 +515,7 @@
         assert __file__ == wi.list[0].filename
 
 
-class TestEvolve(object):
+class TestEvolve:
     """
     Tests for `evolve`.
     """
@@ -528,7 +527,7 @@
         """
 
         @attr.s(slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             pass
 
         i1 = C()
@@ -593,7 +592,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib(validator=instance_of(int))
 
         with pytest.raises(TypeError) as e:
@@ -608,7 +607,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             _a = attr.ib()
 
         assert evolve(C(1), a=2)._a == 2
@@ -625,7 +624,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib()
             b = attr.ib(init=False, default=0)
 
@@ -639,11 +638,11 @@
         """
 
         @attr.s
-        class Cls1(object):
+        class Cls1:
             param1 = attr.ib()
 
         @attr.s
-        class Cls2(object):
+        class Cls2:
             param2 = attr.ib()
 
         obj2a = Cls2(param2="a")
@@ -663,11 +662,11 @@
         """
 
         @attr.s
-        class Cls1(object):
+        class Cls1:
             param1 = attr.ib()
 
         @attr.s
-        class Cls2(object):
+        class Cls2:
             param2 = attr.ib()
 
         obj2a = Cls2(param2="a")
diff --git a/tests/test_functional.py b/tests/test_functional.py
index 6bb989f..c6a8084 100644
--- a/tests/test_functional.py
+++ b/tests/test_functional.py
@@ -4,7 +4,6 @@
 End-to-end tests.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import inspect
 import pickle
@@ -12,14 +11,13 @@
 from copy import deepcopy
 
 import pytest
-import six
 
 from hypothesis import assume, given
 from hypothesis.strategies import booleans
 
 import attr
 
-from attr._compat import PY2, PY36, TYPE
+from attr._compat import PY36, TYPE
 from attr._make import NOTHING, Attribute
 from attr.exceptions import FrozenInstanceError
 
@@ -27,13 +25,13 @@
 
 
 @attr.s
-class C1(object):
+class C1:
     x = attr.ib(validator=attr.validators.instance_of(int))
     y = attr.ib()
 
 
 @attr.s(slots=True)
-class C1Slots(object):
+class C1Slots:
     x = attr.ib(validator=attr.validators.instance_of(int))
     y = attr.ib()
 
@@ -42,19 +40,19 @@
 
 
 @attr.s()
-class C2(object):
+class C2:
     x = attr.ib(default=foo)
     y = attr.ib(default=attr.Factory(list))
 
 
 @attr.s(slots=True)
-class C2Slots(object):
+class C2Slots:
     x = attr.ib(default=foo)
     y = attr.ib(default=attr.Factory(list))
 
 
 @attr.s
-class Base(object):
+class Base:
     x = attr.ib()
 
     def meth(self):
@@ -62,7 +60,7 @@
 
 
 @attr.s(slots=True)
-class BaseSlots(object):
+class BaseSlots:
     x = attr.ib()
 
     def meth(self):
@@ -80,7 +78,7 @@
 
 
 @attr.s(frozen=True, slots=True)
-class Frozen(object):
+class Frozen:
     x = attr.ib()
 
 
@@ -90,7 +88,7 @@
 
 
 @attr.s(frozen=True, slots=False)
-class FrozenNoSlots(object):
+class FrozenNoSlots:
     x = attr.ib()
 
 
@@ -99,21 +97,19 @@
 
 
 @attr.s
-@six.add_metaclass(Meta)
-class WithMeta(object):
+class WithMeta(metaclass=Meta):
     pass
 
 
 @attr.s(slots=True)
-@six.add_metaclass(Meta)
-class WithMetaSlots(object):
+class WithMetaSlots(metaclass=Meta):
     pass
 
 
 FromMakeClass = attr.make_class("FromMakeClass", ["x"])
 
 
-class TestFunctional(object):
+class TestFunctional:
     """
     Functional tests.
     """
@@ -181,7 +177,7 @@
         """
 
         @attr.s(slots=slots)
-        class C3(object):
+        class C3:
             _x = attr.ib()
 
         assert "C3(_x=1)" == repr(C3(x=1))
@@ -351,7 +347,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(default=1)
             y = attr.ib()
 
@@ -380,7 +376,7 @@
         dict-classes are never replaced.
         """
 
-        class C(object):
+        class C:
             x = attr.ib()
 
         C_new = attr.s(C)
@@ -395,7 +391,7 @@
         """
 
         @attr.s(hash=False)
-        class HashByIDBackwardCompat(object):
+        class HashByIDBackwardCompat:
             x = attr.ib()
 
         assert hash(HashByIDBackwardCompat(1)) != hash(
@@ -403,13 +399,13 @@
         )
 
         @attr.s(hash=False, eq=False)
-        class HashByID(object):
+        class HashByID:
             x = attr.ib()
 
         assert hash(HashByID(1)) != hash(HashByID(1))
 
         @attr.s(hash=True)
-        class HashByValues(object):
+        class HashByValues:
             x = attr.ib()
 
         assert hash(HashByValues(1)) == hash(HashByValues(1))
@@ -420,11 +416,11 @@
         """
 
         @attr.s
-        class Unhashable(object):
+        class Unhashable:
             pass
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(default=Unhashable())
 
         @attr.s
@@ -438,7 +434,7 @@
         """
 
         @attr.s(hash=False, eq=False, slots=slots)
-        class C(object):
+        class C:
             pass
 
         assert hash(C()) != hash(C())
@@ -450,7 +446,7 @@
         """
 
         @attr.s(eq=False, slots=slots)
-        class C(object):
+        class C:
             pass
 
         # Ensure both objects live long enough such that their ids/hashes
@@ -468,7 +464,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             c = attr.ib(default=100)
             x = attr.ib(default=1)
             b = attr.ib(default=23)
@@ -515,7 +511,7 @@
             slots=base_slots,
             weakref_slot=base_weakref_slot,
         )
-        class Base(object):
+        class Base:
             a = attr.ib(converter=int if base_converter else None)
 
         @attr.s(
@@ -542,7 +538,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             property = attr.ib()
             itemgetter = attr.ib()
             x = attr.ib()
@@ -633,24 +629,19 @@
         """
 
         @attr.s(eq=True, order=False, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
-        if not PY2:
-            possible_errors = (
-                "unorderable types: C() < C()",
-                "'<' not supported between instances of 'C' and 'C'",
-                "unorderable types: C < C",  # old PyPy 3
-            )
+        possible_errors = (
+            "unorderable types: C() < C()",
+            "'<' not supported between instances of 'C' and 'C'",
+            "unorderable types: C < C",  # old PyPy 3
+        )
 
-            with pytest.raises(TypeError) as ei:
-                C(5) < C(6)
+        with pytest.raises(TypeError) as ei:
+            C(5) < C(6)
 
-            assert ei.value.args[0] in possible_errors
-        else:
-            i = C(42)
-            for m in ("lt", "le", "gt", "ge"):
-                assert None is getattr(i, "__%s__" % (m,), None)
+        assert ei.value.args[0] in possible_errors
 
     @given(cmp=optional_bool, eq=optional_bool, order=optional_bool)
     def test_cmp_deprecated_attribute(self, cmp, eq, order):
@@ -678,7 +669,7 @@
         with pytest.deprecated_call() as dc:
 
             @attr.s
-            class C(object):
+            class C:
                 x = attr.ib(cmp=cmp, eq=eq, order=order)
 
             assert rv == attr.fields(C).x.cmp
@@ -702,7 +693,7 @@
         """
 
         @attr.s(on_setattr=attr.setters.validate)
-        class C(object):
+        class C:
             x = attr.ib()
 
         @attr.s(on_setattr=attr.setters.validate)
@@ -724,7 +715,7 @@
         """
 
         @attr.s(on_setattr=attr.setters.convert)
-        class C(object):
+        class C:
             x = attr.ib()
 
         @attr.s(on_setattr=attr.setters.convert)
@@ -748,7 +739,7 @@
         """
 
         @attr.define
-        class C(object):
+        class C:
             x = attr.ib()
 
         src = inspect.getsource(C.__init__)
@@ -775,7 +766,7 @@
         """
 
         @attr.s(on_setattr=attr.setters.validate)
-        class C(object):
+        class C:
             x = attr.ib(validator=42)
 
         @attr.s(on_setattr=attr.setters.validate)
diff --git a/tests/test_import.py b/tests/test_import.py
index 4231243..9e90a5c 100644
--- a/tests/test_import.py
+++ b/tests/test_import.py
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: MIT
 
 
-class TestImportStar(object):
+class TestImportStar:
     def test_from_attr_import_star(self):
         """
         import * from attr
diff --git a/tests/test_make.py b/tests/test_make.py
index cdca30d..d4d8640 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -4,7 +4,6 @@
 Tests for `attr._make`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import copy
 import functools
@@ -23,7 +22,7 @@
 import attr
 
 from attr import _config
-from attr._compat import PY2, PY310, ordered_dict
+from attr._compat import PY310, ordered_dict
 from attr._make import (
     Attribute,
     Factory,
@@ -41,11 +40,7 @@
     make_class,
     validate,
 )
-from attr.exceptions import (
-    DefaultAlreadySetError,
-    NotAnAttrsClassError,
-    PythonTooOldError,
-)
+from attr.exceptions import DefaultAlreadySetError, NotAnAttrsClassError
 
 from .strategies import (
     gen_attr_names,
@@ -62,7 +57,7 @@
 attrs_st = simple_attrs.map(lambda c: Attribute.from_counting_attr("name", c))
 
 
-class TestCountingAttr(object):
+class TestCountingAttr:
     """
     Tests for `attr`.
     """
@@ -151,7 +146,7 @@
 
 
 def make_tc():
-    class TransformC(object):
+    class TransformC:
         z = attr.ib()
         y = attr.ib()
         x = attr.ib()
@@ -160,7 +155,7 @@
     return TransformC
 
 
-class TestTransformAttrs(object):
+class TestTransformAttrs:
     """
     Tests for `_transform_attrs`.
     """
@@ -189,7 +184,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             pass
 
         assert _Attributes(((), [], {})) == _transform_attrs(
@@ -215,7 +210,7 @@
         mandatory attributes.
         """
 
-        class C(object):
+        class C:
             x = attr.ib(default=None)
             y = attr.ib()
 
@@ -241,7 +236,7 @@
         """
 
         @attr.s
-        class B(object):
+        class B:
             b = attr.ib()
 
         for b_a in B.__attrs_attrs__:
@@ -269,7 +264,7 @@
         If these is passed, use it and ignore body and base classes.
         """
 
-        class Base(object):
+        class Base:
             z = attr.ib()
 
         class C(Base):
@@ -288,7 +283,7 @@
         """
 
         @attr.s(init=False, these={"x": attr.ib()})
-        class C(object):
+        class C:
             x = 5
 
         assert 5 == C().x
@@ -303,7 +298,7 @@
         a = attr.ib(default=1)
 
         @attr.s(these=ordered_dict([("a", a), ("b", b)]))
-        class C(object):
+        class C:
             pass
 
         assert "C(a=1, b=2)" == repr(C())
@@ -316,7 +311,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a1 = attr.ib(default="a1")
             a2 = attr.ib(default="a2")
 
@@ -351,7 +346,7 @@
         """
 
         @attr.s(collect_by_mro=True)
-        class C(object):
+        class C:
             x = attr.ib(default=1)
 
         @attr.s(collect_by_mro=True)
@@ -368,7 +363,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a1 = attr.ib(default="a1")
             a2 = attr.ib(default="a2")
 
@@ -405,7 +400,7 @@
         """
 
         @attr.s(collect_by_mro=True)
-        class A(object):
+        class A:
 
             x = attr.ib(10)
 
@@ -437,7 +432,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a = attr.ib()
 
         @attr.s
@@ -461,31 +456,18 @@
         assert False is f(C).c.inherited
 
 
-class TestAttributes(object):
+class TestAttributes:
     """
     Tests for the `attrs`/`attr.s` class decorator.
     """
 
-    @pytest.mark.skipif(not PY2, reason="No old-style classes in Py3")
-    def test_catches_old_style(self):
-        """
-        Raises TypeError on old-style classes.
-        """
-        with pytest.raises(TypeError) as e:
-
-            @attr.s
-            class C:
-                pass
-
-        assert ("attrs only works with new-style classes.",) == e.value.args
-
     def test_sets_attrs(self):
         """
         Sets the `__attrs_attrs__` class attribute with a list of `Attribute`s.
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib()
 
         assert "x" == C.__attrs_attrs__[0].name
@@ -497,7 +479,7 @@
         """
 
         @attr.s
-        class C3(object):
+        class C3:
             pass
 
         assert "C3()" == repr(C3())
@@ -523,7 +505,7 @@
         # overwritten afterwards.
         sentinel = object()
 
-        class C(object):
+        class C:
             x = attr.ib()
 
         setattr(C, method_name, sentinel)
@@ -564,7 +546,7 @@
         if arg_name == "eq":
             am_args["order"] = False
 
-        class C(object):
+        class C:
             x = attr.ib()
 
         setattr(C, method_name, sentinel)
@@ -580,13 +562,12 @@
         Otherwise, it does not.
         """
 
-        class C(object):
+        class C:
             x = attr.ib()
 
         C = attr.s(init=init)(C)
         assert hasattr(C, "__attrs_init__") != init
 
-    @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.")
     @given(slots_outer=booleans(), slots_inner=booleans())
     def test_repr_qualname(self, slots_outer, slots_inner):
         """
@@ -594,9 +575,9 @@
         """
 
         @attr.s(slots=slots_outer)
-        class C(object):
+        class C:
             @attr.s(slots=slots_inner)
-            class D(object):
+            class D:
                 pass
 
         assert "C.D()" == repr(C.D())
@@ -609,14 +590,13 @@
         """
 
         @attr.s(slots=slots_outer)
-        class C(object):
+        class C:
             @attr.s(repr_ns="C", slots=slots_inner)
-            class D(object):
+            class D:
                 pass
 
         assert "C.D()" == repr(C.D())
 
-    @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.")
     @given(slots_outer=booleans(), slots_inner=booleans())
     def test_name_not_overridden(self, slots_outer, slots_inner):
         """
@@ -624,9 +604,9 @@
         """
 
         @attr.s(slots=slots_outer)
-        class C(object):
+        class C:
             @attr.s(slots=slots_inner)
-            class D(object):
+            class D:
                 pass
 
         assert C.D.__name__ == "D"
@@ -640,7 +620,7 @@
         monkeypatch.setattr(_config, "_run_validators", with_validation)
 
         @attr.s
-        class C(object):
+        class C:
             def __attrs_pre_init__(self2):
                 self2.z = 30
 
@@ -656,7 +636,7 @@
         monkeypatch.setattr(_config, "_run_validators", with_validation)
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib()
             y = attr.ib()
 
@@ -675,7 +655,7 @@
         monkeypatch.setattr(_config, "_run_validators", with_validation)
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __attrs_pre_init__(self2):
@@ -694,7 +674,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(type=int)
             y = attr.ib(type=str)
             z = attr.ib()
@@ -710,7 +690,7 @@
         """
 
         @attr.s(slots=slots)
-        class C(object):
+        class C:
             x = attr.ib()
 
         x = getattr(C, "x", None)
@@ -723,7 +703,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(factory=list)
 
         assert Factory(list) == attr.fields(C).x.default
@@ -735,7 +715,7 @@
         with pytest.raises(ValueError, match="mutually exclusive"):
 
             @attr.s
-            class C(object):
+            class C:
                 x = attr.ib(factory=list, default=Factory(list))
 
     def test_sugar_callable(self):
@@ -746,7 +726,7 @@
         with pytest.raises(ValueError, match="must be a callable"):
 
             @attr.s
-            class C(object):
+            class C:
                 x = attr.ib(factory=Factory(list))
 
     def test_inherited_does_not_affect_hashing_and_equality(self):
@@ -756,7 +736,7 @@
         """
 
         @attr.s
-        class BaseClass(object):
+        class BaseClass:
             x = attr.ib()
 
         @attr.s
@@ -770,7 +750,7 @@
         assert hash(ba) == hash(sa)
 
 
-class TestKeywordOnlyAttributes(object):
+class TestKeywordOnlyAttributes:
     """
     Tests for keyword-only attributes.
     """
@@ -781,7 +761,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib()
             b = attr.ib(default=2, kw_only=True)
             c = attr.ib(kw_only=True)
@@ -800,7 +780,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(init=False, default=0, kw_only=True)
             y = attr.ib()
 
@@ -816,20 +796,15 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(kw_only=True)
 
         with pytest.raises(TypeError) as e:
             C()
 
-        if PY2:
-            assert (
-                "missing required keyword-only argument: 'x'"
-            ) in e.value.args[0]
-        else:
-            assert (
-                "missing 1 required keyword-only argument: 'x'"
-            ) in e.value.args[0]
+        assert (
+            "missing 1 required keyword-only argument: 'x'"
+        ) in e.value.args[0]
 
     def test_keyword_only_attributes_unexpected(self):
         """
@@ -837,7 +812,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             x = attr.ib(kw_only=True)
 
         with pytest.raises(TypeError) as e:
@@ -854,7 +829,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib(kw_only=True)
             b = attr.ib(kw_only=True, default="b")
             c = attr.ib(kw_only=True)
@@ -883,7 +858,7 @@
         """
 
         @attr.s
-        class Base(object):
+        class Base:
             x = attr.ib(default=0)
 
         @attr.s
@@ -902,7 +877,7 @@
         """
 
         @attr.s(kw_only=True)
-        class C(object):
+        class C:
             x = attr.ib()
             y = attr.ib(kw_only=True)
 
@@ -921,7 +896,7 @@
         """
 
         @attr.s
-        class Base(object):
+        class Base:
             x = attr.ib(default=0)
 
         @attr.s(kw_only=True)
@@ -944,7 +919,7 @@
         """
 
         @attr.s
-        class KwArgBeforeInitFalse(object):
+        class KwArgBeforeInitFalse:
             kwarg = attr.ib(kw_only=True)
             non_init_function_default = attr.ib(init=False)
             non_init_keyword_default = attr.ib(
@@ -972,7 +947,7 @@
         """
 
         @attr.s
-        class KwArgBeforeInitFalseParent(object):
+        class KwArgBeforeInitFalseParent:
             kwarg = attr.ib(kw_only=True)
 
         @attr.s
@@ -993,34 +968,14 @@
         assert c.non_init_keyword_default == "default-by-keyword"
 
 
-@pytest.mark.skipif(not PY2, reason="PY2-specific keyword-only error behavior")
-class TestKeywordOnlyAttributesOnPy2(object):
-    """
-    Tests for keyword-only attribute behavior on py2.
-    """
-
-    def test_no_init(self):
-        """
-        Keyworld-only is a no-op, not any error, if ``init=false``.
-        """
-
-        @attr.s(kw_only=True, init=False)
-        class ClassLevel(object):
-            a = attr.ib()
-
-        @attr.s(init=False)
-        class AttrLevel(object):
-            a = attr.ib(kw_only=True)
-
-
 @attr.s
-class GC(object):
+class GC:
     @attr.s
-    class D(object):
+    class D:
         pass
 
 
-class TestMakeClass(object):
+class TestMakeClass:
     """
     Tests for `make_class`.
     """
@@ -1033,7 +988,7 @@
         C1 = make_class("C1", ls(["a", "b"]))
 
         @attr.s
-        class C2(object):
+        class C2:
             a = attr.ib()
             b = attr.ib()
 
@@ -1048,7 +1003,7 @@
         )
 
         @attr.s
-        class C2(object):
+        class C2:
             a = attr.ib(default=42)
             b = attr.ib(default=None)
 
@@ -1076,7 +1031,7 @@
         Parameter bases default to (object,) and subclasses correctly
         """
 
-        class D(object):
+        class D:
             pass
 
         cls = make_class("C", {})
@@ -1120,7 +1075,6 @@
 
         assert "C(a=1, b=2)" == repr(C())
 
-    @pytest.mark.skipif(PY2, reason="Python 3-only")
     def test_generic_dynamic_class(self):
         """
         make_class can create generic dynamic classes.
@@ -1137,7 +1091,7 @@
         attr.make_class("test", {"id": attr.ib(type=str)}, (MyParent[int],))
 
 
-class TestFields(object):
+class TestFields:
     """
     Tests for `fields`.
     """
@@ -1179,7 +1133,7 @@
             assert getattr(fields(C), attribute.name) is attribute
 
 
-class TestFieldsDict(object):
+class TestFieldsDict:
     """
     Tests for `fields_dict`.
     """
@@ -1217,7 +1171,7 @@
         assert [a.name for a in fields(C)] == [field_name for field_name in d]
 
 
-class TestConverter(object):
+class TestConverter:
     """
     Tests for attribute conversion.
     """
@@ -1330,7 +1284,7 @@
         C("1")
 
 
-class TestValidate(object):
+class TestValidate:
     """
     Tests for `validate`.
     """
@@ -1428,7 +1382,7 @@
 )
 
 
-class TestMetadata(object):
+class TestMetadata:
     """
     Tests for metadata handling.
     """
@@ -1523,7 +1477,7 @@
         assert md is a.metadata
 
 
-class TestClassBuilder(object):
+class TestClassBuilder:
     """
     Tests for `_ClassBuilder`.
     """
@@ -1545,7 +1499,7 @@
         repr of builder itself makes sense.
         """
 
-        class C(object):
+        class C:
             pass
 
         b = _ClassBuilder(
@@ -1572,7 +1526,7 @@
         All methods return the builder for chaining.
         """
 
-        class C(object):
+        class C:
             x = attr.ib()
 
         b = _ClassBuilder(
@@ -1627,12 +1581,12 @@
         """
 
         @attr.s(hash=True, str=True)
-        class C(object):
+        class C:
             def organic(self):
                 pass
 
         @attr.s(hash=True, str=True)
-        class D(object):
+        class D:
             pass
 
         meth_C = getattr(C, meth_name)
@@ -1640,11 +1594,10 @@
 
         assert meth_name == meth_C.__name__ == meth_D.__name__
         assert C.organic.__module__ == meth_C.__module__ == meth_D.__module__
-        if not PY2:
-            # This is assertion that would fail if a single __ne__ instance
-            # was reused across multiple _make_eq calls.
-            organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0]
-            assert organic_prefix + "." + meth_name == meth_C.__qualname__
+        # This is assertion that would fail if a single __ne__ instance
+        # was reused across multiple _make_eq calls.
+        organic_prefix = C.organic.__qualname__.rsplit(".", 1)[0]
+        assert organic_prefix + "." + meth_name == meth_C.__qualname__
 
     def test_handles_missing_meta_on_class(self):
         """
@@ -1652,7 +1605,7 @@
         either.
         """
 
-        class C(object):
+        class C:
             pass
 
         b = _ClassBuilder(
@@ -1691,7 +1644,7 @@
         """
 
         @attr.s(slots=True)
-        class C(object):
+        class C:
             __weakref__ = attr.ib(
                 init=False, hash=False, repr=False, eq=False, order=False
             )
@@ -1705,7 +1658,7 @@
         """
 
         @attr.s(slots=True)
-        class C(object):
+        class C:
             pass
 
         @attr.s(slots=True)
@@ -1748,7 +1701,7 @@
         """
 
         @attr.s(eq=True, **kwargs)
-        class C(object):
+        class C:
             x = attr.ib()
 
         a = C(1)
@@ -1763,7 +1716,7 @@
         """
 
         @attr.s(eq=True, **kwargs)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __getstate__(self):
@@ -1792,7 +1745,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             a = attr.ib()
 
         @attr.s
@@ -1815,21 +1768,20 @@
             == a.__ge__(b)
         )
 
-        if not PY2:
-            with pytest.raises(TypeError):
-                a <= b
+        with pytest.raises(TypeError):
+            a <= b
 
-            with pytest.raises(TypeError):
-                a >= b
+        with pytest.raises(TypeError):
+            a >= b
 
-            with pytest.raises(TypeError):
-                a < b
+        with pytest.raises(TypeError):
+            a < b
 
-            with pytest.raises(TypeError):
-                a > b
+        with pytest.raises(TypeError):
+            a > b
 
 
-class TestDetermineAttrsEqOrder(object):
+class TestDetermineAttrsEqOrder:
     def test_default(self):
         """
         If all are set to None, set both eq and order to the passed default.
@@ -1865,7 +1817,7 @@
             _determine_attrs_eq_order(cmp, eq, order, True)
 
 
-class TestDetermineAttribEqOrder(object):
+class TestDetermineAttribEqOrder:
     def test_default(self):
         """
         If all are set to None, set both eq and order to the passed default.
@@ -1953,7 +1905,7 @@
         """
 
         @attr.s
-        class A(object):
+        class A:
             pass
 
         if hasattr(A, "__qualname__"):
@@ -1964,24 +1916,14 @@
             assert expected == method.__doc__
 
 
-@pytest.mark.skipif(not PY2, reason="Needs to be only caught on Python 2.")
-def test_auto_detect_raises_on_py2():
-    """
-    Trying to pass auto_detect=True to attr.s raises PythonTooOldError.
-    """
-    with pytest.raises(PythonTooOldError):
-        attr.s(auto_detect=True)
-
-
-class BareC(object):
+class BareC:
     pass
 
 
-class BareSlottedC(object):
+class BareSlottedC:
     __slots__ = ()
 
 
-@pytest.mark.skipif(PY2, reason="Auto-detection is Python 3-only.")
 class TestAutoDetect:
     @pytest.mark.parametrize("C", (BareC, BareSlottedC))
     def test_determine_detects_non_presence_correctly(self, C):
@@ -2010,7 +1952,7 @@
         """
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
         i = C(1)
@@ -2033,7 +1975,7 @@
         """
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class CI(object):
+        class CI:
             x = attr.ib()
 
             def __init__(self):
@@ -2049,7 +1991,7 @@
         """
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __repr__(self):
@@ -2066,11 +2008,11 @@
         """
 
         @attr.s(slots=slots, frozen=frozen, hash=True)
-        class C(object):
+        class C:
             x = attr.ib(eq=str)
 
         @attr.s(slots=slots, frozen=frozen, hash=True)
-        class D(object):
+        class D:
             x = attr.ib()
 
         # These hashes should be the same because 1 is turned into
@@ -2086,7 +2028,7 @@
         """
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __hash__(self):
@@ -2102,7 +2044,7 @@
         """
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __eq__(self, o):
@@ -2112,7 +2054,7 @@
             C(1) == C(1)
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class D(object):
+        class D:
             x = attr.ib()
 
             def __ne__(self, o):
@@ -2148,19 +2090,19 @@
                 assert_not_set(cls, ex, "__" + m + "__")
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class LE(object):
+        class LE:
             __le__ = 42
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class LT(object):
+        class LT:
             __lt__ = 42
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class GE(object):
+        class GE:
             __ge__ = 42
 
         @attr.s(auto_detect=True, slots=slots, frozen=frozen)
-        class GT(object):
+        class GT:
             __gt__ = 42
 
         assert_none_set(LE, "__le__")
@@ -2176,7 +2118,7 @@
         """
 
         @attr.s(init=True, auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __init__(self):
@@ -2192,7 +2134,7 @@
         """
 
         @attr.s(repr=True, auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __repr__(self):
@@ -2208,7 +2150,7 @@
         """
 
         @attr.s(hash=True, auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __hash__(self):
@@ -2224,7 +2166,7 @@
         """
 
         @attr.s(eq=True, auto_detect=True, slots=slots, frozen=frozen)
-        class C(object):
+        class C:
             x = attr.ib()
 
             def __eq__(self, o):
@@ -2264,7 +2206,7 @@
             slots=slots,
             frozen=frozen,
         )
-        class C(object):
+        class C:
             x = attr.ib()
             __le__ = __lt__ = __gt__ = __ge__ = meth
 
@@ -2283,7 +2225,7 @@
         Ensure the order doesn't matter.
         """
 
-        class C(object):
+        class C:
             x = attr.ib()
             own_eq_called = attr.ib(default=False)
             own_le_called = attr.ib(default=False)
@@ -2329,14 +2271,14 @@
         """
 
         @attr.s(slots=slots, auto_detect=True)
-        class C(object):
+        class C:
             def __getstate__(self):
                 return ("hi",)
 
         assert None is getattr(C(), "__setstate__", None)
 
         @attr.s(slots=slots, auto_detect=True)
-        class C(object):
+        class C:
             called = attr.ib(False)
 
             def __setstate__(self, state):
@@ -2358,14 +2300,14 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a = attr.ib()
 
         assert None is getattr(C, "__match_args__", None)
 
 
 @pytest.mark.skipif(not PY310, reason="Structural pattern matching is 3.10+")
-class TestMatchArgs(object):
+class TestMatchArgs:
     """
     Tests for match_args and __match_args__ generation.
     """
diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml
index ca17b0a..f77984d 100644
--- a/tests/test_mypy.yml
+++ b/tests/test_mypy.yml
@@ -983,29 +983,6 @@
        a: int = attr.ib()  # E: Name "a" already defined on line 16
     reveal_type(C)  # N: Revealed type is "def (a: builtins.int, b: builtins.int) -> main.C"
 
-- case: testAttrsNewStyleClassPy2
-  mypy_config:
-    python_version = 2.7
-  main: |
-    import attr
-    @attr.s
-    class Good(object):
-        pass
-    @attr.s
-    class Bad:  # E: attrs only works with new-style classes
-        pass
-  skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47
-
-- case: testAttrsAutoAttribsPy2
-  mypy_config: |
-    python_version = 2.7
-  main: |
-    import attr
-    @attr.s(auto_attribs=True)  # E: auto_attribs is not supported in Python 2
-    class A(object):
-        x = attr.ib()
-  skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47
-
 - case: testAttrsFrozenSubclass
   main: |
     import attr
@@ -1218,19 +1195,6 @@
         a = attr.ib(kw_only=True)
         b = attr.ib(15)
 
-- case: testAttrsKwOnlyPy2
-  mypy_config:
-    python_version=2.7
-  main: |
-    import attr
-    @attr.s(kw_only=True)  # E: kw_only is not supported in Python 2
-    class A(object):
-        x = attr.ib()
-    @attr.s
-    class B(object):
-        x = attr.ib(kw_only=True)  # E: kw_only is not supported in Python 2
-  skip: True # https://github.com/typeddjango/pytest-mypy-plugins/issues/47
-
 - case: testAttrsDisallowUntypedWorksForward
   main: |
     # flags: --disallow-untyped-defs
diff --git a/tests/test_pattern_matching.py b/tests/test_pattern_matching.py
index 590804a..3855d6a 100644
--- a/tests/test_pattern_matching.py
+++ b/tests/test_pattern_matching.py
@@ -19,7 +19,7 @@
         """
 
         @dec
-        class C(object):
+        class C:
             a = attr.ib()
 
         assert ("a",) == C.__match_args__
diff --git a/tests/test_pyright.py b/tests/test_pyright.py
index c30dcc5..e055ebb 100644
--- a/tests/test_pyright.py
+++ b/tests/test_pyright.py
@@ -18,7 +18,7 @@
 
 
 @attr.s(frozen=True)
-class PyrightDiagnostic(object):
+class PyrightDiagnostic:
     severity = attr.ib()
     message = attr.ib()
 
@@ -36,10 +36,10 @@
     )
     pyright_result = json.loads(pyright.stdout)
 
-    diagnostics = set(
+    diagnostics = {
         PyrightDiagnostic(d["severity"], d["message"])
         for d in pyright_result["generalDiagnostics"]
-    )
+    }
 
     # Expected diagnostics as per pyright 1.1.135
     expected_diagnostics = {
diff --git a/tests/test_setattr.py b/tests/test_setattr.py
index aaedde5..38fcf34 100644
--- a/tests/test_setattr.py
+++ b/tests/test_setattr.py
@@ -1,6 +1,5 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import pickle
 
@@ -9,22 +8,21 @@
 import attr
 
 from attr import setters
-from attr._compat import PY2
 from attr.exceptions import FrozenAttributeError
 from attr.validators import instance_of, matches_re
 
 
 @attr.s(frozen=True)
-class Frozen(object):
+class Frozen:
     x = attr.ib()
 
 
 @attr.s
-class WithOnSetAttrHook(object):
+class WithOnSetAttrHook:
     x = attr.ib(on_setattr=lambda *args: None)
 
 
-class TestSetAttr(object):
+class TestSetAttr:
     def test_change(self):
         """
         The return value of a hook overwrites the value. But they are not run
@@ -35,7 +33,7 @@
             return "hooked!"
 
         @attr.s
-        class Hooked(object):
+        class Hooked:
             x = attr.ib(on_setattr=hook)
             y = attr.ib()
 
@@ -56,7 +54,7 @@
         """
 
         @attr.s
-        class PartiallyFrozen(object):
+        class PartiallyFrozen:
             x = attr.ib(on_setattr=setters.frozen)
             y = attr.ib()
 
@@ -81,7 +79,7 @@
         """
 
         @attr.s(on_setattr=on_setattr)
-        class ValidatedAttribute(object):
+        class ValidatedAttribute:
             x = attr.ib()
             y = attr.ib(validator=[instance_of(str), matches_re("foo.*qux")])
 
@@ -115,7 +113,7 @@
         s = [setters.convert, lambda _, __, nv: nv + 1]
 
         @attr.s
-        class Piped(object):
+        class Piped:
             x1 = attr.ib(converter=int, on_setattr=setters.pipe(*s))
             x2 = attr.ib(converter=int, on_setattr=s)
 
@@ -147,7 +145,7 @@
         """
 
         @attr.s(on_setattr=[setters.convert, setters.validate])
-        class C(object):
+        class C:
             x = attr.ib()
 
         c = C(1)
@@ -162,7 +160,7 @@
         """
 
         @attr.s(on_setattr=setters.validate)
-        class C(object):
+        class C:
             x = attr.ib(validator=attr.validators.instance_of(int))
 
         c = C(1)
@@ -187,7 +185,7 @@
         with pytest.raises(ValueError) as ei:
 
             @attr.s(frozen=True, on_setattr=setters.validate)
-            class C(object):
+            class C:
                 x = attr.ib()
 
         assert "Frozen classes can't use on_setattr." == ei.value.args[0]
@@ -200,7 +198,7 @@
         with pytest.raises(ValueError) as ei:
 
             @attr.s(frozen=True)
-            class C(object):
+            class C:
                 x = attr.ib(on_setattr=setters.validate)
 
         assert "Frozen classes can't use on_setattr." == ei.value.args[0]
@@ -218,16 +216,14 @@
             pytest.fail("Must not be called.")
 
         @attr.s
-        class Hooked(object):
+        class Hooked:
             x = attr.ib(on_setattr=boom)
 
         @attr.s(slots=slots)
         class NoHook(WithOnSetAttrHook):
             x = attr.ib()
 
-        if not PY2:
-            assert NoHook.__setattr__ == object.__setattr__
-
+        assert NoHook.__setattr__ == object.__setattr__
         assert 1 == NoHook(1).x
         assert Hooked.__attrs_own_setattr__
         assert not NoHook.__attrs_own_setattr__
@@ -240,7 +236,7 @@
         not reset it unless necessary.
         """
 
-        class A(object):
+        class A:
             """
             Not an attrs class on purpose to prevent accidental resets that
             would render the asserts meaningless.
@@ -288,7 +284,7 @@
         """
 
         @attr.s(slots=True)
-        class A(object):
+        class A:
             def __setattr__(self, key, value):
                 raise SystemError
 
@@ -306,7 +302,7 @@
         """
 
         @attr.s(slots=True)
-        class A(object):
+        class A:
             x = attr.ib(on_setattr=setters.frozen)
 
         class B(A):
@@ -318,13 +314,6 @@
 
         C(1).x = 2
 
-
-@pytest.mark.skipif(PY2, reason="Python 3-only.")
-class TestSetAttrNoPy2(object):
-    """
-    __setattr__ tests for Py3+ to avoid the skip repetition.
-    """
-
     @pytest.mark.parametrize("slots", [True, False])
     def test_setattr_auto_detect_if_no_custom_setattr(self, slots):
         """
@@ -385,7 +374,7 @@
         ):
 
             @attr.s(auto_detect=True, slots=slots)
-            class HookAndCustomSetAttr(object):
+            class HookAndCustomSetAttr:
                 x = attr.ib(on_setattr=lambda *args: None)
 
                 def __setattr__(self, _, __):
@@ -406,7 +395,7 @@
         """
 
         @attr.s(slots=a_slots)
-        class A(object):
+        class A:
             x = attr.ib(on_setattr=setters.frozen)
 
         @attr.s(slots=b_slots, auto_detect=True)
diff --git a/tests/test_slots.py b/tests/test_slots.py
index 91697a7..89e7e93 100644
--- a/tests/test_slots.py
+++ b/tests/test_slots.py
@@ -13,7 +13,7 @@
 
 import attr
 
-from attr._compat import PY2, PYPY, just_warn, make_set_closure_cell
+from attr._compat import PYPY, just_warn, make_set_closure_cell
 
 
 # Pympler doesn't work on PyPy.
@@ -26,7 +26,7 @@
 
 
 @attr.s
-class C1(object):
+class C1:
     x = attr.ib(validator=attr.validators.instance_of(int))
     y = attr.ib()
 
@@ -41,18 +41,16 @@
     def staticmethod():
         return "staticmethod"
 
-    if not PY2:
+    def my_class(self):
+        return __class__
 
-        def my_class(self):
-            return __class__
-
-        def my_super(self):
-            """Just to test out the no-arg super."""
-            return super().__repr__()
+    def my_super(self):
+        """Just to test out the no-arg super."""
+        return super().__repr__()
 
 
 @attr.s(slots=True, hash=True)
-class C1Slots(object):
+class C1Slots:
     x = attr.ib(validator=attr.validators.instance_of(int))
     y = attr.ib()
 
@@ -67,14 +65,12 @@
     def staticmethod():
         return "staticmethod"
 
-    if not PY2:
+    def my_class(self):
+        return __class__
 
-        def my_class(self):
-            return __class__
-
-        def my_super(self):
-            """Just to test out the no-arg super."""
-            return super().__repr__()
+    def my_super(self):
+        """Just to test out the no-arg super."""
+        return super().__repr__()
 
 
 def test_slots_being_used():
@@ -90,7 +86,7 @@
     assert "__dict__" in dir(non_slot_instance)
     assert "__slots__" not in dir(non_slot_instance)
 
-    assert set(["__weakref__", "x", "y"]) == set(slot_instance.__slots__)
+    assert {"__weakref__", "x", "y"} == set(slot_instance.__slots__)
 
     if has_pympler:
         assert asizeof(slot_instance) < asizeof(non_slot_instance)
@@ -154,7 +150,7 @@
     assert "clsmethod" == c2.classmethod()
     assert "staticmethod" == c2.staticmethod()
 
-    assert set(["z"]) == set(C2Slots.__slots__)
+    assert {"z"} == set(C2Slots.__slots__)
 
     c3 = C2Slots(x=1, y=3, z="test")
 
@@ -178,7 +174,7 @@
     This will actually *replace* the class with another one, using slots.
     """
 
-    class SimpleOrdinaryClass(object):
+    class SimpleOrdinaryClass:
         def __init__(self, x, y, z):
             self.x = x
             self.y = y
@@ -213,7 +209,7 @@
     assert "clsmethod" == c2.classmethod()
     assert "staticmethod" == c2.staticmethod()
 
-    assert set(["__weakref__", "x", "y", "z"]) == set(C2Slots.__slots__)
+    assert {"__weakref__", "x", "y", "z"} == set(C2Slots.__slots__)
 
     c3 = C2Slots(x=1, y=3, z="test")
     assert c3 > c2
@@ -245,7 +241,7 @@
     assert 2 == c2.y
     assert "test" == c2.z
 
-    assert set(["z"]) == set(C2Slots.__slots__)
+    assert {"z"} == set(C2Slots.__slots__)
 
     assert 1 == c2.method()
     assert "clsmethod" == c2.classmethod()
@@ -275,7 +271,7 @@
     Inheriting from a slotted class doesn't re-create existing slots
     """
 
-    class HasXSlot(object):
+    class HasXSlot:
         __slots__ = ("x",)
 
     @attr.s(slots=True, hash=True)
@@ -311,7 +307,7 @@
     We reuse slot descriptor for an attr.ib defined in a slotted attr.s
     """
 
-    class HasXSlot(object):
+    class HasXSlot:
         __slots__ = ("x",)
 
     class OverridesX(HasXSlot):
@@ -342,7 +338,7 @@
     @attr.s(
         init=False, eq=False, order=False, hash=False, repr=False, slots=True
     )
-    class C1BareSlots(object):
+    class C1BareSlots:
         x = attr.ib(validator=attr.validators.instance_of(int))
         y = attr.ib()
 
@@ -358,7 +354,7 @@
             return "staticmethod"
 
     @attr.s(init=False, eq=False, order=False, hash=False, repr=False)
-    class C1Bare(object):
+    class C1Bare:
         x = attr.ib(validator=attr.validators.instance_of(int))
         y = attr.ib()
 
@@ -409,8 +405,7 @@
     assert {"x": 1, "y": 2, "z": "test"} == attr.asdict(c2)
 
 
-@pytest.mark.skipif(PY2, reason="closure cell rewriting is PY3-only.")
-class TestClosureCellRewriting(object):
+class TestClosureCellRewriting:
     def test_closure_cell_rewriting(self):
         """
         Slotted classes support proper closure cell rewriting.
@@ -520,7 +515,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=False)
-    class C(object):
+    class C:
         pass
 
     c = C()
@@ -538,7 +533,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=False)
-    class C(object):
+    class C:
         pass
 
     c = C()
@@ -553,7 +548,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=True)
-    class C(object):
+    class C:
         pass
 
     c = C()
@@ -568,7 +563,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=True)
-    class C(object):
+    class C:
         field = attr.ib()
 
     assert [f.name for f in attr.fields(C)] == ["field"]
@@ -581,7 +576,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=True)
-    class C(object):
+    class C:
         pass
 
     @attr.s(slots=True, weakref_slot=True)
@@ -601,7 +596,7 @@
     """
 
     @attr.s(slots=True, weakref_slot=True)
-    class C(object):
+    class C:
         __weakref__ = attr.ib(
             init=False, hash=False, repr=False, eq=False, order=False
         )
@@ -628,7 +623,7 @@
     """
 
     @attr.s(slots=True)
-    class C(object):
+    class C:
         field = attr.ib()
 
         def f(self, a):
@@ -638,16 +633,16 @@
 
 
 @attr.s(getstate_setstate=True)
-class C2(object):
+class C2:
     x = attr.ib()
 
 
 @attr.s(slots=True, getstate_setstate=True)
-class C2Slots(object):
+class C2Slots:
     x = attr.ib()
 
 
-class TestPickle(object):
+class TestPickle:
     @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL))
     def test_pickleable_by_default(self, protocol):
         """
@@ -676,7 +671,7 @@
         """
 
         @attr.s(slots=True, getstate_setstate=False)
-        class C(object):
+        class C:
             x = attr.ib()
 
         i = C(42)
@@ -699,7 +694,7 @@
     """
 
     @attr.s(slots=True)
-    class A(object):
+    class A:
         x = attr.ib()
 
         @property
@@ -710,20 +705,19 @@
     class B(A):
         @property
         def f(self):
-            return super(B, self).f ** 2
+            return super().f ** 2
 
     assert B(11).f == 121
     assert B(17).f == 289
 
 
-@pytest.mark.skipif(PY2, reason="shortcut super() is PY3-only.")
 def test_slots_super_property_get_shortcut():
     """
     On Python 3, the `super()` shortcut is allowed.
     """
 
     @attr.s(slots=True)
-    class A(object):
+    class A:
         x = attr.ib()
 
         @property
diff --git a/tests/test_validators.py b/tests/test_validators.py
index fce774b..633f235 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -4,7 +4,6 @@
 Tests for `attr.validators`.
 """
 
-from __future__ import absolute_import, division, print_function
 
 import re
 
@@ -14,7 +13,7 @@
 
 from attr import _config, fields, has
 from attr import validators as validator_module
-from attr._compat import PY2, TYPE
+from attr._compat import TYPE
 from attr.validators import (
     and_,
     deep_iterable,
@@ -49,7 +48,7 @@
     return zope.interface
 
 
-class TestDisableValidators(object):
+class TestDisableValidators:
     @pytest.fixture(autouse=True)
     def reset_default(self):
         """
@@ -109,7 +108,7 @@
         assert _config._run_validators is True
 
 
-class TestInstanceOf(object):
+class TestInstanceOf:
     """
     Tests for `instance_of`.
     """
@@ -161,7 +160,7 @@
         ) == repr(v)
 
 
-class TestMatchesRe(object):
+class TestMatchesRe:
     """
     Tests for `matches_re`.
     """
@@ -178,7 +177,7 @@
         """
 
         @attr.s
-        class ReTester(object):
+        class ReTester:
             str_match = attr.ib(validator=matches_re("a|ab"))
 
         ReTester("ab")  # shouldn't raise exceptions
@@ -195,7 +194,7 @@
         """
 
         @attr.s
-        class MatchTester(object):
+        class MatchTester:
             val = attr.ib(validator=matches_re("a", re.IGNORECASE, re.match))
 
         MatchTester("A1")  # test flags and using re.match
@@ -207,7 +206,7 @@
         pattern = re.compile("a")
 
         @attr.s
-        class RePatternTester(object):
+        class RePatternTester:
             val = attr.ib(validator=matches_re(pattern))
 
         RePatternTester("a")
@@ -229,7 +228,7 @@
         """
 
         @attr.s
-        class SearchTester(object):
+        class SearchTester:
             val = attr.ib(validator=matches_re("a", 0, re.search))
 
         SearchTester("bab")  # re.search will match
@@ -241,16 +240,10 @@
         with pytest.raises(ValueError) as ei:
             matches_re("a", 0, lambda: None)
 
-        if not PY2:
-            assert (
-                "'func' must be one of None, fullmatch, match, search."
-                == ei.value.args[0]
-            )
-        else:
-            assert (
-                "'func' must be one of None, match, search."
-                == ei.value.args[0]
-            )
+        assert (
+            "'func' must be one of None, fullmatch, match, search."
+            == ei.value.args[0]
+        )
 
     @pytest.mark.parametrize(
         "func", [None, getattr(re, "fullmatch", None), re.match, re.search]
@@ -283,7 +276,7 @@
     0 / 0
 
 
-class TestAnd(object):
+class TestAnd:
     def test_in_all(self):
         """
         Verify that this validator is in ``__all__``.
@@ -313,7 +306,7 @@
         """
 
         @attr.s
-        class C(object):
+        class C:
             a1 = attr.ib("a1", validator=and_(instance_of(int)))
             a2 = attr.ib("a2", validator=[instance_of(int)])
 
@@ -337,7 +330,7 @@
     return IFoo
 
 
-class TestProvides(object):
+class TestProvides:
     """
     Tests for `provides`.
     """
@@ -354,7 +347,7 @@
         """
 
         @zope_interface.implementer(ifoo)
-        class C(object):
+        class C:
             def f(self):
                 pass
 
@@ -395,7 +388,7 @@
 @pytest.mark.parametrize(
     "validator", [instance_of(int), [always_pass, instance_of(int)]]
 )
-class TestOptional(object):
+class TestOptional:
     """
     Tests for `optional`.
     """
@@ -456,7 +449,7 @@
         assert repr_s == repr(v)
 
 
-class TestIn_(object):
+class TestIn_:
     """
     Tests for `in_`.
     """
@@ -501,7 +494,7 @@
         Returned validator has a useful `__repr__`.
         """
         v = in_([3, 4, 5])
-        assert (("<in_ validator with options [3, 4, 5]>")) == repr(v)
+        assert ("<in_ validator with options [3, 4, 5]>") == repr(v)
 
 
 @pytest.fixture(
@@ -520,7 +513,7 @@
     return request.param
 
 
-class TestDeepIterable(object):
+class TestDeepIterable:
     """
     Tests for `deep_iterable`.
     """
@@ -685,7 +678,7 @@
         assert expected_repr == repr(v)
 
 
-class TestDeepMapping(object):
+class TestDeepMapping:
     """
     Tests for `deep_mapping`.
     """
@@ -789,7 +782,7 @@
         assert expected_repr == repr(v)
 
 
-class TestIsCallable(object):
+class TestIsCallable:
     """
     Tests for `is_callable`.
     """
@@ -879,7 +872,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=v(self.BOUND))
 
         assert fields(Tester).value.validator.bound == self.BOUND
@@ -899,7 +892,7 @@
         """Silent if value {op} bound."""
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=v(self.BOUND))
 
         Tester(value)  # shouldn't raise exceptions
@@ -917,7 +910,7 @@
         """Raise ValueError if value {op} bound."""
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=v(self.BOUND))
 
         with pytest.raises(ValueError):
@@ -953,7 +946,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=max_len(self.MAX_LENGTH))
 
         assert fields(Tester).value.validator.max_length == self.MAX_LENGTH
@@ -976,7 +969,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=max_len(self.MAX_LENGTH))
 
         Tester(value)  # shouldn't raise exceptions
@@ -994,7 +987,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=max_len(self.MAX_LENGTH))
 
         with pytest.raises(ValueError):
@@ -1026,7 +1019,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=min_len(self.MIN_LENGTH))
 
         assert fields(Tester).value.validator.min_length == self.MIN_LENGTH
@@ -1047,7 +1040,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=min_len(self.MIN_LENGTH))
 
         Tester(value)  # shouldn't raise exceptions
@@ -1065,7 +1058,7 @@
         """
 
         @attr.s
-        class Tester(object):
+        class Tester:
             value = attr.ib(validator=min_len(self.MIN_LENGTH))
 
         with pytest.raises(ValueError):
diff --git a/tests/test_version_info.py b/tests/test_version_info.py
index 41f75f4..5bd101b 100644
--- a/tests/test_version_info.py
+++ b/tests/test_version_info.py
@@ -1,11 +1,9 @@
 # SPDX-License-Identifier: MIT
 
-from __future__ import absolute_import, division, print_function
 
 import pytest
 
 from attr import VersionInfo
-from attr._compat import PY2
 
 
 @pytest.fixture(name="vi")
@@ -29,9 +27,6 @@
             == VersionInfo._from_version_string("19.2.0.dev0").releaselevel
         )
 
-    @pytest.mark.skipif(
-        PY2, reason="Python 2 is too YOLO to care about comparability."
-    )
     @pytest.mark.parametrize("other", [(), (19, 2, 0, "final", "garbage")])
     def test_wrong_len(self, vi, other):
         """
diff --git a/tests/utils.py b/tests/utils.py
index a2fefbd..3d10621 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,7 +4,6 @@
 Common helper functions for tests.
 """
 
-from __future__ import absolute_import, division, print_function
 
 from attr import Attribute
 from attr._make import NOTHING, make_class
@@ -68,7 +67,7 @@
     )
 
 
-class TestSimpleClass(object):
+class TestSimpleClass:
     """
     Tests for the testing helper function `make_class`.
     """
diff --git a/tox.ini b/tox.ini
index 48e969b..c5dc2ff 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,19 +10,17 @@
 # Keep docs in sync with docs env and .readthedocs.yml.
 [gh-actions]
 python =
-    2.7: py27
     3.5: py35
     3.6: py36
     3.7: py37
     3.8: py38, changelog
     3.9: py39, pyright
     3.10: py310, manifest, typing, docs
-    pypy-2: pypy
     pypy-3: pypy3
 
 
 [tox]
-envlist = typing,pre-commit,py27,py35,py36,py37,py38,py39,py310,pypy,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
+envlist = typing,pre-commit,py35,py36,py37,py38,py39,py310,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
 isolated_build = True
 
 
@@ -41,7 +39,7 @@
 commands = python -m pytest {posargs}
 
 
-[testenv:py27]
+[testenv:py35]
 extras = tests
 commands = coverage run -m pytest {posargs}
 
@@ -64,7 +62,7 @@
 
 [testenv:coverage-report]
 basepython = python3.10
-depends = py27,py37,py310
+depends = py35,py37,py310
 skip_install = true
 deps = coverage[toml]>=5.4
 commands =