Repository: projectmesa/mesa
Pull request #2131 (2024-05-06 22:16:46)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.4.3)
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
<!--pre-commit.ci end-->


Patch:
diff --git a/.github/workflows/build_lint.yml b/.github/workflows/build_lint.yml
index c7b6b4f0..2beefecb 100644
--- a/.github/workflows/build_lint.yml
+++ b/.github/workflows/build_lint.yml
@@ -10,6 +10,9 @@ on:
   pull_request:
     paths-ignore:
       - '**.md'
+  workflow_dispatch:
+  schedule:
+    - cron: '0 6 * * 1'
 
 # This will cancel previous run if a newer job that obsoletes the said previous
 # run, is started.
@@ -34,8 +37,6 @@ jobs:
             python-version: "3.11"
           - os: ubuntu
             python-version: "3.10"
-          - os: ubuntu
-            python-version: "3.9"
           # Disabled for now. See https://github.com/projectmesa/mesa/issues/1253
           #- os: ubuntu
           #  python-version: 'pypy-3.8'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 627987f3..b8f01c47 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,6 +14,8 @@ on:
       - '**.md'
       - '**.rst'
   workflow_dispatch:
+  schedule:
+    - cron: '0 6 * * 1'
 
 permissions:
   id-token: write
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d00bb6d4..0cbd083d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ ci:
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: v0.3.5
+  rev: v0.4.3
   hooks:
     # Run the linter.
     - id: ruff
@@ -17,9 +17,9 @@ repos:
     rev: v3.15.2
     hooks:
     -   id: pyupgrade
-        args: [--py38-plus]
+        args: [--py310-plus]
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.5.0  # Use the ref you want to point at
+    rev: v4.6.0  # Use the ref you want to point at
     hooks:
     -   id: trailing-whitespace
     -   id: check-toml
diff --git a/docs/mesa.md b/docs/mesa.md
index 08a7148c..6c3e8cc8 100644
--- a/docs/mesa.md
+++ b/docs/mesa.md
@@ -3,7 +3,7 @@
 ## Subpackages
 
 ```{toctree}
-mesa.visualization
+mesa.visualization_old
 ```
 
 ## Submodules
diff --git a/docs/mesa.visualization.md b/docs/mesa.visualization.md
deleted file mode 100644
index 5efa9662..00000000
--- a/docs/mesa.visualization.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# mesa.visualization package
-
-## Subpackages
-
-```{toctree}
-mesa.visualization.modules
-```
-
-## Submodules
-
-## mesa.visualization.ModularVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.ModularVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.TextVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.TextVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.UserParam module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.UserParam
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-```
-
-## Module contents
-
-```{eval-rst}
-.. automodule:: mesa.visualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
diff --git a/docs/mesa.visualization.modules.md b/docs/mesa.visualization.modules.md
deleted file mode 100644
index 6380f982..00000000
--- a/docs/mesa.visualization.modules.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# mesa.visualization.modules package
-
-## Submodules
-
-## mesa.visualization.modules.BarChartVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.BarChartVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.CanvasGridVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.CanvasGridVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.ChartVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.ChartVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.HexGridVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.HexGridVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.NetworkVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.NetworkVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.PieChartVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.PieChartVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
-
-## mesa.visualization.modules.TextVisualization module
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules.TextVisualization
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-```
-
-## Module contents
-
-```{eval-rst}
-.. automodule:: mesa.visualization.modules
-    :members:
-    :undoc-members:
-    :show-inheritance:
-```
diff --git a/docs/mesa.visualization_old.md b/docs/mesa.visualization_old.md
new file mode 100644
index 00000000..700eabf1
--- /dev/null
+++ b/docs/mesa.visualization_old.md
@@ -0,0 +1,46 @@
+# mesa.visualization_old package
+
+## Subpackages
+
+```{toctree}
+mesa.visualization_old.modules
+```
+
+## Submodules
+
+## mesa.visualization_old.ModularVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.ModularVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.TextVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.TextVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.UserParam module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.UserParam
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+```
+
+## Module contents
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
diff --git a/docs/mesa.visualization_old.modules.md b/docs/mesa.visualization_old.modules.md
new file mode 100644
index 00000000..55d94b64
--- /dev/null
+++ b/docs/mesa.visualization_old.modules.md
@@ -0,0 +1,76 @@
+# mesa.visualization_old.modules package
+
+## Submodules
+
+## mesa.visualization_old.modules.BarChartVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.BarChartVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.CanvasGridVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.CanvasGridVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.ChartVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.ChartVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.HexGridVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.HexGridVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.NetworkVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.NetworkVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.PieChartVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.PieChartVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
+
+## mesa.visualization_old.modules.TextVisualization module
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules.TextVisualization
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+```
+
+## Module contents
+
+```{eval-rst}
+.. automodule:: mesa.visualization_old.modules
+    :members:
+    :undoc-members:
+    :show-inheritance:
+```
diff --git a/docs/modular-visualization.md b/docs/modular-visualization.md
index 93fb4673..716c2f1b 100644
--- a/docs/modular-visualization.md
+++ b/docs/modular-visualization.md
@@ -1,4 +1,4 @@
-# Modular Visualization - An In-Depth Look
+# Modular Visualization - An In-Depth Look (Deprecated)
 
 Modular visualization is one of Mesa's core features. Mesa is designed
 to provide predefined visualization modules, which can be easily
@@ -127,7 +127,7 @@ Suppose we want a module which can get an arbitrary variable out of a
 model, and display its name and value. Let's create a new subclass:
 
 ```python
