diff --git a/.circleci/config.yml b/.circleci/config.yml
index 09ffc838de..8cc681c73e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,19 +1,19 @@
 version: 2.1
 
 orbs:
-  win: circleci/windows@2.4.0
+  win: circleci/windows@5.0.0
   percy: percy/agent@0.1.3
   browser-tools: circleci/browser-tools@1.4.6
 
 jobs:
   artifacts:
     docker:
-      - image: cimg/python:3.10.13
+      - image: cimg/python:3.12.1
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
     steps:
       - checkout
@@ -27,16 +27,16 @@ jobs:
           path: ~/dash/dash-main
           destination: /tmp/dash-main
 
-  install-dependencies-310: &install-dependencies
+  install-dependencies-312: &install-dependencies
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-node
+      - image: cimg/python:3.12.1-node
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYLINTRC: .pylintrc310
-          PYVERSION: python310
+          PYLINTRC: .pylintrc312
+          PYVERSION: python312
           PERCY_ENABLE: 0
 
     steps:
@@ -49,7 +49,7 @@ jobs:
           name: 🏁 Build Component Packages & Update Dependencies/Artifacts
           command: |
             python -m venv venv && . venv/bin/activate
-            pip install --upgrade pip wheel
+            pip install --upgrade pip wheel setuptools
             set -eo pipefail
             pip install -e .[ci,dev,testing,celery,diskcache] --progress-bar off
             pip list | grep dash
@@ -75,28 +75,28 @@ jobs:
           paths:
             - dash-package
 
-  install-dependencies-36:
+  install-dependencies-38:
     <<: *install-dependencies
     docker:
-      - image: cimg/python:3.6.15-node
+      - image: cimg/python:3.8.18-node
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
           PYLINTRC: .pylintrc
-          PYVERSION: python36
+          PYVERSION: python38
           PERCY_ENABLE: 0
 
-  lint-unit-310: &lint-unit
+  lint-unit-312: &lint-unit
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYLINTRC: .pylintrc310
-          PYVERSION: python310
+          PYLINTRC: .pylintrc312
+          PYVERSION: python312
           PERCY_ENABLE: 0
 
     steps:
@@ -129,19 +129,19 @@ jobs:
             . venv/bin/activate
             npm run citest.unit
 
-  lint-unit-36:
+  lint-unit-38:
     <<: *lint-unit
     docker:
-      - image: cimg/python:3.6.15-browsers
+      - image: cimg/python:3.8.18-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
           PYLINTRC: .pylintrc
-          PYVERSION: python36
+          PYVERSION: python38
           PERCY_ENABLE: 0
 
-  build-windows-310:
+  build-windows-312:
     working_directory: ~/dash
     executor:
       name: win/default
@@ -150,16 +150,21 @@ jobs:
       PERCY_ENABLE: 0
     steps:
       - checkout
+
+      - run:
+          name: "Install Python"
+          command: choco install -y python --version=3.12.1
+
       - run:
           name: ️️🏗️ build core
           command: |
-            pip install --no-cache-dir --upgrade -e .[dev,testing] --progress-bar off
-            cd dash/dash-renderer && renderer build && cd ../../
+            py -3.12 -m pip install --no-cache-dir --upgrade -e .[dev,testing] --progress-bar off
+            cd dash/dash-renderer && C:/Python312/Scripts/renderer.exe build && cd ../../
 
-  test-310: &test
+  test-312: &test
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
@@ -167,7 +172,7 @@ jobs:
           PERCY_ENABLE: 1
           PERCY_PARALLEL_TOTAL: -1
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: True
-          PYVERSION: python310
+          PYVERSION: python312
           REDIS_URL: redis://localhost:6379
       - image: cimg/redis:6.2.6
         auth:
@@ -207,17 +212,17 @@ jobs:
       - store_artifacts:
           path: /tmp/dash_artifacts
 
