Repository: marshmallow-code/marshmallow

Issue:
Deprecate __version__ and related attributes
See https://github.com/pallets/flask/issues/5230 for rationale and the linked PR for implementation.

Additionally, this will allow us to remove `packaging` as a dependency

Discussion:

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3f590c73..a04147ca 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,10 +5,7 @@ updates:
   schedule:
     interval: daily
   open-pull-requests-limit: 10
-  ignore:
-  - dependency-name: sphinx
-    versions:
-    - 3.5.0
-    - 3.5.1
-    - 3.5.2
-    - 3.5.3
+- package-ecosystem: "github-actions"
+  directory: "/"
+  schedule:
+    interval: "monthly"
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 8ca621c7..2232d637 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4.0.0
-      - uses: actions/setup-python@v4.7.0
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.11"
       - run: python -m pip install --upgrade pip
@@ -22,10 +22,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4.0.0
-      - uses: actions/setup-python@v4.7.0
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.11"
-      - run: python -m pip install --upgrade pip wheel
       - run: pip install tox
       - run: tox -e docs
   tests:
@@ -39,11 +38,10 @@ jobs:
           - { name: "3.12", python: "3.12", tox: py312 }
     steps:
       - uses: actions/checkout@v4.0.0
-      - uses: actions/setup-python@v4.7.0
+      - uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python }}
           allow-prereleases: true
-      - run: python -m pip install --upgrade pip
       - run: python -m pip install tox
       - run: python -m tox -e ${{ matrix.tox }}
   build:
@@ -51,7 +49,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.11"
       - name: Install pypa/build
@@ -63,7 +61,7 @@ jobs:
       - name: Check build
         run: python -m twine check --strict dist/*
       - name: Store the distribution packages
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: python-package-distributions
           path: dist/
@@ -74,10 +72,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4.0.0
-      - uses: actions/setup-python@v4.7.0
+      - uses: actions/setup-python@v5
         with:
           python-version: "3.11"
-      - run: python -m pip install --upgrade pip
       - run: python -m pip install tox
       - run: python -m tox -e lint
   publish-to-pypi:
@@ -92,7 +89,7 @@ jobs:
       id-token: write
     steps:
       - name: Download all the dists
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           name: python-package-distributions
           path: dist/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 62f2d5af..a2881a28 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,11 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.1.14
+  rev: v0.2.1
   hooks:
     - id: ruff
     - id: ruff-format
 - repo: https://github.com/python-jsonschema/check-jsonschema
-  rev: 0.27.3
+  rev: 0.28.0
   hooks:
     - id: check-github-workflows
     - id: check-readthedocs
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5b272dc1..82e39df6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,15 @@
 Changelog
 ---------
 
+3.21.0 (unreleased)
+*******************
+
+Other changes:
+
+- *Backwards-incomaptible*: ``__version__``, ``__parsed_version__``, and ``__version_info__``
+  attributes are deprecated (:issue:`2227`). Use feature detection or
+  ``importlib.metadata.version("marshmallow")`` instead.
+
 3.20.2 (2024-01-09)
 *******************
 
diff --git a/RELEASING.md b/RELEASING.md
index b2465ca6..563db130 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -6,4 +6,3 @@
 3. Tag the commit: `git tag x.y.z`
 4. Push: `git push --tags origin dev`. CI will take care of the
    PyPI release.
-5. Add release notes on Tidelift.
diff --git a/src/marshmallow/__init__.py b/src/marshmallow/__init__.py
index dd061ff4..98224bde 100644
--- a/src/marshmallow/__init__.py
+++ b/src/marshmallow/__init__.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import importlib.metadata
+import typing
 
 from packaging.version import Version
 
@@ -18,14 +19,49 @@ from marshmallow.utils import EXCLUDE, INCLUDE, RAISE, missing, pprint
 
 from . import fields
 
-# TODO: Deprecate __version__?
-__version__ = importlib.metadata.version("marshmallow")
-__parsed_version__ = Version(__version__)
-__version_info__: tuple[int, int, int] | tuple[
-    int, int, int, str, int
-] = __parsed_version__.release  # type: ignore[assignment]
-if __parsed_version__.pre:
-    __version_info__ += __parsed_version__.pre  # type: ignore[assignment]
+
+def __getattr__(name: str) -> typing.Any:
+    import warnings
+
+    if name == "__version__":
+        warnings.warn(
+            "The '__version__' attribute is deprecated and will be removed in"
+            " in a future version. Use feature detection or"
+            " 'importlib.metadata.version(\"marshmallow\")' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return importlib.metadata.version("marshmallow")
+
+    if name == "__parsed_version__":
+        warnings.warn(
+            "The '__parsed_version__' attribute is deprecated and will be removed in"
+            " in a future version. Use feature detection or"
+            " 'packaging.Version(importlib.metadata.version(\"marshmallow\"))' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return Version(importlib.metadata.version("marshmallow"))
+
+    if name == "__version_info__":
+        warnings.warn(
+            "The '__version_info__' attribute is deprecated and will be removed in"
+            " in a future version. Use feature detection or"
+            " 'packaging.Version(importlib.metadata.version(\"marshmallow\")).release' instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        __parsed_version__ = Version(importlib.metadata.version("marshmallow"))
+        __version_info__: tuple[int, int, int] | tuple[
+            int, int, int, str, int
+        ] = __parsed_version__.release  # type: ignore[assignment]
+        if __parsed_version__.pre:
+            __version_info__ += __parsed_version__.pre  # type: ignore[assignment]
+        return __version_info__
+
+    raise AttributeError(name)
+
+
 __all__ = [
     "EXCLUDE",
     "INCLUDE",


Test patch:
diff --git a/tests/test_version_attributes.py b/tests/test_version_attributes.py
new file mode 100644
index 00000000..d233b39d
--- /dev/null
+++ b/tests/test_version_attributes.py
@@ -0,0 +1,15 @@
+# ruff: noqa: B018
+import pytest
+
+import marshmallow
+
+
+def test_version_attributes_deprecated():
+    with pytest.warns(DeprecationWarning):
+        marshmallow.__version__
+
+    with pytest.warns(DeprecationWarning):
+        marshmallow.__parsed_version__
+
+    with pytest.warns(DeprecationWarning):
+        marshmallow.__version_info__