Repository: altair-viz/altair
Pull request #2827 (2023-01-12 13:25:57)

Backport bug fixes for a 4.2.1 release
Hi everyone,

I'm a big fan of altair! Thanks for all your work on this.

I've read https://github.com/altair-viz/altair/discussions/2588, https://github.com/altair-viz/altair/issues/2768 and related issues and I was wondering if you'd be willing to make a 4.2.1 (or 4.3.0) release with bug fixes as long as I do all the work on it? (Or almost all of the work, because based on https://github.com/altair-viz/altair/issues/2793, one of you would still need to make the actual PyPI release).

In this PR, I'm backporting
- #2771 
- #2683  

and I bumped the version number and added a changlog entry.

I'm happy to backport more PRs that are fixing critical issues (if desired).

The PR is using `v4.2.X` as base branch.

Test suite is ✅  (after installing `altair_saver` and `'vega-lite-cli<5'` so that no tests are skipped).

<details>

```
pytest --pyargs --doctest-modules altair -x                                                           
============================================================================== test session starts ==============================================================================
platform darwin -- Python 3.11.0, pytest-7.2.0, pluggy-1.0.0
rootdir: /Users/jtilly/altair
plugins: xdist-3.1.0, cov-4.0.0
collected 605 items                                                                                                                                                             

altair/examples/tests/test_examples.py .................................................................................................................................. [ 21%]
......................................................................................................................................................................... [ 49%]
.............                                                                                                                                                             [ 51%]
altair/expr/tests/test_expr.py ..........                                                                                                                                 [ 53%]
altair/tests/test_magics.py .......                                                                                                                                       [ 54%]
altair/utils/core.py ..                                                                                                                                                   [ 54%]
altair/utils/deprecation.py .                                                                                                                                             [ 54%]
altair/utils/tests/test_core.py ..............                                                                                                                            [ 57%]
altair/utils/tests/test_data.py ........                                                                                                                                  [ 58%]
altair/utils/tests/test_deprecation.py ..                                                                                                                                 [ 58%]
altair/utils/tests/test_execeval.py ..                                                                                                                                    [ 59%]
altair/utils/tests/test_html.py ....                                                                                                                                      [ 59%]
altair/utils/tests/test_mimebundle.py ....                                                                                                                                [ 60%]
altair/utils/tests/test_plugin_registry.py ....                                                                                                                           [ 61%]
altair/utils/tests/test_schemapi.py ...................                                                                                                                   [ 64%]
altair/utils/tests/test_server.py .                                                                                                                                       [ 64%]
altair/utils/tests/test_utils.py ........                                                                                                                                 [ 65%]
altair/vega/tests/test_import.py .                                                                                                                                        [ 65%]
altair/vegalite/tests/test_common.py ........                                                                                                                             [ 67%]
altair/vegalite/v3/api.py ......                                                                                                                                          [ 68%]
altair/vegalite/v3/tests/test_api.py ................................................................................                                                     [ 81%]
altair/vegalite/v3/tests/test_data.py .                                                                                                                                   [ 81%]
altair/vegalite/v3/tests/test_display.py ..                                                                                                                               [ 81%]
altair/vegalite/v3/tests/test_geo_interface.py ........                                                                                                                   [ 83%]
altair/vegalite/v3/tests/test_renderers.py ...                                                                                                                            [ 83%]
altair/vegalite/v3/tests/test_theme.py .                                                                                                                                  [ 83%]
altair/vegalite/v4/api.py ......                                                                                                                                          [ 84%]
altair/vegalite/v4/tests/test_api.py ............................................................................                                                         [ 97%]
altair/vegalite/v4/tests/test_data.py .                                                                                                                                   [ 97%]
altair/vegalite/v4/tests/test_display.py ..                                                                                                                               [ 98%]
altair/vegalite/v4/tests/test_geo_interface.py ........                                                                                                                   [ 99%]
altair/vegalite/v4/tests/test_renderers.py ...                                                                                                                            [ 99%]
altair/vegalite/v4/tests/test_theme.py .                                                                                                                                  [100%]

======================================================================== 605 passed in 193.91s (0:03:13) ========================================================================
```

</details>

In https://github.com/altair-viz/altair/discussions/2588#discussioncomment-4346117, @joelostblom writes that 
> but it would require some reordering of the commits on the main branch

I'm not exactly sure what you mean by that and I would hope that it's fine to not have these bug fix commits in the correct order in the main branch.

If you're saying this is not worth the effort, feel free to close the PR.

joelostblom: Thanks for working on this @jtilly ! Regarding my previous comment, I think I made a more accurate one here https://github.com/altair-viz/altair/issues/2817#issuecomment-1374852750:

> Regarding a 4.3 version, I think there are some nice things with this, but it would involve a lot of rearranging **or** cherrypicking of commits to create the 4.3 release. I would rather spend that time on working toward a 5.0 release, and recommend people to update.

I guess between the two approaches it would make more sense to cherrypick the relevant commits into a separate branch (as you have here) and do a 4.3 release from that rather than trying to get the main branch in the right order.

As I mentioned in that comment, I do see advantage with a 4.3 release, but I don't have the bandwidth to put into work or review on that myself and would rather focus my efforts towards 5.0. Having that said, if someone else is willing to review, I definitely don't have anything against there being a 4.3/4.2.1 release with the most important bugfixes.

I would also like to draw your attention to https://github.com/altair-viz/altair/discussions/2599#discussioncomment-4582388, which mentions not all of the maintainers have the required  permissions to make a PyPI release, so if you are hoping to make a quick 4.3 release while waiting for 5.0, I am not sure if that will happen and it might be more likely that they both come out at a similar time (but that's a bit of speculation from my side, just wanted you to be aware of these caveats).
jtilly: > so if you are hoping to make a quick 4.3 release while waiting for 5.0, I am not sure if that will happen and it might be more likely that they both come out at a similar time 

That was my hope indeed 😁 

In any case, I wanted to give it a try -- please let me know if there are any other bug fixes that should be backported.
th0ger: Would love a patch for https://github.com/altair-viz/altair/pull/2771 soon, to avoid forcing `jsonschema<=4.16.0`.
mattijn: I think it is a good idea to prepare for a hotfixed version of altair 4.2. I also would like to include some warnings of features that will be removed in altair 5. 
Let me merge this PR first, as it looks solid to me.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 40922ad2..43666249 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,6 +24,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install .[dev]
+        pip install "selenium<4.3.0"
         pip install altair_saver
     - name: Test with pytest
       run: |
diff --git a/.github/workflows/docbuild.yml b/.github/workflows/docbuild.yml
index 1c99835f..10f25992 100644
--- a/.github/workflows/docbuild.yml
+++ b/.github/workflows/docbuild.yml
@@ -19,10 +19,9 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install .[dev]
+        pip install "selenium<4.3.0"
         pip install altair_saver
         pip install -r doc/requirements.txt
     - name: Run docbuild
       run: |
         cd doc && make ${{ matrix.build-type }}
-
-        
diff --git a/altair/__init__.py b/altair/__init__.py
index 7079e8cc..bcbc8749 100644
--- a/altair/__init__.py
+++ b/altair/__init__.py
@@ -1,5 +1,5 @@
 # flake8: noqa
-__version__ = "4.2.0"
+__version__ = "4.2.1"
 
 from .vegalite import *
 from . import examples
diff --git a/altair/utils/core.py b/altair/utils/core.py
index 43bc1b4d..588e8c60 100644
--- a/altair/utils/core.py
+++ b/altair/utils/core.py
@@ -314,7 +314,7 @@ def sanitize_dataframe(df):  # noqa: C901
         else:
             return val
 
-    for col_name, dtype in df.dtypes.iteritems():
+    for col_name, dtype in df.dtypes.items():
         if str(dtype) == "category":
             # XXXX: work around bug in to_json for categorical types
             # https://github.com/pydata/pandas/issues/10778
diff --git a/altair/utils/display.py b/altair/utils/display.py
index 92d7c783..bcf8232b 100644
--- a/altair/utils/display.py
+++ b/altair/utils/display.py
@@ -4,10 +4,9 @@ import textwrap
 from typing import Callable, Dict
 import uuid
 
-from jsonschema import validate
-
 from .plugin_registry import PluginRegistry
 from .mimebundle import spec_to_mimebundle
+from .schemapi import validate_jsonschema
 
 
 # ==============================================================================
@@ -126,7 +125,7 @@ class Displayable(object):
         # type: () -> None
         """Validate the spec against the schema."""
         schema_dict = json.loads(pkgutil.get_data(*self.schema_path).decode("utf-8"))
-        validate(self.spec, schema_dict)
+        validate_jsonschema(self.spec, schema_dict)
 
     def _repr_mimebundle_(self, include=None, exclude=None):
         """Return a MIME bundle for display in Jupyter frontends."""
diff --git a/altair/utils/schemapi.py b/altair/utils/schemapi.py
index 2dfdc8ee..f170acd3 100644
--- a/altair/utils/schemapi.py
+++ b/altair/utils/schemapi.py
@@ -6,10 +6,11 @@ import inspect
 import json
 
 import jsonschema
+import jsonschema.exceptions
 import numpy as np
 import pandas as pd
 
-
+JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -39,6 +40,24 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
+def validate_jsonschema(spec, schema, resolver=None):
+    # We don't use jsonschema.validate as this would validate the schema itself.
+    # Instead, we pass the schema directly to the validator class. This is done for
+    # two reasons: The schema comes from Vega-Lite and is not based on the user
+    # input, therefore there is no need to validate it in the first place. Furthermore,
+    # the "uri-reference" format checker fails for some of the references as URIs in
+    # "$ref" are not encoded,
+    # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
+    # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
+    # it is not a valid URI reference due to the characters such as '<'.
+    validator = JSONSCHEMA_VALIDATOR(
+        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
+    )
+    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
+    if error is not None:
+        raise error
+
+
 def _subclasses(cls):
     """Breadth-first sequence of all classes which inherit from cls."""
     seen = set()
@@ -150,7 +169,7 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = jsonschema.Draft7Validator
+    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -440,9 +459,7 @@ class SchemaBase(object):
         if schema is None:
             schema = cls._schema
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(
-            instance, schema, cls=cls._validator, resolver=resolver
-        )
+        return validate_jsonschema(instance, schema, resolver=resolver)
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -461,7 +478,7 @@ class SchemaBase(object):
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -555,7 +572,7 @@ class _FromDict(object):
             for possible_schema in schemas:
                 resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    jsonschema.validate(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, resolver=resolver)
                 except jsonschema.ValidationError:
                     continue
                 else:
diff --git a/doc/releases/changes.rst b/doc/releases/changes.rst
index c69f85ac..4b58cdaf 100644
--- a/doc/releases/changes.rst
+++ b/doc/releases/changes.rst
@@ -3,6 +3,15 @@
 Altair Change Log
 =================
 
+Version 4.2.1 (released XXX XX, 2023)
+-------------------------------------
+
+Bug Fixes
+~~~~~~~~~
+- Disable uri-reference format check in jsonsschema (#2771)
+- Replace ``iteritems`` with ```items``` due to pandas deprecation (#2683)
+
+
 Version 4.2.0 (released Dec 29, 2021)
 -------------------------------------
 - Update Vega-Lite from version 4.8.1 to version 4.17.0;
diff --git a/tools/schemapi/jsonschema-draft04.json b/tools/schemapi/jsonschema-draft04.json
deleted file mode 100644
index bcbb8474..00000000
--- a/tools/schemapi/jsonschema-draft04.json
+++ /dev/null
@@ -1,149 +0,0 @@
-{
-    "id": "http://json-schema.org/draft-04/schema#",
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "description": "Core schema meta-schema",
-    "definitions": {
-        "schemaArray": {
-            "type": "array",
-            "minItems": 1,
-            "items": { "$ref": "#" }
-        },
-        "positiveInteger": {
-            "type": "integer",
-            "minimum": 0
-        },
-        "positiveIntegerDefault0": {
-            "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
-        },
-        "simpleTypes": {
-            "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
-        },
-        "stringArray": {
-            "type": "array",
-            "items": { "type": "string" },
-            "minItems": 1,
-            "uniqueItems": true
-        }
-    },
-    "type": "object",
-    "properties": {
-        "id": {
-            "type": "string"
-        },
-        "$schema": {
-            "type": "string"
-        },
-        "title": {
-            "type": "string"
-        },
-        "description": {
-            "type": "string"
-        },
-        "default": {},
-        "multipleOf": {
-            "type": "number",
-            "minimum": 0,
-            "exclusiveMinimum": true
-        },
-        "maximum": {
-            "type": "number"
-        },
-        "exclusiveMaximum": {
-            "type": "boolean",
-            "default": false
-        },
-        "minimum": {
-            "type": "number"
-        },
-        "exclusiveMinimum": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxLength": { "$ref": "#/definitions/positiveInteger" },
-        "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "pattern": {
-            "type": "string",
-            "format": "regex"
-        },
-        "additionalItems": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "items": {
-            "anyOf": [
-                { "$ref": "#" },
-                { "$ref": "#/definitions/schemaArray" }
-            ],
-            "default": {}
-        },
-        "maxItems": { "$ref": "#/definitions/positiveInteger" },
-        "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "uniqueItems": {
-            "type": "boolean",
-            "default": false
-        },
-        "maxProperties": { "$ref": "#/definitions/positiveInteger" },
-        "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
-        "required": { "$ref": "#/definitions/stringArray" },
-        "additionalProperties": {
-            "anyOf": [
-                { "type": "boolean" },
-                { "$ref": "#" }
-            ],
-            "default": {}
-        },
-        "definitions": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "properties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "patternProperties": {
-            "type": "object",
-            "additionalProperties": { "$ref": "#" },
-            "default": {}
-        },
-        "dependencies": {
-            "type": "object",
-            "additionalProperties": {
-                "anyOf": [
-                    { "$ref": "#" },
-                    { "$ref": "#/definitions/stringArray" }
-                ]
-            }
-        },
-        "enum": {
-            "type": "array",
-            "minItems": 1,
-            "uniqueItems": true
-        },
-        "type": {
-            "anyOf": [
-                { "$ref": "#/definitions/simpleTypes" },
-                {
-                    "type": "array",
-                    "items": { "$ref": "#/definitions/simpleTypes" },
-                    "minItems": 1,
-                    "uniqueItems": true
-                }
-            ]
-        },
-        "format": { "type": "string" },
-        "allOf": { "$ref": "#/definitions/schemaArray" },
-        "anyOf": { "$ref": "#/definitions/schemaArray" },
-        "oneOf": { "$ref": "#/definitions/schemaArray" },
-        "not": { "$ref": "#" }
-    },
-    "dependencies": {
-        "exclusiveMaximum": [ "maximum" ],
-        "exclusiveMinimum": [ "minimum" ]
-    },
-    "default": {}
-}
diff --git a/tools/schemapi/schemapi.py b/tools/schemapi/schemapi.py
index 3ca87b99..3844483a 100644
--- a/tools/schemapi/schemapi.py
+++ b/tools/schemapi/schemapi.py
@@ -4,10 +4,11 @@ import inspect
 import json
 
 import jsonschema
