diff --git a/.flake8 b/.flake8
index e642613bfa23f30a1302cae51894a668b4ca66f4..4ce483e22750b6f8e2fb1223478c3d383f24a2aa 100644
--- a/.flake8
+++ b/.flake8
@@ -2,4 +2,4 @@
 max-line-length=120
 exclude=lbmpy/plot.py
         lbmpy/session.py
-ignore = W293 W503 W291 E741
+ignore = W293 W503 W291 C901 E741
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d0fe68d1ddf02ffb0df2d93efd51041789d7f895..e25c6fbdec4484467cbd01947bddc3e19785201b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -68,7 +68,7 @@ minimal-conda:
   image: i10git.cs.fau.de:5005/pycodegen/pycodegen/minimal_conda
   script:
     - pip install git+https://gitlab-ci-token:${CI_JOB_TOKEN}@i10git.cs.fau.de/pycodegen/pystencils.git@master#egg=pystencils
-    - python setup.py quicktest
+    - python quicktest.py
   tags:
     - docker
 
@@ -140,7 +140,7 @@ minimal-sympy-master:
     - pip install git+https://gitlab-ci-token:${CI_JOB_TOKEN}@i10git.cs.fau.de/pycodegen/pystencils.git@master#egg=pystencils
     - python -m pip install --upgrade git+https://github.com/sympy/sympy.git
     - pip list
-    - python setup.py quicktest
+    - python quicktest.py
   allow_failure: true
   tags:
     - docker
diff --git a/MANIFEST.in b/MANIFEST.in
index 8d256aa5f49c3fdb1a410391f46dc677113c532f..db0bf6352a30f5d23215e61ebcf2fd89982dc3c4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,3 @@
-include README.md
-include COPYING.txt
 include AUTHORS.txt
 include CONTRIBUTING.md
-global-include *.pyx
-include versioneer.py
-include lbmpy/_version.py
+include CHANGELOG.md
diff --git a/lbmpy/phasefield/nphase_nestler.py b/lbmpy/phasefield/nphase_nestler.py
index f11e9f7c2ea41a6f733ed205dc6875a3ba274bd0..3b0ea148dc200b32631a9ac9141a3c6f7778120e 100644
--- a/lbmpy/phasefield/nphase_nestler.py
+++ b/lbmpy/phasefield/nphase_nestler.py
@@ -4,10 +4,7 @@ try:
     pyximport.install(language_level=3)
     from lbmpy.phasefield.simplex_projection import simplex_projection_2d  # NOQA
 except ImportError:
-    try:
-        from lbmpy.phasefield.simplex_projection import simplex_projection_2d  # NOQA
-    except ImportError:
-        raise ImportError("neither pyximport nor binary module simplex_projection_2d available.")
+    raise ImportError("pyximport not available. Please install Cython to use simplex_projection_2d.")
 
 import sympy as sp
 