-from mesa.visualization.ModularTextVisualization import TextElement
+from mesa.visualization_old.ModularTextVisualization import TextElement
 
 class AttributeElement(TextElement):
     def __init__(self, attr_name):
@@ -205,7 +205,7 @@ Now let's take a look at the TextModule's Python counterpart,
 here's the whole thing:
 
 ```python
-from mesa.visualization.ModularVisualization import VisualizationElement
+from mesa.visualization_old.ModularVisualization import VisualizationElement
 
 class TextElement(VisualizationElement):
     js_includes = ["TextModule.js"]
@@ -240,8 +240,8 @@ the list, the code passes that element of the data to the *render*
 function of the corresponding element, in the elements array.
 
 Currently, module JavaScript files live in the
-*mesa/visualization/templates* directory, and the Python files live in
-*mesa/visualization/modules*.
+*mesa/visualization_old/templates* directory, and the Python files live in
+*mesa/visualization_old/modules*.
 
 When creating a new module, the Python and JavaScript code need to be
 written in synch: the module Python-side **render** method needs to
diff --git a/docs/overview.md b/docs/overview.md
index a384b02e..923d64d1 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -119,8 +119,8 @@ batch_df = batch_run.get_model_vars_dataframe()
 
 Finally, you may want to directly observe your model as it runs. Mesa's main visualization tool uses a small local web server to render the model in a browser, using JavaScript. There are different components for drawing different types of data: for example, grids for drawing agents moving around on a grid, or charts for showing how some data changes as the model runs. A few core modules are:
 
-- mesa.visualization.ModularVisualization
-- mesa.visualization.modules
+- mesa.visualization_old.ModularVisualization
+- mesa.visualization_old.modules
 
 To quickly spin up a model visualization, you might do something like:
 
@@ -135,8 +135,8 @@ def agent_portrayal(agent):
                  "r": 0.5}
     return portrayal
 
-grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
-server = mesa.visualization.ModularServer(MyModel,
+grid = mesa.visualization_old.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
+server = mesa.visualization_old.ModularServer(MyModel,
                        [grid],
                        "My Model",
                        {'n_agents': 10})
diff --git a/docs/tutorials/adv_tutorial_legacy.ipynb b/docs/tutorials/adv_tutorial_legacy.ipynb
index c0c26e42..92b04f9b 100644
--- a/docs/tutorials/adv_tutorial_legacy.ipynb
+++ b/docs/tutorials/adv_tutorial_legacy.ipynb
@@ -86,7 +86,7 @@
    },
    "outputs": [],
    "source": [
-    "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)"
+    "grid = mesa.visualization_old.CanvasGrid(agent_portrayal, 10, 10, 500, 500)"
    ]
   },
   {
@@ -109,7 +109,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "server = mesa.visualization.ModularServer(\n",
+    "server = mesa.visualization_old.ModularServer(\n",
     "    MoneyModel, [grid], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n",
     ")\n",
     "server.port = 8521  # the default"
@@ -140,8 +140,8 @@
     "                 \"r\": 0.5}\n",
     "    return portrayal\n",
     "\n",
-    "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n",
-    "server = mesa.visualization.ModularServer(MoneyModel,\n",
+    "grid = mesa.visualization_old.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n",
+    "server = mesa.visualization_old.ModularServer(MoneyModel,\n",
     "                       [grid],\n",
     "                       \"Money Model\",\n",
     "                       {\"N\":100, \"width\":10, \"height\":10})\n",
@@ -221,11 +221,11 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "chart = mesa.visualization.ChartModule(\n",
+    "chart = mesa.visualization_old.ChartModule(\n",
     "    [{\"Label\": \"Gini\", \"Color\": \"Black\"}], data_collector_name=\"datacollector\"\n",
     ")\n",
     "\n",
-    "server = mesa.visualization.ModularServer(\n",
+    "server = mesa.visualization_old.ModularServer(\n",
     "    MoneyModel, [grid, chart], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n",
     ")"
    ]
@@ -253,12 +253,12 @@
     "    return portrayal\n",
     "\n",
     "\n",
-    "grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n",
-    "chart = mesa.visualization.ChartModule(\n",
+    "grid = mesa.visualization_old.CanvasGrid(agent_portrayal, 10, 10, 500, 500)\n",
+    "chart = mesa.visualization_old.ChartModule(\n",
     "    [{\"Label\": \"Gini\", \"Color\": \"Black\"}], data_collector_name=\"datacollector\"\n",
     ")\n",
     "\n",
-    "server = mesa.visualization.ModularServer(\n",
+    "server = mesa.visualization_old.ModularServer(\n",
     "    MoneyModel, [grid, chart], \"Money Model\", {\"N\": 100, \"width\": 10, \"height\": 10}\n",
     ")\n",
     "server.port = 8521  # The default\n",
@@ -421,7 +421,7 @@
     "In a Python file (either its own, or in the same file as your visualization code), import the `VisualizationElement` class we'll inherit from, and create the new visualization class.\n",
     "\n",
     "```python\n",