-  test-310-react-18:
+  test-312-react-18:
     <<: *test
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
           PERCY_ENABLE: 0
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: True
-          PYVERSION: python310
+          PYVERSION: python312
           REDIS_URL: redis://localhost:6379
           REACT_VERSION: "18.2.0"
       - image: cimg/redis:6.2.6
@@ -225,32 +230,32 @@ jobs:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
 
-  test-36:
+  test-38:
     <<: *test
     docker:
-      - image: cimg/python:3.6.15-browsers
+      - image: cimg/python:3.8.18-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
           PERCY_ENABLE: 0
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: True
-          PYVERSION: python36
+          PYVERSION: python38
           REDIS_URL: redis://localhost:6379
       - image: cimg/redis:6.2.6
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
 
-  dcc-lint-unit-310: &dcc-lint-unit
+  dcc-lint-unit-312: &dcc-lint-unit
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-node
+      - image: cimg/python:3.12.1-node
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
     steps:
       - checkout:
@@ -268,26 +273,26 @@ jobs:
             npm ci
             npm run lint
 
-  dcc-lint-unit-36:
+  dcc-lint-unit-38:
     <<: *dcc-lint-unit
     docker:
-      - image: cimg/python:3.6.15-node
+      - image: cimg/python:3.8.18-node
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python36
+          PYVERSION: python38
           PERCY_ENABLE: 0
 
-  dcc-310: &dcc-test
+  dcc-312: &dcc-test
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_PARALLEL_TOTAL: -1
           PERCY_ENABLE: 1
     parallelism: 3
@@ -325,38 +330,38 @@ jobs:
       - store_artifacts:
           path: /tmp/dash_artifacts
 
-  dcc-310-react-18:
+  dcc-312-react-18:
     <<: *dcc-test
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
           REACT_VERSION: "18.2.0"
 
-  dcc-36:
+  dcc-38:
     <<: *dcc-test
     docker:
-      - image: cimg/python:3.6.15-browsers
+      - image: cimg/python:3.8.18-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python36
+          PYVERSION: python38
           PERCY_ENABLE: 0
 
-  html-310: &html-test
+  html-312: &html-test
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 1
           PERCY_PARALLEL_TOTAL: -1
 
@@ -402,35 +407,35 @@ jobs:
       - store_artifacts:
           path: /tmp/dash_artifacts
 
-  html-310-react-18:
+  html-312-react-18:
     <<: *html-test
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
           REACT_VERSION: "18.2.0"
 
-  html-36:
+  html-38:
     <<: *html-test
     docker:
-      - image: cimg/python:3.6.15-browsers
+      - image: cimg/python:3.8.18-browsers
         auth:
           username: dashautomation
           password: $DASH_PAT_DOCKERHUB
         environment:
-          PYVERSION: python36
+          PYVERSION: python38
           PERCY_ENABLE: 0
 
   table-server: &table-server
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 1
           PERCY_PARALLEL_TOTAL: -1
 
@@ -477,18 +482,18 @@ jobs:
   table-server-react-18:
     <<: *table-server
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
           REACT_VERSION: "18.2.0"
 
   table-unit-test:
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-browsers
+      - image: cimg/python:3.12.1-browsers
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
     steps:
       - checkout:
@@ -561,9 +566,9 @@ jobs:
   table-node:
     working_directory: ~/dash
     docker:
-      - image: cimg/python:3.10.13-node
+      - image: cimg/python:3.12.1-node
         environment:
-          PYVERSION: python310
+          PYVERSION: python312
           PERCY_ENABLE: 0
     steps:
       - checkout:
@@ -601,74 +606,74 @@ workflows:
   version: 2
   tests:
     jobs:
-      - install-dependencies-310
-      - install-dependencies-36
+      - install-dependencies-312
+      - install-dependencies-38
 
-      - build-windows-310
+      - build-windows-312
 
-      - lint-unit-310:
+      - lint-unit-312:
           requires:
-            - install-dependencies-310
-      - lint-unit-36:
+            - install-dependencies-312
+      - lint-unit-38:
           requires:
-            - install-dependencies-36
+            - install-dependencies-38
 