+import jsonschema.exceptions
 import numpy as np
 import pandas as pd
 
-
+JSONSCHEMA_VALIDATOR = jsonschema.Draft7Validator
 # If DEBUG_MODE is True, then schema objects are converted to dict and
 # validated at creation time. This slows things down, particularly for
 # larger specs, but leads to much more useful tracebacks for the user.
@@ -37,6 +38,24 @@ def debug_mode(arg):
         DEBUG_MODE = original
 
 
+def validate_jsonschema(spec, schema, resolver=None):
+    # We don't use jsonschema.validate as this would validate the schema itself.
+    # Instead, we pass the schema directly to the validator class. This is done for
+    # two reasons: The schema comes from Vega-Lite and is not based on the user
+    # input, therefore there is no need to validate it in the first place. Furthermore,
+    # the "uri-reference" format checker fails for some of the references as URIs in
+    # "$ref" are not encoded,
+    # e.g. '#/definitions/ValueDefWithCondition<MarkPropFieldOrDatumDef,
+    # (Gradient|string|null)>' would be a valid $ref in a Vega-Lite schema but
+    # it is not a valid URI reference due to the characters such as '<'.
+    validator = JSONSCHEMA_VALIDATOR(
+        schema, format_checker=JSONSCHEMA_VALIDATOR.FORMAT_CHECKER, resolver=resolver
+    )
+    error = jsonschema.exceptions.best_match(validator.iter_errors(spec))
+    if error is not None:
+        raise error
+
+
 def _subclasses(cls):
     """Breadth-first sequence of all classes which inherit from cls."""
     seen = set()
