diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 27bbcb7634..cdf296748a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,6 +31,9 @@ repos:
   hooks:
   - id: codespell
     exclude: locale|kickstarter-announcement.md|coreapi-0.1.1.js
+    additional_dependencies:
+      # python doesn't come with a toml parser prior to 3.11
+      - "tomli; python_version < '3.11'"
 
 - repo: https://github.com/asottile/pyupgrade
   rev: v3.19.1
diff --git a/docs/community/project-management.md b/docs/community/project-management.md
index daf2cda8d8..85ea87a846 100644
--- a/docs/community/project-management.md
+++ b/docs/community/project-management.md
@@ -59,8 +59,8 @@ The following template should be used for the description of the issue, and serv
 
     - [ ] Create pull request for [release notes](https://github.com/encode/django-rest-framework/blob/master/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/encode/django-rest-framework/milestones/***).
     - [ ] Update supported versions:
-        - [ ] `setup.py` `python_requires` list
-        - [ ] `setup.py` Python & Django version trove classifiers
+        - [ ] `pyproject.toml` `python_requires` list
+        - [ ] `pyproject.toml` Python & Django version trove classifiers
         - [ ] `README` Python & Django versions
         - [ ] `docs` Python & Django versions
     - [ ] Update the translations from [transifex](https://www.django-rest-framework.org/topics/project-management/#translations).
@@ -71,7 +71,9 @@ The following template should be used for the description of the issue, and serv
     - [ ] Confirm with @tomchristie that release is finalized and ready to go.
     - [ ] Ensure that release date is included in pull request.
     - [ ] Merge the release pull request.
-    - [ ] Push the package to PyPI with `./setup.py publish`.
+    - [ ] Install the release tools: `pip install build twine`
+    - [ ] Build the package: `python -m build`
+    - [ ] Push the package to PyPI with `twine upload dist/*`
     - [ ] Tag the release, with `git tag -a *.*.* -m 'version *.*.*'; git push --tags`.
     - [ ] Deploy the documentation with `mkdocs gh-deploy`.
     - [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework).
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000..3909f25b9a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,79 @@
+[build-system]
+requires = ["setuptools>=61.2"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "djangorestframework"
+readme = "README.md"
+authors = [{name = "Tom Christie", email = "tom@tomchristie.com"}]
+license = {text = "BSD"}
+description = "Web APIs for Django, made easy."
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Environment :: Web Environment",
+    "Framework :: Django",
+    "Framework :: Django :: 4.2",
+    "Framework :: Django :: 5.0",
+    "Framework :: Django :: 5.1",
+    "Framework :: Django :: 5.2",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3 :: Only",
+    "Topic :: Internet :: WWW/HTTP",
+]
+requires-python = ">=3.9"
+dependencies = ["django>=4.2"]
+dynamic = ["version"]
+
+[tool.setuptools.dynamic]
+version = {attr = "rest_framework.__version__"}
+
+[project.urls]
+Homepage = "https://www.django-rest-framework.org"
+Funding = "https://fund.django-rest-framework.org/topics/funding/"
+Source = "https://github.com/encode/django-rest-framework"
+Changelog = "https://www.django-rest-framework.org/community/release-notes/"
+
+[tool.setuptools]
+
+[tool.setuptools.packages.find]
+include = ["rest_framework*"]
+
+[tool.pytest.ini_options]
+addopts = "--tb=short --strict-markers -ra"
+testpaths = ["tests"]
+filterwarnings = ["ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning"]
+
+[tool.isort]
+skip = [".tox"]
+atomic = true
+multi_line_output = 5
+extra_standard_library = ["types"]
+known_third_party = ["pytest", "_pytest", "django", "pytz", "uritemplate"]
+known_first_party = ["rest_framework", "tests"]
+
+[tool.coverage.run]
+# NOTE: source is ignored with pytest-cov (but uses the same).
+source = ["."]
+include = ["rest_framework/*", "tests/*"]
+branch = true
+
+[tool.coverage.report]
+include = ["rest_framework/*", "tests/*"]
+exclude_lines = [
+    "pragma: no cover",
+    "raise NotImplementedError",
+]
+
+[tool.codespell]
+# Ref: https://github.com/codespell-project/codespell#using-a-config-file
+skip = "*/kickstarter-announcement.md,*.js,*.map,*.po"
+ignore-words-list = "fo,malcom,ser"
diff --git a/setup.cfg b/setup.cfg
index 4592388360..aebdf53629 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,36 +1,3 @@
-[metadata]
-license_files = LICENSE.md
-
-[tool:pytest]
-addopts=--tb=short --strict-markers -ra
-testpaths = tests
-filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning
-
 [flake8]
 ignore = E501,W503,W504
 banned-modules = json = use from rest_framework.utils import json!
-
-[isort]
-skip=.tox
-atomic=true
-multi_line_output=5
-extra_standard_library=types
-known_third_party=pytest,_pytest,django,pytz,uritemplate
-known_first_party=rest_framework,tests
-
-[coverage:run]
-# NOTE: source is ignored with pytest-cov (but uses the same).
-source = .
-include = rest_framework/*,tests/*
-branch = 1
-
-[coverage:report]
-include = rest_framework/*,tests/*
-exclude_lines =
-    pragma: no cover
-    raise NotImplementedError
-
-[codespell]
-# Ref: https://github.com/codespell-project/codespell#using-a-config-file
-skip = */kickstarter-announcement.md,*.js,*.map,*.po
-ignore-words-list = fo,malcom,ser
diff --git a/setup.py b/setup.py
old mode 100755
new mode 100644
index e5e78c2c78..606849326a
--- a/setup.py
+++ b/setup.py
@@ -1,119 +1,3 @@
-import os
-import re
-import shutil
-import sys
+from setuptools import setup
 
-from setuptools import find_packages, setup
-
-CURRENT_PYTHON = sys.version_info[:2]
-REQUIRED_PYTHON = (3, 9)
-
-# This check and everything above must remain compatible with Python 2.7.
-if CURRENT_PYTHON < REQUIRED_PYTHON:
-    sys.stderr.write("""
-==========================
-Unsupported Python version
-==========================
-
-This version of Django REST Framework requires Python {}.{}, but you're trying
-to install it on Python {}.{}.
-
-This may be because you are using a version of pip that doesn't
-understand the python_requires classifier. Make sure you
-have pip >= 9.0 and setuptools >= 24.2, then try again:
-
-    $ python -m pip install --upgrade pip setuptools
-    $ python -m pip install djangorestframework
-
-This will install the latest version of Django REST Framework which works on
-your version of Python. If you can't upgrade your pip (or Python), request
-an older version of Django REST Framework:
-
-    $ python -m pip install "djangorestframework<3.10"
-""".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
-    sys.exit(1)
-
-
-def read(f):
-    with open(f, encoding='utf-8') as file:
-        return file.read()
-
-
-def get_version(package):
-    """
-    Return package version as listed in `__version__` in `init.py`.
-    """
-    init_py = open(os.path.join(package, '__init__.py')).read()
-    return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
-
-
-version = get_version('rest_framework')
-
-
-if sys.argv[-1] == 'publish':
-    if os.system("pip freeze | grep twine"):
-        print("twine not installed.\nUse `pip install twine`.\nExiting.")
-        sys.exit()
-    os.system("python setup.py sdist bdist_wheel")
-    if os.system("twine check dist/*"):
-        print("twine check failed. Packages might be outdated.")
-        print("Try using `pip install -U twine wheel`.\nExiting.")
-        sys.exit()
-    os.system("twine upload dist/*")
-    print("You probably want to also tag the version now:")
-    print("  git tag -a %s -m 'version %s'" % (version, version))
-    print("  git push --tags")
-    shutil.rmtree('dist')
-    shutil.rmtree('build')
-    shutil.rmtree('djangorestframework.egg-info')
-    sys.exit()
-
-
-setup(
-    name='djangorestframework',
-    version=version,
-    url='https://www.django-rest-framework.org/',
-    license='BSD',
-    description='Web APIs for Django, made easy.',
-    long_description=read('README.md'),
-    long_description_content_type='text/markdown',
-    author='Tom Christie',
-    author_email='tom@tomchristie.com',  # SEE NOTE BELOW (*)
-    packages=find_packages(exclude=['tests*']),
-    include_package_data=True,
-    install_requires=["django>=4.2"],
-    python_requires=">=3.9",
-    zip_safe=False,
-    classifiers=[
-        'Development Status :: 5 - Production/Stable',
-        'Environment :: Web Environment',
-        'Framework :: Django',
-        'Framework :: Django :: 4.2',
-        'Framework :: Django :: 5.0',
-        'Framework :: Django :: 5.1',
-        'Framework :: Django :: 5.2',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: BSD License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3.11',
-        'Programming Language :: Python :: 3.12',
-        'Programming Language :: Python :: 3.13',
-        'Programming Language :: Python :: 3 :: Only',
-        'Topic :: Internet :: WWW/HTTP',
-    ],
-    project_urls={
-        'Funding': 'https://fund.django-rest-framework.org/topics/funding/',
-        'Source': 'https://github.com/encode/django-rest-framework',
-        'Changelog': 'https://www.django-rest-framework.org/community/release-notes/',
-    },
-)
-
-# (*) Please direct queries to the discussion group, rather than to me directly
-#     Doing so helps ensure your question is helpful to other users.
-#     Queries directly to my email are likely to receive a canned response.
-#
-#     Many thanks for your understanding.
+setup()