-      - test-310:
+      - test-312:
           requires:
-            - install-dependencies-310
-      - test-310-react-18:
+            - install-dependencies-312
+      - test-312-react-18:
           requires:
-            - install-dependencies-310
-      - test-36:
+            - install-dependencies-312
+      - test-38:
           requires:
-            - install-dependencies-36
+            - install-dependencies-38
 
-      - dcc-lint-unit-310:
+      - dcc-lint-unit-312:
           requires:
-            - install-dependencies-310
-      - dcc-lint-unit-36:
+            - install-dependencies-312
+      - dcc-lint-unit-38:
           requires:
-            - install-dependencies-36
+            - install-dependencies-38
 
-      - dcc-310:
+      - dcc-312:
           requires:
-            - install-dependencies-310
-      - dcc-310-react-18:
+            - install-dependencies-312
+      - dcc-312-react-18:
           requires:
-            - install-dependencies-310
-      - dcc-36:
+            - install-dependencies-312
+      - dcc-38:
           requires:
-            - install-dependencies-36
+            - install-dependencies-38
 
-      - html-310:
+      - html-312:
           requires:
-            - install-dependencies-310
-      - html-310-react-18:
+            - install-dependencies-312
+      - html-312-react-18:
           requires:
-            - install-dependencies-310
-      - html-36:
+            - install-dependencies-312
+      - html-38:
           requires:
-            - install-dependencies-36
+            - install-dependencies-38
 
       - table-node:
           requires:
-            - install-dependencies-310
+            - install-dependencies-312
       - table-unit-test:
           requires:
-            - install-dependencies-310
+            - install-dependencies-312
       - table-visual-test
       - table-server:
           requires:
-            - install-dependencies-310
+            - install-dependencies-312
       - table-server-react-18:
           requires:
-            - install-dependencies-310
+            - install-dependencies-312
 
       - percy/finalize_all:
           requires:
-            - test-310
-            - dcc-310
-            - html-310
+            - test-312
+            - dcc-312
+            - html-312
             - table-server
       - artifacts:
           requires:
diff --git a/.pylintrc b/.pylintrc
index b00f3ed384..c2330d844b 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -3,7 +3,7 @@
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code
-extension-pkg-whitelist=
+extension-pkg-allow-list=
 
 # Add files or directories to the blacklist. They should be base names, not
 # paths.
@@ -68,7 +68,13 @@ disable=fixme,
         superfluous-parens,
         bad-continuation,
         line-too-long,
-        bad-option-value
+        bad-option-value,
+        use-dict-literal,
+        missing-timeout,
+        unnecessary-dunder-call,
+        unnecessary-lambda-assignment,
+        broad-exception-raised,
+        consider-using-generator,
 
 
 # Enable the message, report, category or checker with the given id(s). You can
@@ -234,13 +240,6 @@ indent-string='    '
 # Maximum number of lines in a module
 max-module-lines=1000
 
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-               dict-separator
-
 # Allow the body of a class to be on the same line as the declaration if body
 # contains single statement.
 single-line-class-stmt=no
@@ -415,7 +414,7 @@ max-bool-expr=5
 max-branches=15
 
 # Maximum number of locals for function / method body
-max-locals=15
+max-locals=18
 
 # Maximum number of parents for a class (see R0901).
 max-parents=7
@@ -467,10 +466,3 @@ known-standard-library=
 
 # Force import order to recognize a module as part of a third party library.
 known-third-party=enchant
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=Exception
diff --git a/.pylintrc310 b/.pylintrc312
similarity index 97%
rename from .pylintrc310
rename to .pylintrc312
index e3927edeab..eb25586d08 100644
--- a/.pylintrc310
+++ b/.pylintrc312
@@ -3,7 +3,7 @@
 # A comma-separated list of package or module names from where C extensions may
 # be loaded. Extensions are loading into the active Python interpreter and may
 # run arbitrary code.
-extension-pkg-whitelist=
+extension-pkg-allow-list=
 
 # Add files or directories to the blacklist. They should be base names, not
 # paths.