@@ -148,7 +167,7 @@ class SchemaBase(object):
     _schema = None
     _rootschema = None
     _class_is_valid_at_instantiation = True
-    _validator = jsonschema.Draft7Validator
+    _validator = JSONSCHEMA_VALIDATOR
 
     def __init__(self, *args, **kwds):
         # Two valid options for initialization, which should be handled by
@@ -438,9 +457,7 @@ class SchemaBase(object):
         if schema is None:
             schema = cls._schema
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(
-            instance, schema, cls=cls._validator, resolver=resolver
-        )
+        return validate_jsonschema(instance, schema, resolver=resolver)
 
     @classmethod
     def resolve_references(cls, schema=None):
@@ -459,7 +476,7 @@ class SchemaBase(object):
         value = _todict(value, validate=False, context={})
         props = cls.resolve_references(schema or cls._schema).get("properties", {})
         resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema)
-        return jsonschema.validate(value, props.get(name, {}), resolver=resolver)
+        return validate_jsonschema(value, props.get(name, {}), resolver=resolver)
 
     def __dir__(self):
         return list(self._kwds.keys())
@@ -553,7 +570,7 @@ class _FromDict(object):
             for possible_schema in schemas:
                 resolver = jsonschema.RefResolver.from_schema(rootschema)
                 try:
-                    jsonschema.validate(dct, possible_schema, resolver=resolver)
+                    validate_jsonschema(dct, possible_schema, resolver=resolver)
                 except jsonschema.ValidationError:
                     continue
                 else:
diff --git a/tools/schemapi/utils.py b/tools/schemapi/utils.py
index d0b149f3..5a73c96c 100644
--- a/tools/schemapi/utils.py
+++ b/tools/schemapi/utils.py
@@ -1,8 +1,6 @@
 """Utilities for working with schemas"""
 
-import json
 import keyword
-import pkgutil
 import re
 import textwrap
 import urllib
@@ -13,12 +11,6 @@ import jsonschema
 EXCLUDE_KEYS = ("definitions", "title", "description", "$schema", "id")
 
 
-def load_metaschema():
-    schema = pkgutil.get_data("schemapi", "jsonschema-draft04.json")
-    schema = schema.decode()
-    return json.loads(schema)
-
-
 def resolve_references(schema, root=None):
     """Resolve References within a JSON schema"""
     resolver = jsonschema.RefResolver.from_schema(root or schema)
@@ -144,7 +136,7 @@ class SchemaProperties(object):
 class SchemaInfo(object):
     """A wrapper for inspecting a JSON schema"""
 
-    def __init__(self, schema, rootschema=None, validate=False):
+    def __init__(self, schema, rootschema=None):
         if hasattr(schema, "_schema"):
             if hasattr(schema, "_rootschema"):
                 schema, rootschema = schema._schema, schema._rootschema