diff --git a/lbmpy/phasefield/simplex_projection.pyx b/lbmpy/phasefield/simplex_projection.pyx
index 26fa3176c8db93947ce243ec2c1d1c1f573e50c1..7af2ca8d16e13219dda7127b7ce8e0af529178fb 100644
--- a/lbmpy/phasefield/simplex_projection.pyx
+++ b/lbmpy/phasefield/simplex_projection.pyx
@@ -1,7 +1,4 @@
-# Workaround for cython bug
-# see https://stackoverflow.com/questions/8024805/cython-compiled-c-extension-importerror-dynamic-module-does-not-define-init-fu
-WORKAROUND = "Something"
-
+# cython: language_level=3str
 import cython
 
 
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..fca4285aaba5844a95e4a8927d94214ce3b8c33c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,93 @@
+[project]
+name = "lbmpy"
+description = "Code Generation for Lattice Boltzmann Methods"
+dynamic = ["version"]
+readme = "README.md"
+authors = [
+    { name = "Martin Bauer" },
+    { name = "Markus Holzer" },
+    { name = "Frederik Hennig" },
+    { email = "cs10-codegen@fau.de" },
+]
+license = { file = "COPYING.txt" }
+requires-python = ">=3.10"
+dependencies = ["pystencils>=1.3", "sympy>=1.6,<=1.11.1", "numpy>=1.8.0", "appdirs", "joblib"]
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Framework :: Jupyter",
+    "Topic :: Software Development :: Code Generators",
+    "Topic :: Scientific/Engineering :: Physics",
+    "Intended Audience :: Developers",
+    "Intended Audience :: Science/Research",
+    "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
+]
+
+[project.urls]
+"Bug Tracker" = "https://i10git.cs.fau.de/pycodegen/lbmpy/-/issues"
+"Documentation" = "https://pycodegen.pages.i10git.cs.fau.de/lbmpy/"
+"Source Code" = "https://i10git.cs.fau.de/pycodegen/lbmpy"
+
+[project.optional-dependencies]
+gpu = ['cupy']
+alltrafos = ['islpy', 'py-cpuinfo']
+bench_db = ['blitzdb', 'pymongo', 'pandas']
+interactive = [
+    'matplotlib',
+    'ipy_table',
+    'imageio',
+    'jupyter',
+    'pyevtk',
+    'rich',
+    'graphviz',
+    'scipy',
+    'scikit-image'
+]
+use_cython = [
+    'Cython'
+]
+doc = [
+    'sphinx',
+    'sphinx_rtd_theme',
+    'nbsphinx',
+    'sphinxcontrib-bibtex',
+    'sphinx_autodoc_typehints',
+    'pandoc',
+]
+tests = [
+    'pytest',
+    'pytest-cov',
+    'pytest-html',
+    'ansi2html',
+    'pytest-xdist',
+    'flake8',
+    'nbformat',
+    'nbconvert',
+    'ipython',
+    'randomgen>=1.18',
+]
+
+[build-system]
+requires = [
+    "setuptools>=69",
+    "versioneer>=0.29",
+    "tomli; python_version < '3.11'",
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.package-data]
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["lbmpy", "lbmpy.*"]
+namespaces = false
+
+[tool.versioneer]
+# See the docstring in versioneer.py for instructions. Note that you must
+# re-run 'versioneer.py setup' after changing this section, and commit the
+# resulting files.
+VCS = "git"
+style = "pep440"
+versionfile_source = "lbmpy/_version.py"
+versionfile_build = "lbmpy/_version.py"
+tag_prefix = "release/"
+parentdir_prefix = "lbmpy-"
diff --git a/quicktest.py b/quicktest.py
new file mode 100644
index 0000000000000000000000000000000000000000..76daf8b49b1477e559b2e06368ae9bd9e4aba8dd
--- /dev/null
+++ b/quicktest.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+
+from contextlib import redirect_stdout
+import io
+from lbmpy_tests.test_quicktests import (
+    test_poiseuille_channel_quicktest,
+    test_entropic_methods,
+    test_cumulant_ldc
+)
+
+quick_tests = [
+    test_poiseuille_channel_quicktest,
+    test_entropic_methods,
+    test_cumulant_ldc,
+]
+
+if __name__ == "__main__":
+    print("Running lbmpy quicktests")
+    for qt in quick_tests:
+        print(f"   -> {qt.__name__}")
+        with redirect_stdout(io.StringIO()):
+            qt()
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 60699171f13f62c334b55e9f91dac14ad8da9b94..0000000000000000000000000000000000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,11 +0,0 @@
-# See the docstring in versioneer.py for instructions. Note that you must
-# re-run 'versioneer.py setup' after changing this section, and commit the
-# resulting files.
-
-[versioneer]
-VCS = git
-style = pep440
-versionfile_source = lbmpy/_version.py
-versionfile_build = lbmpy/_version.py
-tag_prefix = release/
-parentdir_prefix = lbmpy-
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 87ca1837bbb20d6cdd74106d3ac909fa7135a473..7b51269f229df79fcd216fac2cdf0dae3c7c3551 100644
--- a/setup.py
+++ b/setup.py
@@ -1,121 +1,21 @@
-import os
-import io
-from setuptools import setup, find_packages
-import distutils
-from contextlib import redirect_stdout
-from importlib import import_module
+from setuptools import setup, __version__ as setuptools_version
 