@@ -159,7 +159,13 @@ disable=invalid-name,
         line-too-long,
         super-with-arguments,
         raise-missing-from,
-        bad-option-value
+        bad-option-value,
+        use-dict-literal,
+        missing-timeout,
+        unnecessary-dunder-call,
+        unnecessary-lambda-assignment,
+        broad-exception-raised,
+        consider-using-generator,
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
@@ -332,13 +338,6 @@ indent-string='    '
 # Maximum number of lines in a module.
 max-module-lines=1000
 
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-               dict-separator
-
 # Allow the body of a class to be on the same line as the declaration if body
 # contains single statement.
 single-line-class-stmt=no
@@ -516,7 +515,7 @@ max-bool-expr=5
 max-branches=15
 
 # Maximum number of locals for function / method body.
-max-locals=15
+max-locals=18
 
 # Maximum number of parents for a class (see R0901).
 max-parents=7
@@ -574,4 +573,4 @@ known-third-party=enchant
 
 # Exceptions that will emit a warning when being caught. Defaults to
 # "Exception".
-overgeneral-exceptions=Exception
+overgeneral-exceptions=builtins.Exception
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 891a5982fa..4740f5523a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
 ## Changed
 
 - [#2734](https://github.com/plotly/dash/pull/2734) Configure CI for Python 3.10 [#1863](https://github.com/plotly/dash/issues/1863)
+- [#2735](https://github.com/plotly/dash/pull/2735) Configure CI for Python 3.8 and 3.12, drop support for Python 3.6 and Python 3.7 [#2736](https://github.com/plotly/dash/issues/2736)
 
 ## [2.15.0] - 2024-01-31
 
diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json
index 0aaee32a3b..d1da109bab 100644
--- a/components/dash-core-components/package.json
+++ b/components/dash-core-components/package.json
@@ -15,7 +15,7 @@
     "private::format.black": "black dash_core_components_base/ tests/ setup.py",
     "private::format.eslint": "eslint src --fix",
     "private::format.prettier": "prettier --config .prettierrc --write src/**/*.js",
-    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python310') !== 'python36'){process.exit(1)} \" || black --check dash_core_components_base/ tests/ setup.py",
+    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python312') !== 'python38'){process.exit(1)} \" || black --check dash_core_components_base/ tests/ setup.py",
     "private::lint.eslint": "eslint src",
     "private::lint.flake8": "flake8 --exclude=dash_core_components,node_modules,venv",
     "private::lint.prettier": "prettier --config .prettierrc src/**/*.js --list-different",
diff --git a/components/dash-table/package.json b/components/dash-table/package.json
index 8489a26599..a7a9ce4426 100644
--- a/components/dash-table/package.json
+++ b/components/dash-table/package.json
@@ -23,7 +23,7 @@
     "private::format.black": "black dash_table_base tests",
     "private::lint.ts": "eslint ./src ./tests",
     "private::lint.flake": "flake8 dash_table_base tests",
-    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python310') !== 'python36'){process.exit(1)} \" || black --check dash_table_base tests",
+    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python312') !== 'python38'){process.exit(1)} \" || black --check dash_table_base tests",
     "private::lint.prettier": "prettier --config .prettierrc \"{src,tests,demo}/**/*.{js,ts,tsx}\" --list-different",
     "private::test.python": "python -m unittest tests/unit/format_test.py",
     "private::test.unit": "karma start karma.conf.js --single-run",
diff --git a/components/dash-table/tests/selenium/conftest.py b/components/dash-table/tests/selenium/conftest.py
index 9b017d3713..6ee1bba60d 100644
--- a/components/dash-table/tests/selenium/conftest.py
+++ b/components/dash-table/tests/selenium/conftest.py
@@ -1,14 +1,102 @@
 import platform
 import pytest
+from functools import wraps
+import inspect
 
 from dash.testing.browser import Browser
-from preconditions import preconditions
 from selenium.webdriver.common.action_chains import ActionChains
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
 from selenium.webdriver.support import expected_conditions as EC
 from selenium.webdriver.support.wait import WebDriverWait
 
+
+# @preconditions decorator, copied from the preconditions PyPI package
+# https://pypi.org/project/preconditions/
+# https://github.com/nejucomo/preconditions
+# and modified to support Python 3.12
+
+
+class PreconditionError(TypeError):
+    pass
+
+
+def preconditions(*precs):
+    stripped_source = lambda obj: inspect.getsource(obj).strip()
+
+    if not precs:
+        # This edge case makes ``@preconditions()`` efficiently delegate
+        # to the wrapped function, which I anticipate will be useful
+        # for stubbing and code consistency in applications:
+        def null_decorator(f):
+            f.nopre = f  # Meet the .nopre interface requirement.
+            return f
+
+        return null_decorator
+
+    precinfo = []
+    for p in precs:
+        spec = inspect.getfullargspec(p)
+        if spec.varargs or spec.varkw:
+            raise PreconditionError(
+                (
+                    "Invalid precondition must not accept * nor ** args:\n" + "  {!s}\n"
+                ).format(stripped_source(p))
+            )
+
+        i = -len(spec.defaults or ())
+        if i == 0:
+            appargs, closureargs = spec.args, []
+        else:
+            appargs, closureargs = spec.args[:i], spec.args[i:]
+        precinfo.append((appargs, closureargs, p))
+
+    def decorate(f):
+        fspec = inspect.getfullargspec(f)
+
+        for (appargs, closureargs, p) in precinfo:
+            for apparg in appargs:
+                if apparg not in fspec.args:
+                    raise PreconditionError(
+                        (
+                            "Invalid precondition refers to unknown parameter {!r}:\n"
+                            + "  {!s}\n"
+                            + "Known parameters: {!r}\n"
+                        ).format(apparg, stripped_source(p), fspec.args)
+                    )
+            for carg in closureargs:
+                if carg in fspec.args:
+                    raise PreconditionError(
+                        (
+                            "Invalid precondition masks parameter {!r}:\n"
+                            + "  {!s}\n"
+                            + "Known parameters: {!r}\n"
+                        ).format(carg, stripped_source(p), fspec.args)
+                    )
+
+        @wraps(f)
+        def g(*a, **kw):
+            args = inspect.getcallargs(f, *a, **kw)
+            for (appargs, _, p) in precinfo:
+                if not p(*[args[aa] for aa in appargs]):
+                    raise PreconditionError(
+                        "Precondition failed in call {!r}{}:\n  {!s}\n".format(
+                            g,
+                            inspect.formatargvalues(
+                                fspec.args, fspec.varargs, fspec.varkw, args
+                            ),
+                            stripped_source(p),
+                        )
+                    )
+
+            return f(*a, **kw)
+
+        g.nopre = f
+        return g
+
+    return decorate
+
+
 _validate_col = lambda col: (isinstance(col, str) and len(col) > 0) or (
     isinstance(col, int) and col >= 0
 )
diff --git a/dash/_callback.py b/dash/_callback.py
index db682fd808..76027bd318 100644
--- a/dash/_callback.py
+++ b/dash/_callback.py
@@ -35,6 +35,10 @@
 from ._callback_context import context_value
 
 
+def _invoke_callback(func, *args, **kwargs):  # used to mark the frame for the debugger
+    return func(*args, **kwargs)  # %% callback invoked %%
+
+
 class NoUpdate:
     def to_plotly_json(self):  # pylint: disable=no-self-use
         return {"_dash_no_update": "_dash_no_update"}
@@ -438,8 +442,7 @@ def add_context(*args, **kwargs):
                 if output_value is callback_manager.UNDEFINED:
                     return to_json(response)
             else:
-                # don't touch the comment on the next line - used by debugger
-                output_value = func(*func_args, **func_kwargs)  # %% callback invoked %%
+                output_value = _invoke_callback(func, *func_args, **func_kwargs)
 
             if NoUpdate.is_no_update(output_value):
                 raise PreventUpdate
diff --git a/dash/_jupyter.py b/dash/_jupyter.py
index d383efd419..d1eb498a06 100644
--- a/dash/_jupyter.py
+++ b/dash/_jupyter.py
@@ -8,7 +8,6 @@
 import sys
 import threading
 import time
-import traceback
 
 from typing_extensions import Literal
 
@@ -37,12 +36,18 @@
 
 
 def _get_skip(error: Exception):
-    tb = traceback.format_exception(type(error), error, error.__traceback__)
-    skip = 0
-    for i, line in enumerate(tb):
-        if "%% callback invoked %%" in line:
-            skip = i + 1
-            break
+    from dash._callback import (  # pylint: disable=import-outside-toplevel
+        _invoke_callback,
+    )
+
+    tb = error.__traceback__
+    skip = 1
+    while tb.tb_next is not None:
+        skip += 1
+        tb = tb.tb_next
+        if tb.tb_frame.f_code is _invoke_callback.__code__:
+            return skip
+
     return skip
 
 
diff --git a/dash/dash.py b/dash/dash.py
index 08b5ea34c3..343538c8e3 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -146,32 +146,44 @@ def _get_traceback(secret, error: Exception):
     except ImportError:
         tbtools = None
 
-    def _get_skip(text, divider=2):
-        skip = 0
-        for i, line in enumerate(text):
-            if "%% callback invoked %%" in line:
-                skip = int((i + 1) / divider)
-                break
+    def _get_skip(error):
+        from dash._callback import (  # pylint: disable=import-outside-toplevel
+            _invoke_callback,
+        )
+
+        tb = error.__traceback__
+        skip = 1
+        while tb.tb_next is not None:
+            skip += 1
+            tb = tb.tb_next
+            if tb.tb_frame.f_code is _invoke_callback.__code__:
+                return skip
+
         return skip
 
+    def _do_skip(error):
+        from dash._callback import (  # pylint: disable=import-outside-toplevel
+            _invoke_callback,
+        )
+
+        tb = error.__traceback__
+        while tb.tb_next is not None:
+            if tb.tb_frame.f_code is _invoke_callback.__code__:
+                return tb.tb_next
+            tb = tb.tb_next
+        return error.__traceback__
+
     # werkzeug<2.1.0
     if hasattr(tbtools, "get_current_traceback"):
-        tb = tbtools.get_current_traceback()
-        skip = _get_skip(tb.plaintext.splitlines())
-        return tbtools.get_current_traceback(skip=skip).render_full()
+        return tbtools.get_current_traceback(skip=_get_skip(error)).render_full()
 
     if hasattr(tbtools, "DebugTraceback"):
-        tb = tbtools.DebugTraceback(error)  # pylint: disable=no-member
-        skip = _get_skip(tb.render_traceback_text().splitlines())
-
         # pylint: disable=no-member
-        return tbtools.DebugTraceback(error, skip=skip).render_debugger_html(
-            True, secret, True
-        )
+        return tbtools.DebugTraceback(
+            error, skip=_get_skip(error)
+        ).render_debugger_html(True, secret, True)
 
-    tb = traceback.format_exception(type(error), error, error.__traceback__)
-    skip = _get_skip(tb, 1)
-    return tb[0] + "".join(tb[skip:])
+    return "".join(traceback.format_exception(type(error), error, _do_skip(error)))
 
 
 # Singleton signal to not update an output, alternative to PreventUpdate
diff --git a/dash/testing/browser.py b/dash/testing/browser.py
index d2751bf6b9..c4fcb8fca8 100644
--- a/dash/testing/browser.py
+++ b/dash/testing/browser.py
@@ -603,7 +603,7 @@ def get_logs(self):
 
         Chrome only
         """
-        if self.driver.name.lower() == "chrome":
+        if self._browser == "chrome":
             return [
                 entry
                 for entry in self.driver.get_log("browser")
@@ -614,7 +614,7 @@ def get_logs(self):
 
     def reset_log_timestamp(self):
         """reset_log_timestamp only work with chrome webdriver."""
-        if self.driver.name.lower() == "chrome":
+        if self._browser == "chrome":
             entries = self.driver.get_log("browser")
             if entries:
                 self._last_ts = entries[-1]["timestamp"]
diff --git a/package.json b/package.json
index d13f16150b..d868cd52fa 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,10 @@
     "private::cibuild.renderer": "cd dash/dash-renderer && renderer build",
     "private::build.renderer": "cd dash/dash-renderer && renderer build",
     "private::build.jupyterlab": "cd \\@plotly/dash-jupyterlab && jlpm install && jlpm build:pack",
-    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python310') !== 'python36'){process.exit(1)} \" || black dash tests --exclude metadata_test.py --check",
+    "private::lint.black": "node -e \"if ((process.env.PYVERSION || 'python312') !== 'python38'){process.exit(1)} \" || black dash tests --exclude metadata_test.py --check",
     "private::lint.flake8": "flake8 --exclude=metadata_test.py dash tests",
-    "private::lint.pylint-dash": "PYLINTRC=${PYLINTRC:=.pylintrc39} && pylint dash setup.py --rcfile=$PYLINTRC",
-    "private::lint.pylint-tests": "PYLINTRC=${PYLINTRC:=.pylintrc39} && pylint tests/unit tests/integration -d all --rcfile=$PYLINTRC",
+    "private::lint.pylint-dash": "PYLINTRC=${PYLINTRC:=.pylintrc312} && pylint dash setup.py --rcfile=$PYLINTRC",
+    "private::lint.pylint-tests": "PYLINTRC=${PYLINTRC:=.pylintrc312} && pylint tests/unit tests/integration -d all -e C0410,C0413,W0109 --rcfile=$PYLINTRC",
     "private::lint.renderer": "cd dash/dash-renderer && npm run lint",
     "private::test.setup-components": "cd \\@plotly/dash-test-components && npm ci && npm run build",
     "private::test.setup-nested": "cd \\@plotly/dash-generator-test-component-nested && npm ci && npm run build",
diff --git a/requires-all.txt b/requires-all.txt
deleted file mode 100644
index 59a15e4465..0000000000
--- a/requires-all.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-# Dependencies used by the CeleryLongCallbackManager
-redis>=3.5.3
-celery[redis]>=5.1.2
-# Dependencies used by CI on github.com/plotly/dash
-black==22.3.0
-dash-flow-example==0.0.5
-dash-dangerously-set-inner-html
-flake8==3.9.2
-flaky==3.7.0
-flask-talisman==0.8.1
-isort==4.3.21;python_version<"3.7"
-mimesis
-mock==4.0.3
-numpy
-orjson==3.5.4;python_version<"3.7"
-orjson==3.6.3;python_version>="3.7"
-pandas
-preconditions
-pyarrow<3;python_version<"3.7"
-pyarrow;python_version>="3.7"
-pylint==2.10.2
-pytest-mock==3.2.0
-pytest-sugar==0.9.4
-xlrd<2
-# Dependencies used for development new Dash components
-coloredlogs>=15.0.1
-fire>=0.4.0
-PyYAML>=5.4.1
-# Dependencies used by the DiskcacheLongCallbackManager
-diskcache>=5.2.1
-multiprocess>=0.70.12
-psutil>=5.8.0
-Flask>=1.0.4
-flask-compress
-plotly>=5.0.0
-dash_html_components==2.0.0
-dash_core_components==2.0.0
-dash_table==5.0.0
-importlib-metadata==4.8.3;python_version<"3.7"
-# Dependencies necessary for utilizing Dash provided testing utilities
-beautifulsoup4>=4.8.2
-cryptography<3.4;python_version<"3.7"
-lxml>=4.6.2
-percy>=2.0.2
-pytest>=6.0.2
-requests[security]>=2.21.0
-selenium>=3.141.0
-waitress>=1.4.4
-webdriver-manager>=3.5.1
-setuptools
\ No newline at end of file
diff --git a/requires-celery.txt b/requires-celery.txt
index cb1568ec19..0696a73b3c 100644
--- a/requires-celery.txt
+++ b/requires-celery.txt
@@ -1,4 +1,3 @@
 # Dependencies used by the CeleryLongCallbackManager
 redis>=3.5.3
 celery[redis]>=5.1.2
-importlib-metadata<5;python_version<"3.8"
diff --git a/requires-ci.txt b/requires-ci.txt
index 6bdd07d63b..baef694377 100644
--- a/requires-ci.txt
+++ b/requires-ci.txt
@@ -1,27 +1,21 @@
 # Dependencies used by CI on github.com/plotly/dash
-black==22.3.0;python_version>="3.7"
-black==21.6b0;python_version<"3.7"
+black==22.3.0
 dash-flow-example==0.0.5
 dash-dangerously-set-inner-html
-flake8==3.9.2
+flake8==7.0.0
 flaky==3.7.0
 flask-talisman==1.0.0
-isort==4.3.21;python_version<"3.7"
 mimesis<=11.1.0
 mock==4.0.3
-numpy<=1.25.2
-orjson==3.5.4;python_version<"3.7"
-orjson==3.6.7;python_version>="3.7"
-openpyxl;python_version>="3.8"
-pandas>=1.4.0;python_version>="3.8"
-pandas==1.1.5;python_version<"3.8"
-preconditions
-pyarrow<3;python_version<"3.7"
-pyarrow;python_version>="3.7"
-pylint==2.13.5
+numpy<=1.26.3
+orjson==3.9.12
+openpyxl
+pandas>=1.4.0
+pyarrow
+pylint==3.0.3
 pytest-mock
 pytest-sugar==0.9.6
-xlrd>=2.0.1;python_version>="3.8"
-xlrd<2;python_version<"3.8"
+pyzmq==25.1.2
+xlrd>=2.0.1
 pytest-rerunfailures
 jupyterlab<4.0.0
diff --git a/requires-install.txt b/requires-install.txt
index 1b34a0cf49..139752ea65 100644
--- a/requires-install.txt
+++ b/requires-install.txt
@@ -4,9 +4,7 @@ plotly>=5.0.0
 dash_html_components==2.0.0
 dash_core_components==2.0.0
 dash_table==5.0.0
-importlib-metadata==4.8.3;python_version<"3.7"
-importlib-metadata;python_version>="3.7"
-contextvars==2.4;python_version<"3.7"
+importlib-metadata
 typing_extensions>=4.1.1
 requests
 retrying
diff --git a/setup.py b/setup.py
index 5edd96f5e6..940d128e59 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ def read_req_file(req_type):
     long_description=io.open("README.md", encoding="utf-8").read(),  # pylint: disable=consider-using-with
     long_description_content_type="text/markdown",
     install_requires=read_req_file("install"),
-    python_requires=">=3.6",
+    python_requires=">=3.8",
     extras_require={
         "ci": read_req_file("ci"),
         "dev": read_req_file("dev"),
@@ -64,11 +64,11 @@ def read_req_file(req_type):
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "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 :: 3.11",
+        "Programming Language :: Python :: 3.12",
         "Topic :: Database :: Front-Ends",
         "Topic :: Office/Business :: Financial :: Spreadsheet",
         "Topic :: Scientific/Engineering :: Visualization",
diff --git a/tests/unit/test_browser.py b/tests/unit/test_browser.py
index a7aff954bb..bec004c340 100644
--- a/tests/unit/test_browser.py
+++ b/tests/unit/test_browser.py
@@ -15,7 +15,7 @@ def test_browser_smoke(browser_type, tmpdir):
         download_path=tmpdir.mkdir("download").strpath,
         percy_finalize=True,
     )
-    assert browser.driver.name == browser_type.lower()
+    assert browser.driver.name.startswith(browser_type.lower())
 
 
 def test_browser_use_remote_webdriver(tmpdir):