@@ -152,10 +144,6 @@ class SchemaInfo(object):
                 schema, rootschema = schema._schema, schema._schema
         elif not rootschema:
             rootschema = schema
-        if validate:
-            metaschema = load_metaschema()
-            jsonschema.validate(schema, metaschema)
-            jsonschema.validate(rootschema, metaschema)
         self.raw_schema = schema
         self.rootschema = rootschema
         self.schema = resolve_references(schema, rootschema)


Test patch:
diff --git a/altair/utils/tests/test_plugin_registry.py b/altair/utils/tests/test_plugin_registry.py
index cbfb62ac..38f9cc05 100644
--- a/altair/utils/tests/test_plugin_registry.py
+++ b/altair/utils/tests/test_plugin_registry.py
@@ -26,7 +26,7 @@ def test_plugin_registry():
     assert plugins.get() is None
     assert repr(plugins) == "TypedCallableRegistry(active='', registered=[])"
 
-    plugins.register("new_plugin", lambda x: x ** 2)
+    plugins.register("new_plugin", lambda x: x**2)
     assert plugins.names() == ["new_plugin"]
     assert plugins.active == ""
     assert plugins.get() is None
@@ -46,7 +46,7 @@ def test_plugin_registry():
 def test_plugin_registry_extra_options():
     plugins = GeneralCallableRegistry()
 
