diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 1280a01..01784ec 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -7,34 +7,33 @@ on:
     branches: [main]
 
 jobs:
-  build:
-
+  lint:
     runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Python 3.8
+        uses: actions/setup-python@v4
+        with:
+          python-version: 3.8
+      - name: Install Hatch
+        run: pipx install hatch
+      - name: Lint
+        run: hatch run dev:lint
+      - name: Type Check
+        run: hatch run dev:typecheck
+  tests:
+    runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
-
+        os: [ubuntu-latest, windows-latest]
     steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install .[tests]
-    - name: Lint with flake8
-      run: |
-        # stop the build if there are Python syntax errors or undefined names
-        flake8 delphin --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. PEP-8 says 99 chars is a good width
-        flake8 delphin --count --exit-zero --extend-ignore E221 --max-line-length=99 --statistics
-        # E221 = whitespace before operator
-        # disabled for now: --max-complexity=10
-    - name: Test with pytest
-      run: |
-        pytest .
-    - name: Type-check with mypy
-      run: |
-        mypy delphin --namespace-packages --explicit-package-bases --ignore-missing-imports
+      - uses: actions/checkout@v4
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install Hatch
+        run: pipx install hatch
+      - name: Test
+        run: hatch run dev:test
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..76754b9
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,68 @@
+name: Build and Publish to PyPI or TestPyPI
+
+# Adapted from https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+
+on:
+  push
+
+jobs:
+  build:
+    name: Build distribution
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: "3.x"
+      - name: Install Hatch
+        run: pipx install hatch
+      - name: Build
+        run: hatch build
+      - name: Store the distribution packages
+        uses: actions/upload-artifact@v3
+        with:
+          name: python-package-distributions
+          path: dist/
+
+  publish-to-pypi:
+    name: Publish distributions to PyPI
+    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/PyDelphin
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+    steps:
+      - name: Download the dists
+        uses: actions/download-artifact@v3
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+
+  publish-to-testpypi:
+    name: Publish distributions to TestPyPI
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    environment:
+      name: testpypi
+      url: https://test.pypi.org/p/PyDelphin
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+    steps:
+      - name: Download the dists
+        uses: actions/download-artifact@v3
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish to TestPyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          repository-url: https://test.pypi.org/legacy/
+          skip-existing: true
diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml
deleted file mode 100644
index 0bc0d48..0000000
--- a/.github/workflows/pythonpublish.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Upload Python Package
-
-on:
-  release:
-    types: [created]
-
-jobs:
-  deploy:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python
-      uses: actions/setup-python@v4
-      with:
-        python-version: '3.x'
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install "setuptools >= 67" "wheel >= 0.40" "twine >= 4"
-    - name: Build and publish
-      env:
-        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
-      run: |
-        python setup.py sdist bdist_wheel
-        twine upload dist/*
diff --git a/pyproject.toml b/pyproject.toml
index 2f21011..4c45c91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,86 @@
 [build-system]
-requires = ["setuptools>=40.8.0", "wheel"]
-build-backend = "setuptools.build_meta"
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "PyDelphin"
+dynamic = ["version"]
+description = "Libraries and scripts for DELPH-IN data"
+readme = "README.md"
+requires-python = ">=3.8"
+license = "MIT"
+authors = [
+  {name = "Michael Wayne Goodman", email = "goodman.m.w@gmail.com"}
+]
+keywords = ["nlp", "semantics", "hpsg", "delph-in", "linguistics"]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Environment :: Console",
+  "Intended Audience :: Developers",
+  "Intended Audience :: Information Technology",
+  "Intended Audience :: Science/Research",
+  "License :: OSI Approved :: MIT License",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+  "Topic :: Scientific/Engineering :: Information Analysis",
+  "Topic :: Software Development :: Libraries :: Python Modules",
+  "Topic :: Text Processing :: Linguistic",
+  "Topic :: Utilities",
+]
+dependencies = [
+  "penman",
+  "progress",
+  "Pygments",
+]
+
+[project.optional-dependencies]
+web = [
+  "falcon",
+  "requests",
+]
+repp = [
+  "regex"
+]
+
+[projects.scripts]
+delphin = "delphin.main:main"
+
+[project.urls]
+Homepage = "https://github.com/delph-in/pydelphin"
+Documentation = "https://pydelphin.readthedocs.io"
+Changelog = "https://github.com/delph-in/pydelphin/blob/main/CHANGELOG.md"
+
+[tool.hatch.version]
+path = "delphin/__about__.py"
+
+[tool.hatch.build.targets.sdist]
+exclude = [
+  "/.github",
+]
+
+[tool.hatch.envs.dev]
+dependencies = [
+  "pytest",
+  "flake8",
+  "mypy",
+  "types-requests",
+]
+[tool.hatch.envs.dev.scripts]
+test = "pytest {args:.}"
+lint = "flake8 delphin --count --exit-zero --extend-ignore=E221 --max-line-length=99 --statistics"
+typecheck = "mypy delphin --namespace-packages --explicit-package-bases --ignore-missing-imports"
+
+[tool.hatch.envs.docs]
+dependencies = [
+  "sphinx",
+  "sphinx-rtd-theme",
+  "requests",
+  "falcon",
+]
+[tool.hatch.envs.docs.scripts]
+build = "make -C docs html"
+clean = "make -C docs clean"
diff --git a/setup.py b/setup.py
deleted file mode 100755
index f1152dd..0000000
--- a/setup.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python3
-
-import os
-from setuptools import setup
-
-base_dir = os.path.dirname(__file__)
-about = {}
-with open(os.path.join(base_dir, "delphin", "__about__.py")) as f:
-    exec(f.read(), about)
-
-with open(os.path.join(base_dir, 'README.md'), encoding='utf-8') as f:
-    long_description = f.read()
-
-repp_requires = ['regex==2020.1.8']
-web_requires = ['requests==2.22.0', 'falcon==2.0.0']
-
-# thanks: https://snarky.ca/clarifying-pep-518/
-doc_requirements = os.path.join(base_dir, 'docs', 'requirements.txt')
-if os.path.isfile(doc_requirements):
-    with open(doc_requirements) as f:
-        docs_require = f.readlines()
-else:
-    docs_require = []
-
-tests_require = repp_requires + web_requires + [
-    'pytest',
-    'flake8',
-    'mypy',
-    'types-requests',
-]
-
-setup(
-    name=about['__title__'],
-    version=about['__version__'],
-    description=about['__summary__'],
-    long_description=long_description,
-    long_description_content_type='text/markdown',
-    url=about['__uri__'],
-    author=about['__author__'],
-    author_email=about['__email__'],
-    license=about['__license__'],
-    classifiers=[
-        'Development Status :: 5 - Production/Stable',
-        'Environment :: Console',
-        'Intended Audience :: Developers',
-        'Intended Audience :: Information Technology',
-        'Intended Audience :: Science/Research',
-        'License :: OSI Approved :: MIT License',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3.11',
-        'Programming Language :: Python :: 3.12',
-        'Topic :: Scientific/Engineering :: Information Analysis',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-        'Topic :: Text Processing :: Linguistic',
-        'Topic :: Utilities'
-    ],
-    keywords='nlp semantics hpsg delph-in linguistics',
-    packages=[
-        'delphin',
-        'delphin.cli',
-        'delphin.codecs',
-        'delphin.mrs',
-        'delphin.eds',
-        'delphin.dmrs',
-        'delphin.web',
-    ],
-    install_requires=[
-        'penman==1.1.0',
-        'progress==1.5',
-        'Pygments >= 2.3.1',
-    ],
-    extras_require={
-        'docs': docs_require,
-        'tests': tests_require,
-        'dev': docs_require + tests_require + [
-            # https://packaging.python.org/guides/making-a-pypi-friendly-readme
-            'setuptools >= 38.6.0',
-            'wheel >= 0.31.0',
-            'twine >= 1.11.0'
-        ],
-        'web': web_requires,
-        'repp': repp_requires,
-    },
-    entry_points={
-        'console_scripts': [
-            'delphin=delphin.main:main'
-        ],
-    },
-)