-    "    from mesa.visualization.ModularVisualization import VisualizationElement, CHART_JS_FILE\n",
+    "    from mesa.visualization_old.ModularVisualization import VisualizationElement, CHART_JS_FILE\n",
     "\n",
     "    class HistogramModule(VisualizationElement):\n",
     "        package_includes = [CHART_JS_FILE]\n",
@@ -438,7 +438,7 @@
     "            self.js_code = \"elements.push(\" + new_element + \");\"\n",
     "```\n",
     "\n",
-    "There are a few things going on here. `package_includes` is a list of JavaScript files that are part of Mesa itself that the visualization element relies on. You can see the included files in [mesa/visualization/templates/](https://github.com/projectmesa/mesa/tree/main/mesa/visualization/templates). Similarly, `local_includes` is a list of JavaScript files in the same directory as the class code itself. Note that both of these are class variables, not object variables -- they hold for all particular objects.\n",
+    "There are a few things going on here. `package_includes` is a list of JavaScript files that are part of Mesa itself that the visualization element relies on. You can see the included files in [mesa/visualization_old/templates/](https://github.com/projectmesa/mesa/tree/main/mesa/visualization_old/templates). Similarly, `local_includes` is a list of JavaScript files in the same directory as the class code itself. Note that both of these are class variables, not object variables -- they hold for all particular objects.\n",
     "\n",
     "Next, look at the `__init__` method. It takes three arguments: the number of bins, and the width and height for the histogram. It then uses these values to populate the `js_code` property; this is code that the server will insert into the visualization page, which will run when the page loads. In this case, it creates a new HistogramModule (the class we created in JavaScript in the step above) with the desired bins, width and height; it then appends  (`push`es) this object to `elements`, the list of visualization elements that the visualization page itself maintains.\n",
     "\n",
@@ -461,8 +461,8 @@
     "Now, you can create your new HistogramModule and add it to the server:\n",
     "\n",
     "```python\n",
-    "    histogram = mesa.visualization.HistogramModule(list(range(10)), 200, 500)\n",
-    "    server = mesa.visualization.ModularServer(MoneyModel, \n",
+    "    histogram = mesa.visualization_old.HistogramModule(list(range(10)), 200, 500)\n",
+    "    server = mesa.visualization_old.ModularServer(MoneyModel, \n",
     "                           [grid, histogram, chart], \n",
     "                           \"Money Model\", \n",
     "                           {\"N\":100, \"width\":10, \"height\":10})\n",
@@ -473,7 +473,7 @@
     "\n",
     "![Histogram Visualization](files/viz_histogram.png)\n",
     "\n",