-    plugins.register("metadata_plugin", lambda x, p=2: x ** p)
+    plugins.register("metadata_plugin", lambda x, p=2: x**p)
     plugins.enable("metadata_plugin")
     assert plugins.get()(3) == 9
 
@@ -86,7 +86,7 @@ def test_plugin_registry_global_settings():
 def test_plugin_registry_context():
     plugins = GeneralCallableRegistry()
 
-    plugins.register("default", lambda x, p=2: x ** p)
+    plugins.register("default", lambda x, p=2: x**p)
 
     # At first there is no plugin enabled
     assert plugins.active == ""
diff --git a/altair/utils/tests/test_utils.py b/altair/utils/tests/test_utils.py
index 1ccede41..54a12721 100644
--- a/altair/utils/tests/test_utils.py
+++ b/altair/utils/tests/test_utils.py
@@ -129,9 +129,9 @@ def test_sanitize_nullable_integers():
     )
 
     df_clean = sanitize_dataframe(df)
-    assert {col.dtype.name for _, col in df_clean.iteritems()} == {"object"}
+    assert {col.dtype.name for _, col in df_clean.items()} == {"object"}
 
-    result_python = {col_name: list(col) for col_name, col in df_clean.iteritems()}
+    result_python = {col_name: list(col) for col_name, col in df_clean.items()}
     assert result_python == {
         "int_np": [1, 2, 3, 4, 5],
         "int64": [1, 2, 3, None, 5],
@@ -157,9 +157,9 @@ def test_sanitize_string_dtype():
     )
 
     df_clean = sanitize_dataframe(df)
-    assert {col.dtype.name for _, col in df_clean.iteritems()} == {"object"}
+    assert {col.dtype.name for _, col in df_clean.items()} == {"object"}
 
-    result_python = {col_name: list(col) for col_name, col in df_clean.iteritems()}
+    result_python = {col_name: list(col) for col_name, col in df_clean.items()}
     assert result_python == {
         "string_object": ["a", "b", "c", "d"],
         "string_string": ["a", "b", "c", "d"],
@@ -182,9 +182,9 @@ def test_sanitize_boolean_dtype():
     )
 
     df_clean = sanitize_dataframe(df)
-    assert {col.dtype.name for _, col in df_clean.iteritems()} == {"object"}
+    assert {col.dtype.name for _, col in df_clean.items()} == {"object"}
 
-    result_python = {col_name: list(col) for col_name, col in df_clean.iteritems()}
+    result_python = {col_name: list(col) for col_name, col in df_clean.items()}
     assert result_python == {
         "bool_none": [True, False, None],
         "none": [None, None, None],