diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 09bfcde0..c81e4ba6 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -3,9 +3,9 @@ current_version = 4.0.0
 commit = True
 tag = True
 
-[bumpversion:file:setup.py]
-search = version='{current_version}'
-replace = version='{new_version}'
+[bumpversion:file:pyproject.toml]
+search = version = "{current_version}"
+replace = version = "{new_version}"
 
 [bumpversion:file (badge):README.rst]
 search = /v{current_version}.svg
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a85a1e03..03076700 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -60,24 +60,6 @@ jobs:
             toxpython: 'python3.9'
             tox_env: 'docs'
             os: 'ubuntu-latest'
-          - name: 'py36-pytest70-xdist250-coverage62 (ubuntu)'
-            python: '3.6'
-            toxpython: 'python3.6'
-            python_arch: 'x64'
-            tox_env: 'py36-pytest70-xdist250-coverage62'
-            os: 'ubuntu-latest'
-          - name: 'py36-pytest70-xdist250-coverage62 (windows)'
-            python: '3.6'
-            toxpython: 'python3.6'
-            python_arch: 'x64'
-            tox_env: 'py36-pytest70-xdist250-coverage62'
-            os: 'windows-latest'
-          - name: 'py36-pytest70-xdist250-coverage62 (macos)'
-            python: '3.6'
-            toxpython: 'python3.6'
-            python_arch: 'x64'
-            tox_env: 'py36-pytest70-xdist250-coverage62'
-            os: 'macos-latest'
           - name: 'py37-pytest71-xdist250-coverage64 (ubuntu)'
             python: '3.7'
             toxpython: 'python3.7'
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index cbb88f74..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,28 +0,0 @@
-graft docs
-graft examples
-prune examples/*/.tox
-prune examples/*/htmlcov
-prune examples/*/*/htmlcov
-prune examples/adhoc-layout/*.egg-info
-prune examples/src-layout/src/*.egg-info
-
-graft .github/workflows
-graft src
-graft ci
-graft tests
-
-include .bumpversion.cfg
-include .cookiecutterrc
-include .coveragerc
-include .editorconfig
-include tox.ini
-include .readthedocs.yml
-include .pre-commit-config.yaml
-include AUTHORS.rst
-include CHANGELOG.rst
-include CONTRIBUTING.rst
-include LICENSE
-include README.rst
-
-
-global-exclude *.py[cod] __pycache__/* *.so *.dylib
diff --git a/docs/releasing.rst b/docs/releasing.rst
index ae78228d..89c4aa8e 100644
--- a/docs/releasing.rst
+++ b/docs/releasing.rst
@@ -21,12 +21,12 @@ The process for releasing should follow these steps:
 #. Make sure you have a clean checkout, run ``git status`` to verify.
 #. Manually clean temporary files (that are ignored and won't show up in ``git status``)::
 
-        rm -rf dist build src/*.egg-info
+        rm -rf dist
 
    These files need to be removed to force distutils/setuptools to rebuild everything and recreate the egg-info metadata.
 #. Build the dists::
 
-        python3 setup.py clean --all sdist bdist_wheel
+        python -m build
 
 #. Verify that the resulting archives (found in ``dist/``) are good.
 #. Upload the sdist and wheel with twine::
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..afdc8b68
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,76 @@
+[build-system]
+requires = ["hatchling", "hatch-fancy-pypi-readme"]
+build-backend = "hatchling.build"
+
+[project]
+name = "pytest-cov"
+dynamic = ["readme"]
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+license = "MIT"
+requires-python = ">=3.7"
+authors = [
+  { name = "Marc Schlaich", email = "marc.schlaich@gmail.com" },
+]
+keywords = [
+  "cover",
+  "coverage",
+  "distributed",
+  "parallel",
+  "py.test",
+  "pytest",
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Framework :: Pytest",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: Microsoft :: Windows",
+  "Operating System :: POSIX",
+  "Operating System :: Unix",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.7",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: Implementation :: CPython",
+  "Programming Language :: Python :: Implementation :: PyPy",
+  "Topic :: Software Development :: Testing",
+  "Topic :: Utilities",
+]
+dependencies = [
+  "coverage[toml]>=5.2.1",
+  "pytest>=4.6",
+]
+
+[project.optional-dependencies]
+testing = [
+  "fields",
+  "hunter",
+  "process-tests",
+  "pytest-xdist",
+  "six",
+  "virtualenv",
+]
+
+[project.entry-points.pytest11]
+pytest_cov = "pytest_cov.plugin"
+
+[project.urls]
+Changelog = "https://pytest-cov.readthedocs.io/en/latest/changelog.html"
+Documentation = "https://pytest-cov.readthedocs.io/"
+Homepage = "https://github.com/pytest-dev/pytest-cov"
+"Issue Tracker" = "https://github.com/pytest-dev/pytest-cov/issues"
+
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/x-rst"
+fragments = [
+  { path = "README.rst" },
+  { path = "CHANGELOG.rst" },
+]
+
+[tool.hatch.build.targets.wheel.hooks.autorun]
+dependencies = ["hatch-autorun"]
+file = "src/pytest-cov.embed"
diff --git a/setup.py b/setup.py
deleted file mode 100755
index 799d4699..00000000
--- a/setup.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python
-
-import re
-from glob import glob
-from itertools import chain
-from os.path import basename
-from os.path import dirname
-from os.path import join
-from os.path import splitext
-
-from setuptools import Command
-from setuptools import find_packages
-from setuptools import setup
-
-try:
-    # https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html
-    from setuptools.command.build import build
-except ImportError:
-    from distutils.command.build import build
-
-from setuptools.command.develop import develop
-from setuptools.command.easy_install import easy_install
-from setuptools.command.install_lib import install_lib
-
-
-def read(*names, **kwargs):
-    with open(
-        join(dirname(__file__), *names),
-        encoding=kwargs.get('encoding', 'utf8')
-    ) as fh:
-        return fh.read()
-
-
-class BuildWithPTH(build):
-    def run(self, *args, **kwargs):
-        super().run(*args, **kwargs)
-        path = join(dirname(__file__), 'src', 'pytest-cov.pth')
-        dest = join(self.build_lib, basename(path))
-        self.copy_file(path, dest)
-
-
-class EasyInstallWithPTH(easy_install):
-    def run(self, *args, **kwargs):
-        super().run(*args, **kwargs)
-        path = join(dirname(__file__), 'src', 'pytest-cov.pth')
-        dest = join(self.install_dir, basename(path))
-        self.copy_file(path, dest)
-
-
-class InstallLibWithPTH(install_lib):
-    def run(self, *args, **kwargs):
-        super().run(*args, **kwargs)
-        path = join(dirname(__file__), 'src', 'pytest-cov.pth')
-        dest = join(self.install_dir, basename(path))
-        self.copy_file(path, dest)
-        self.outputs = [dest]
-
-    def get_outputs(self):
-        return chain(super().get_outputs(), self.outputs)
-
-
-class DevelopWithPTH(develop):
-    def run(self, *args, **kwargs):
-        super().run(*args, **kwargs)
-        path = join(dirname(__file__), 'src', 'pytest-cov.pth')
-        dest = join(self.install_dir, basename(path))
-        self.copy_file(path, dest)
-
-
-class GeneratePTH(Command):
-    user_options = []
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        with open(join(dirname(__file__), 'src', 'pytest-cov.pth'), 'w') as fh:
-            with open(join(dirname(__file__), 'src', 'pytest-cov.embed')) as sh:
-                fh.write(
-                    'import os, sys;'
-                    'exec(%r)' % sh.read().replace('    ', ' ')
-                )
-
-
-setup(
-    name='pytest-cov',
-    version='4.0.0',
-    license='MIT',
-    description='Pytest plugin for measuring coverage.',
-    long_description='{}\n{}'.format(read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))),
-    author='Marc Schlaich',
-    author_email='marc.schlaich@gmail.com',
-    url='https://github.com/pytest-dev/pytest-cov',
-    packages=find_packages('src'),
-    package_dir={'': 'src'},
-    py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')],
-    include_package_data=True,
-    zip_safe=False,
-    classifiers=[
-        # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers
-        'Development Status :: 5 - Production/Stable',
-        'Framework :: Pytest',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: Microsoft :: Windows',
-        'Operating System :: POSIX',
-        'Operating System :: Unix',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3 :: Only',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: Implementation :: CPython',
-        'Programming Language :: Python :: Implementation :: PyPy',
-        'Topic :: Software Development :: Testing',
-        'Topic :: Utilities',
-    ],
-    project_urls={
-        'Documentation': 'https://pytest-cov.readthedocs.io/',
-        'Changelog': 'https://pytest-cov.readthedocs.io/en/latest/changelog.html',
-        'Issue Tracker': 'https://github.com/pytest-dev/pytest-cov/issues',
-    },
-    keywords=[
-        'cover', 'coverage', 'pytest', 'py.test', 'distributed', 'parallel',
-    ],
-    install_requires=[
-        'pytest>=4.6',
-        'coverage[toml]>=5.2.1'
-    ],
-    python_requires='>=3.6',
-    extras_require={
-        'testing': [
-            'fields',
-            'hunter',
-            'process-tests',
-            'six',
-            'pytest-xdist',
-            'virtualenv',
-        ]
-    },
-    entry_points={
-        'pytest11': [
-            'pytest_cov = pytest_cov.plugin',
-        ],
-    },
-    cmdclass={
-        'build': BuildWithPTH,
-        'easy_install': EasyInstallWithPTH,
-        'install_lib': InstallLibWithPTH,
-        'develop': DevelopWithPTH,
-        'genpth': GeneratePTH,
-    },
-)
diff --git a/src/pytest-cov.pth b/src/pytest-cov.pth
deleted file mode 100644
index 8ed1a516..00000000
--- a/src/pytest-cov.pth
+++ /dev/null
@@ -1 +0,0 @@
-import os, sys;exec('if \'COV_CORE_SOURCE\' in os.environ:\n try:\n  from pytest_cov.embed import init\n  init()\n except Exception as exc:\n  sys.stderr.write(\n   "pytest-cov: Failed to setup subprocess coverage. "\n   "Environ: {0!r} "\n   "Exception: {1!r}\\n".format(\n    dict((k, v) for k, v in os.environ.items() if k.startswith(\'COV_CORE\')),\n    exc\n   )\n  )\n')
diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py
index 84fe42ba..492c232e 100644
--- a/tests/test_pytest_cov.py
+++ b/tests/test_pytest_cov.py
@@ -1777,8 +1777,8 @@ def test_append_coverage_subprocess(testdir):
 
 
 def test_pth_failure(monkeypatch):
-    with open('src/pytest-cov.pth') as fh:
-        payload = fh.read()
+    with open('src/pytest-cov.embed') as fh:
+        payload = f'import os, sys;exec({fh.read()!r})'
 
     class SpecificError(Exception):
         pass
diff --git a/tox.ini b/tox.ini
index 282cd244..9264a0eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,9 +10,9 @@ passenv =
 ; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist
 
 [tox]
+isolated_build = true
 envlist =
     check
-    py{36}-pytest{70}-xdist250-coverage{62}
     py{37,38,39,310,py37,py38}-pytest{71}-xdist250-coverage{64}
     docs
 
@@ -87,8 +87,9 @@ commands =
 
 [testenv:check]
 deps =
+    build
+    twine
     docutils
-    check-manifest
     flake8
     readme-renderer
     pygments
@@ -96,7 +97,7 @@ deps =
 skip_install = true
 usedevelop = false
 commands =
-    python setup.py check --strict --metadata --restructuredtext
-    check-manifest {toxinidir}
-    flake8 src tests setup.py
-    isort --check-only --diff src tests setup.py
+    python -m build
+    twine check dist/*
+    flake8 src tests
+    isort --check-only --diff src tests