-    "If you've felt comfortable with this section, it might be instructive to read the code for the [ModularServer](https://github.com/projectmesa/mesa/blob/main/mesa/visualization/ModularVisualization.py#L259) and the [modular_template](https://github.com/projectmesa/mesa/blob/main/mesa/visualization/templates/modular_template.html) to get a better idea of how all the pieces fit together."
+    "If you've felt comfortable with this section, it might be instructive to read the code for the [ModularServer](https://github.com/projectmesa/mesa/blob/main/mesa/visualization_old/ModularVisualization.py#L259) and the [modular_template](https://github.com/projectmesa/mesa/blob/main/mesa/visualization_old/templates/modular_template.html) to get a better idea of how all the pieces fit together."
    ]
   },
   {
diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb
index 65f2a586..a9fe5687 100644
--- a/docs/tutorials/intro_tutorial.ipynb
+++ b/docs/tutorials/intro_tutorial.ipynb
@@ -60,7 +60,7 @@
    "source": [
     "### Tutorial Setup\n",
     "\n",
-    "Create and activate a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/). *Python version 3.9 or higher is required*.\n",
+    "Create and activate a [virtual environment](http://docs.python-guide.org/en/latest/dev/virtualenvs/). *Python version 3.10 or higher is required*.\n",
     "\n",
     "Install Mesa:\n",
     "\n",
diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb
index f19b2142..493324ea 100644
--- a/docs/tutorials/visualization_tutorial.ipynb
+++ b/docs/tutorials/visualization_tutorial.ipynb
@@ -119,7 +119,7 @@
    },
    "outputs": [],
    "source": [
-    "from mesa.experimental import JupyterViz\n",
+    "from mesa.visualization import JupyterViz\n",
     "\n",
     "page = JupyterViz(\n",
     "    BoltzmannWealthModel,\n",
diff --git a/mesa/__init__.py b/mesa/__init__.py
index e4b92ab0..529507e1 100644
--- a/mesa/__init__.py
+++ b/mesa/__init__.py
@@ -8,7 +8,7 @@ import datetime
 
 import mesa.space as space
 import mesa.time as time
-import mesa.visualization as visualization
+import mesa.visualization_old as visualization_old
 from mesa.agent import Agent
 from mesa.batchrunner import batch_run
 from mesa.datacollection import DataCollector
@@ -19,7 +19,7 @@ __all__ = [
     "Agent",
     "time",
     "space",
-    "visualization",
+    "visualization_old",
     "DataCollector",
     "batch_run",
     "experimental",
diff --git a/mesa/agent.py b/mesa/agent.py
index ab5e1800..2f0e842d 100644
--- a/mesa/agent.py
+++ b/mesa/agent.py
@@ -14,11 +14,11 @@ import operator
 import warnings
 import weakref
 from collections import defaultdict
-from collections.abc import Iterable, Iterator, MutableSet, Sequence
+from collections.abc import Callable, Iterable, Iterator, MutableSet, Sequence
 from random import Random
 
 # mypy
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING, Any
 
 if TYPE_CHECKING:
     # We ensure that these are not imported during runtime to prevent cyclic
diff --git a/mesa/batchrunner.py b/mesa/batchrunner.py
index aba96855..a74eddbe 100644
--- a/mesa/batchrunner.py
+++ b/mesa/batchrunner.py
@@ -2,7 +2,7 @@ import itertools
 from collections.abc import Iterable, Mapping
 from functools import partial
 from multiprocessing import Pool
-from typing import Any, Optional, Union
+from typing import Any
 
 from tqdm.auto import tqdm
 
@@ -11,9 +11,9 @@ from mesa.model import Model
 
 def batch_run(
     model_cls: type[Model],
-    parameters: Mapping[str, Union[Any, Iterable[Any]]],
+    parameters: Mapping[str, Any | Iterable[Any]],
     # We still retain the Optional[int] because users may set it to None (i.e. use all CPUs)
-    number_processes: Optional[int] = 1,
+    number_processes: int | None = 1,
     iterations: int = 1,
     data_collection_period: int = -1,
     max_steps: int = 1000,
@@ -76,7 +76,7 @@ def batch_run(
 
 
 def _make_model_kwargs(
-    parameters: Mapping[str, Union[Any, Iterable[Any]]],
+    parameters: Mapping[str, Any | Iterable[Any]],
 ) -> list[dict[str, Any]]:
     """Create model kwargs from parameters dictionary.
 
diff --git a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate
index 832f2df7..7b168b6d 100644
--- a/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate
+++ b/mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate
@@ -21,14 +21,14 @@ def circle_portrayal_example(agent):
     return portrayal
 
 
-canvas_element = mesa.visualization.CanvasGrid(
+canvas_element = mesa.visualization_old.CanvasGrid(
     circle_portrayal_example, 20, 20, 500, 500
 )
-chart_element = mesa.visualization.ChartModule([{"Label": "{{ cookiecutter.camel }}", "Color": "Pink"}])
+chart_element = mesa.visualization_old.ChartModule([{"Label": "{{ cookiecutter.camel }}", "Color": "Pink"}])
 
 model_kwargs = {"num_agents": 10, "width": 10, "height": 10}
 
-server = mesa.visualization.ModularServer(
+server = mesa.visualization_old.ModularServer(
     {{cookiecutter.model}},
     [canvas_element, chart_element],
     "{{ cookiecutter.camel }}",
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index 59247953..712482af 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -36,6 +36,7 @@ The default DataCollector here makes several assumptions:
 import contextlib
 import itertools
 import types
+from copy import deepcopy
 from functools import partial
 
 with contextlib.suppress(ImportError):
@@ -196,18 +197,22 @@ class DataCollector:
         if self.model_reporters:
             for var, reporter in self.model_reporters.items():
                 # Check if lambda or partial function
-                if isinstance(reporter, (types.LambdaType, partial)):
-                    self.model_vars[var].append(reporter(model))
+                if isinstance(reporter, types.LambdaType | partial):
+                    # Use deepcopy to store a copy of the data,
+                    # preventing references from being updated across steps.
+                    self.model_vars[var].append(deepcopy(reporter(model)))
                 # Check if model attribute
                 elif isinstance(reporter, str):
-                    self.model_vars[var].append(getattr(model, reporter, None))
+                    self.model_vars[var].append(
+                        deepcopy(getattr(model, reporter, None))
+                    )
                 # Check if function with arguments
                 elif isinstance(reporter, list):
-                    self.model_vars[var].append(reporter[0](*reporter[1]))
-                # TODO: Check if method of a class, as of now it is assumed
-                # implicitly if the other checks fail.
+                    self.model_vars[var].append(deepcopy(reporter[0](*reporter[1])))
+                # Assume it's a callable otherwise (e.g., method)
+                # TODO: Check if method of a class explicitly
                 else:
-                    self.model_vars[var].append(reporter())
+                    self.model_vars[var].append(deepcopy(reporter()))
 
         if self.agent_reporters:
             agent_records = self._record_agents(model)
diff --git a/mesa/experimental/UserParam.py b/mesa/experimental/UserParam.py
deleted file mode 100644
index 58715f32..00000000
--- a/mesa/experimental/UserParam.py
+++ /dev/null
@@ -1,56 +0,0 @@
-class UserParam:
-    _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
-
-    def maybe_raise_error(self, param_type, valid):
-        if valid:
-            return
-        msg = self._ERROR_MESSAGE.format(param_type, self.label)
-        raise ValueError(msg)
-
-
-class Slider(UserParam):
-    """
-    A number-based slider input with settable increment.
-
-    Example:
-
-    slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
-
-    Args:
-        label: The displayed label in the UI
-        value: The initial value of the slider
-        min: The minimum possible value of the slider
-        max: The maximum possible value of the slider
-        step: The step between min and max for a range of possible values
-        dtype: either int or float
-    """
-
-    def __init__(
-        self,
-        label="",
-        value=None,
-        min=None,
-        max=None,
-        step=1,
-        dtype=None,
-    ):
-        self.label = label
-        self.value = value
-        self.min = min
-        self.max = max
-        self.step = step
-
-        # Validate option type to make sure values are supplied properly
-        valid = not (self.value is None or self.min is None or self.max is None)
-        self.maybe_raise_error("slider", valid)
-
-        if dtype is None:
-            self.is_float_slider = self._check_values_are_float(value, min, max, step)
-        else:
-            self.is_float_slider = dtype == float
-
-    def _check_values_are_float(self, value, min, max, step):
-        return any(isinstance(n, float) for n in (value, min, max, step))
-
-    def get(self, attr):
-        return getattr(self, attr)
diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py
index 961b8762..e37cdb04 100644
--- a/mesa/experimental/__init__.py
+++ b/mesa/experimental/__init__.py
@@ -1,5 +1,3 @@
-from .jupyter_viz import JupyterViz, make_text, Slider  # noqa
 from mesa.experimental import cell_space
 
-
-__all__ = ["JupyterViz", "make_text", "Slider", "cell_space"]
+__all__ = ["cell_space"]
diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py
index 9ca36589..114301db 100644
--- a/mesa/experimental/cell_space/cell_collection.py
+++ b/mesa/experimental/cell_space/cell_collection.py
@@ -1,10 +1,10 @@
 from __future__ import annotations
 
 import itertools
-from collections.abc import Iterable, Mapping
+from collections.abc import Callable, Iterable, Mapping
 from functools import cached_property
 from random import Random
-from typing import TYPE_CHECKING, Callable, Generic, TypeVar
+from typing import TYPE_CHECKING, Generic, TypeVar
 
 if TYPE_CHECKING:
     from mesa.experimental.cell_space.cell import Cell
diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py
index e73d18f4..f08657d2 100644
--- a/mesa/experimental/cell_space/grid.py
+++ b/mesa/experimental/cell_space/grid.py
@@ -60,7 +60,7 @@ class Grid(DiscreteSpace, Generic[T]):
             raise ValueError("Dimensions must be a list of positive integers.")
         if not isinstance(self.torus, bool):
             raise ValueError("Torus must be a boolean.")
-        if self.capacity is not None and not isinstance(self.capacity, (float, int)):
+        if self.capacity is not None and not isinstance(self.capacity, float | int):
             raise ValueError("Capacity must be a number or None.")
 
     def select_random_empty_cell(self) -> T:
diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py
index 57c4d492..3983287e 100644
--- a/mesa/experimental/cell_space/network.py
+++ b/mesa/experimental/cell_space/network.py
@@ -1,5 +1,5 @@
 from random import Random
-from typing import Any, Optional
+from typing import Any
 
 from mesa.experimental.cell_space.cell import Cell
 from mesa.experimental.cell_space.discrete_space import DiscreteSpace
@@ -11,8 +11,8 @@ class Network(DiscreteSpace):
     def __init__(
         self,
         G: Any,  # noqa: N803
-        capacity: Optional[int] = None,
-        random: Optional[Random] = None,
+        capacity: int | None = None,
+        random: Random | None = None,
         cell_klass: type[Cell] = Cell,
     ) -> None:
         """A Networked grid
diff --git a/mesa/experimental/devs/eventlist.py b/mesa/experimental/devs/eventlist.py
index 48af72a4..a0ec9337 100644
--- a/mesa/experimental/devs/eventlist.py
+++ b/mesa/experimental/devs/eventlist.py
@@ -1,10 +1,11 @@
 from __future__ import annotations
 
 import itertools
+from collections.abc import Callable
 from enum import IntEnum
 from heapq import heapify, heappop, heappush
 from types import MethodType
-from typing import Any, Callable
+from typing import Any
 from weakref import WeakMethod, ref
 
 
diff --git a/mesa/experimental/devs/simulator.py b/mesa/experimental/devs/simulator.py
index 11c33f4e..74f018e8 100644
--- a/mesa/experimental/devs/simulator.py
+++ b/mesa/experimental/devs/simulator.py
@@ -1,7 +1,8 @@
 from __future__ import annotations
 
 import numbers
-from typing import Any, Callable
+from collections.abc import Callable
+from typing import Any
 
 from mesa import Model
 
diff --git a/mesa/model.py b/mesa/model.py
index 724fea2e..d998e112 100644
--- a/mesa/model.py
+++ b/mesa/model.py
@@ -14,12 +14,12 @@ import warnings
 from collections import defaultdict
 
 # mypy
-from typing import Any, Union
+from typing import Any
 
 from mesa.agent import Agent, AgentSet
 from mesa.datacollection import DataCollector
 
-TimeT = Union[float, int]
+TimeT = float | int
 
 
 class Model:
diff --git a/mesa/space.py b/mesa/space.py
index 2be9503e..a6d070f0 100644
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -23,9 +23,9 @@ import inspect
 import itertools
 import math
 import warnings
-from collections.abc import Iterable, Iterator, Sequence
+from collections.abc import Callable, Iterable, Iterator, Sequence
 from numbers import Real
-from typing import Any, Callable, TypeVar, Union, cast, overload
+from typing import Any, TypeVar, cast, overload
 from warnings import warn
 
 with contextlib.suppress(ImportError):
@@ -42,12 +42,12 @@ _types_integer = (int, np.integer)
 
 Coordinate = tuple[int, int]
 # used in ContinuousSpace
-FloatCoordinate = Union[tuple[float, float], npt.NDArray[float]]
+FloatCoordinate = tuple[float, float] | npt.NDArray[float]
 NetworkCoordinate = int
 
-Position = Union[Coordinate, FloatCoordinate, NetworkCoordinate]
+Position = Coordinate | FloatCoordinate | NetworkCoordinate
 
-GridContent = Union[Agent, None]
+GridContent = Agent | None
 MultiGridContent = list[Agent]
 
 F = TypeVar("F", bound=Callable[..., Any])
diff --git a/mesa/time.py b/mesa/time.py
index 10fa4005..e47563b4 100644
--- a/mesa/time.py
+++ b/mesa/time.py
@@ -30,14 +30,12 @@ from collections import defaultdict
 from collections.abc import Iterable
 
 # mypy
-from typing import Union
-
 from mesa.agent import Agent, AgentSet
 from mesa.model import Model
 
 # BaseScheduler has a self.time of int, while
 # StagedActivation has a self.time of float
-TimeT = Union[float, int]
+TimeT = float | int
 
 
 class BaseScheduler:
diff --git a/mesa/visualization/UserParam.py b/mesa/visualization/UserParam.py
index d4136238..58715f32 100644
--- a/mesa/visualization/UserParam.py
+++ b/mesa/visualization/UserParam.py
@@ -1,1 +1,56 @@
-from mesa_viz_tornado.UserParam import *  # noqa
+class UserParam:
+    _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
+
+    def maybe_raise_error(self, param_type, valid):
+        if valid:
+            return
+        msg = self._ERROR_MESSAGE.format(param_type, self.label)
+        raise ValueError(msg)
+
+
+class Slider(UserParam):
+    """
+    A number-based slider input with settable increment.
+
+    Example:
+
+    slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
+
+    Args:
+        label: The displayed label in the UI
+        value: The initial value of the slider
+        min: The minimum possible value of the slider
+        max: The maximum possible value of the slider
+        step: The step between min and max for a range of possible values
+        dtype: either int or float
+    """
+
+    def __init__(
+        self,
+        label="",
+        value=None,
+        min=None,
+        max=None,
+        step=1,
+        dtype=None,
+    ):
+        self.label = label
+        self.value = value
+        self.min = min
+        self.max = max
+        self.step = step
+
+        # Validate option type to make sure values are supplied properly
+        valid = not (self.value is None or self.min is None or self.max is None)
+        self.maybe_raise_error("slider", valid)
+
+        if dtype is None:
+            self.is_float_slider = self._check_values_are_float(value, min, max, step)
+        else:
+            self.is_float_slider = dtype == float
+
+    def _check_values_are_float(self, value, min, max, step):
+        return any(isinstance(n, float) for n in (value, min, max, step))
+
+    def get(self, attr):
+        return getattr(self, attr)
diff --git a/mesa/visualization/__init__.py b/mesa/visualization/__init__.py
index e8b7256e..f6611e0a 100644
--- a/mesa/visualization/__init__.py
+++ b/mesa/visualization/__init__.py
@@ -1,7 +1,3 @@
-import contextlib
+from .jupyter_viz import JupyterViz, Slider, make_text
 
-with contextlib.suppress(ImportError):
-    from mesa_viz_tornado.ModularVisualization import *  # noqa
-    from mesa_viz_tornado.modules import *  # noqa
-    from mesa_viz_tornado.UserParam import *  # noqa
-    from mesa_viz_tornado.TextVisualization import *  # noqa
+__all__ = ["JupyterViz", "make_text", "Slider"]
diff --git a/mesa/experimental/components/altair.py b/mesa/visualization/components/altair.py
similarity index 95%
rename from mesa/experimental/components/altair.py
rename to mesa/visualization/components/altair.py
index 6952c03f..f9d1a81c 100644
--- a/mesa/experimental/components/altair.py
+++ b/mesa/visualization/components/altair.py
@@ -1,5 +1,4 @@
 import contextlib
-from typing import Optional
 
 import solara
 
@@ -8,7 +7,7 @@ with contextlib.suppress(ImportError):
 
 
 @solara.component
-def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None):
+def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
     space = getattr(model, "grid", None)
     if space is None:
         # Sometimes the space is defined as model.space instead of model.grid
diff --git a/mesa/experimental/components/matplotlib.py b/mesa/visualization/components/matplotlib.py
similarity index 94%
rename from mesa/experimental/components/matplotlib.py
rename to mesa/visualization/components/matplotlib.py
index 85f8df9c..6284944a 100644
--- a/mesa/experimental/components/matplotlib.py
+++ b/mesa/visualization/components/matplotlib.py
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import networkx as nx
 import solara
 from matplotlib.figure import Figure
@@ -9,7 +7,7 @@ import mesa
 
 
 @solara.component
-def SpaceMatplotlib(model, agent_portrayal, dependencies: Optional[list[any]] = None):
+def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
     space_fig = Figure()
     space_ax = space_fig.subplots()
     space = getattr(model, "grid", None)
@@ -116,7 +114,7 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
 
 
 @solara.component
-def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
+def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
     fig = Figure()
     ax = fig.subplots()
     df = model.datacollector.get_model_vars_dataframe()
@@ -127,7 +125,7 @@ def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
         for m, color in measure.items():
             ax.plot(df.loc[:, m], label=m, color=color)
         fig.legend()
-    elif isinstance(measure, (list, tuple)):
+    elif isinstance(measure, list | tuple):
         for m in measure:
             ax.plot(df.loc[:, m], label=m)
         fig.legend()
diff --git a/mesa/experimental/jupyter_viz.py b/mesa/visualization/jupyter_viz.py
similarity index 98%
rename from mesa/experimental/jupyter_viz.py
rename to mesa/visualization/jupyter_viz.py
index a4dec7f6..2067dcfd 100644
--- a/mesa/experimental/jupyter_viz.py
+++ b/mesa/visualization/jupyter_viz.py
@@ -6,9 +6,9 @@ import reacton.ipywidgets as widgets
 import solara
 from solara.alias import rv
 
-import mesa.experimental.components.altair as components_altair
-import mesa.experimental.components.matplotlib as components_matplotlib
-from mesa.experimental.UserParam import Slider
+import mesa.visualization.components.altair as components_altair
+import mesa.visualization.components.matplotlib as components_matplotlib
+from mesa.visualization.UserParam import Slider
 
 # Avoid interactive backend
 plt.switch_backend("agg")
diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization_old/ModularVisualization.py
similarity index 100%
rename from mesa/visualization/ModularVisualization.py
rename to mesa/visualization_old/ModularVisualization.py
diff --git a/mesa/visualization/TextVisualization.py b/mesa/visualization_old/TextVisualization.py
similarity index 100%
rename from mesa/visualization/TextVisualization.py
rename to mesa/visualization_old/TextVisualization.py
diff --git a/mesa/visualization_old/UserParam.py b/mesa/visualization_old/UserParam.py
new file mode 100644
index 00000000..d4136238
--- /dev/null
+++ b/mesa/visualization_old/UserParam.py
@@ -0,0 +1,1 @@
+from mesa_viz_tornado.UserParam import *  # noqa
diff --git a/mesa/visualization_old/__init__.py b/mesa/visualization_old/__init__.py
new file mode 100644
index 00000000..e8b7256e
--- /dev/null
+++ b/mesa/visualization_old/__init__.py
@@ -0,0 +1,7 @@
+import contextlib
+
+with contextlib.suppress(ImportError):
+    from mesa_viz_tornado.ModularVisualization import *  # noqa
+    from mesa_viz_tornado.modules import *  # noqa
+    from mesa_viz_tornado.UserParam import *  # noqa
+    from mesa_viz_tornado.TextVisualization import *  # noqa
diff --git a/mesa/visualization/modules.py b/mesa/visualization_old/modules.py
similarity index 100%
rename from mesa/visualization/modules.py
rename to mesa/visualization_old/modules.py
diff --git a/pyproject.toml b/pyproject.toml
index ebc685a0..0a94f491 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
 name = "Mesa"
 description = "Agent-based modeling (ABM) in Python"
 license = { text = "Apache 2.0" }
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 authors = [
   { name = "Project Mesa Team", email = "projectmesa@googlegroups.com" },
 ]
@@ -25,7 +25,6 @@ classifiers = [
   "Topic :: Scientific/Engineering :: Artificial Intelligence",
   "Intended Audience :: Science/Research",
   "Programming Language :: Python :: 3 :: Only",
-  "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
   "Programming Language :: Python :: 3.11",
   "Programming Language :: Python :: 3.12",
@@ -81,9 +80,9 @@ path = "mesa/__init__.py"
 
 [tool.ruff]
 # See https://github.com/charliermarsh/ruff#rules for error code definitions.
-# Hardcode to Python 3.9.
+# Hardcode to Python 3.10.
 # Reminder to update mesa-examples if the value below is changed.
-target-version = "py39"
+target-version = "py310"
 extend-exclude = ["docs", "build"]
 
 [tool.ruff.lint]


Test patch:
diff --git a/tests/test_import_namespace.py b/tests/test_import_namespace.py
index f9489711..20a06f33 100644
--- a/tests/test_import_namespace.py
+++ b/tests/test_import_namespace.py
@@ -15,9 +15,9 @@ def test_import():
     _ = MultiGrid
     _ = mf.MultiGrid
 
-    from mesa.visualization.ModularVisualization import ModularServer
+    from mesa.visualization_old.ModularVisualization import ModularServer
 
-    _ = mesa.visualization.ModularServer
+    _ = mesa.visualization_old.ModularServer
     _ = ModularServer
     _ = mf.ModularServer
 
diff --git a/tests/test_jupyter_viz.py b/tests/test_jupyter_viz.py
index f67daa09..76ee666e 100644
--- a/tests/test_jupyter_viz.py
+++ b/tests/test_jupyter_viz.py
@@ -5,7 +5,7 @@ import ipyvuetify as vw
 import solara
 
 import mesa
-from mesa.experimental.jupyter_viz import JupyterViz, Slider, UserInputs
+from mesa.visualization.jupyter_viz import JupyterViz, Slider, UserInputs
 
 
 class TestMakeUserInput(unittest.TestCase):
@@ -84,7 +84,7 @@ class TestMakeUserInput(unittest.TestCase):
 
 def test_call_space_drawer(mocker):
     mock_space_matplotlib = mocker.patch(
-        "mesa.experimental.components.matplotlib.SpaceMatplotlib"
+        "mesa.visualization.components.matplotlib.SpaceMatplotlib"
     )
 
     model = mesa.Model()
diff --git a/tests/test_main.py b/tests/test_main.py
index 9f7f9834..b6bba370 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -24,7 +24,7 @@ class TestCli(unittest.TestCase):
         "Skipping test_run, because examples folder was moved. More discussion needed."
     )
     def test_run(self):
-        with patch("mesa.visualization.ModularServer") as ModularServer:  # noqa: N806
+        with patch("mesa.visualization_old.ModularServer") as ModularServer:  # noqa: N806
             example_dir = os.path.abspath(
                 os.path.join(os.path.dirname(__file__), "../examples/wolf_sheep")
             )
diff --git a/tests/test_tornado.py b/tests/test_tornado.py
index 24025a58..52f40417 100644
--- a/tests/test_tornado.py
+++ b/tests/test_tornado.py
@@ -4,7 +4,7 @@ import tornado
 from tornado.testing import AsyncHTTPTestCase
 
 from mesa import Model
-from mesa.visualization.ModularVisualization import ModularServer
+from mesa.visualization_old.ModularVisualization import ModularServer
 
 
 class TestServer(AsyncHTTPTestCase):
diff --git a/tests/test_usersettableparam.py b/tests/test_usersettableparam.py
index 8f02fa4b..52ba971e 100644
--- a/tests/test_usersettableparam.py
+++ b/tests/test_usersettableparam.py
@@ -1,6 +1,6 @@
 from unittest import TestCase
 
-from mesa.visualization.UserParam import (
+from mesa.visualization_old.UserParam import (
     Checkbox,
     Choice,
     NumberInput,
diff --git a/tests/test_visualization.py b/tests/test_visualization.py
index ca9f0949..b04a7624 100644
--- a/tests/test_visualization.py
+++ b/tests/test_visualization.py
@@ -5,9 +5,9 @@ import mesa
 from mesa.model import Model
 from mesa.space import MultiGrid
 from mesa.time import SimultaneousActivation
-from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.modules import CanvasGrid, TextElement
-from mesa.visualization.UserParam import (
+from mesa.visualization_old.ModularVisualization import ModularServer
+from mesa.visualization_old.modules import CanvasGrid, TextElement
+from mesa.visualization_old.UserParam import (
     NumberInput,
     Slider,
 )