-import versioneer
-
-try:
-    import cython  # noqa
-
-    USE_CYTHON = True
-except ImportError:
-    USE_CYTHON = False
-
-quick_tests = [
-    'test_quicktests.test_poiseuille_channel_quicktest',
-    'test_quicktests.test_entropic_methods',
-    'test_quicktests.test_cumulant_ldc',
-]
-
-
-class SimpleTestRunner(distutils.cmd.Command):
-    """A custom command to run selected tests"""
-
-    description = 'run some quick tests'
-    user_options = []
-
-    @staticmethod
-    def _run_tests_in_module(test):
-        """Short test runner function - to work also if py.test is not installed."""
-        test = 'lbmpy_tests.' + test
-        mod, function_name = test.rsplit('.', 1)
-        if isinstance(mod, str):
-            mod = import_module(mod)
+if int(setuptools_version.split('.')[0]) < 61:
+    raise Exception(
+        "[ERROR] lbmpy requires at least setuptools version 61 to install.\n"
+        "If this error occurs during an installation via pip, it is likely that there is a conflict between "
+        "versions of setuptools installed by pip and the system package manager. "
+        "In this case, it is recommended to install lbmpy into a virtual environment instead."
+    )
 
-        func = getattr(mod, function_name)
-        with redirect_stdout(io.StringIO()):
-            func()
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def run(self):
-        """Run command."""
-        for test in quick_tests:
-            self._run_tests_in_module(test)
-
-
-def readme():
-    with open('README.md') as f:
-        return f.read()
-
-
-def cython_extensions(*extensions):
-    from distutils.extension import Extension
-    if USE_CYTHON:
-        ext = '.pyx'
-        result = [Extension(e, [os.path.join(*e.split(".")) + ext]) for e in extensions]
-        from Cython.Build import cythonize
-        result = cythonize(result, language_level=3)
-        return result
-    elif all([os.path.exists(os.path.join(*e.split(".")) + '.c') for e in extensions]):
-        ext = '.c'
-        result = [Extension(e, [os.path.join(*e.split(".")) + ext]) for e in extensions]
-        return result
-    else:
-        return None
+import versioneer
 
 
 def get_cmdclass():
-    cmdclass = {"quicktest": SimpleTestRunner}
-    cmdclass.update(versioneer.get_cmdclass())
-    return cmdclass
+    return versioneer.get_cmdclass()
 
 
-major_version = versioneer.get_version().split("+")[0]
-setup(name='lbmpy',
-      version=versioneer.get_version(),
-      description='Code Generation for Lattice Boltzmann Methods',
-      long_description=readme(),
-      long_description_content_type="text/markdown",
-      author='Martin Bauer, Markus Holzer, Frederik Hennig',
-      license='AGPLv3',
-      author_email='cs10-codegen@fau.de',
-      url='https://i10git.cs.fau.de/pycodegen/lbmpy/',
-      packages=['lbmpy'] + ['lbmpy.' + s for s in find_packages('lbmpy')],
-      install_requires=[f'pystencils>=0.4.0,<={major_version}', 'sympy>=1.5.1,<=1.11.1', 'numpy>=1.11.0'],
-      package_data={'lbmpy': ['phasefield/simplex_projection.pyx', 'phasefield/simplex_projection.c']},
-      ext_modules=cython_extensions("lbmpy.phasefield.simplex_projection"),
-      classifiers=[
-          'Development Status :: 4 - Beta',
-          'Framework :: Jupyter',
-          'Topic :: Software Development :: Code Generators',
-          'Topic :: Scientific/Engineering :: Physics',
-          'Intended Audience :: Developers',
-          'Intended Audience :: Science/Research',
-          'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
-      ],
-      project_urls={
-          "Bug Tracker": "https://i10git.cs.fau.de/pycodegen/lbmpy/-/issues",
-          "Documentation": "https://pycodegen.pages.i10git.cs.fau.de/lbmpy/",
-          "Source Code": "https://i10git.cs.fau.de/pycodegen/lbmpy",
-      },
-      extras_require={
-          'gpu': ['cupy'],
-          'opencl': ['pyopencl'],
-          'alltrafos': ['islpy', 'py-cpuinfo'],
-          'interactive': ['scipy', 'scikit-image', 'cython', 'matplotlib',
-                          'ipy_table', 'imageio', 'jupyter', 'pyevtk'],
-          'doc': ['sphinx', 'sphinx_rtd_theme', 'nbsphinx',
-                  'sphinxcontrib-bibtex', 'sphinx_autodoc_typehints', 'pandoc'],
-          'phasefield': ['Cython']
-      },
-      python_requires=">=3.8",
-      cmdclass=get_cmdclass()
-      )
+setup(
+    version=versioneer.get_version(),
+    cmdclass=get_cmdclass(),
+)