Search code examples
pythonmypypython-typing

How to provide type hinting to UserDict?


I want to define a UserDict that reads values from JSON and stores a position for a given key. The JSON file looks like this:

{
    "pages": [
        {
            "areas": [
                {
                    "name": "My_Name",
                    "x": 179.95495495495493,
                    "y": 117.92792792792793,
                    "height": 15.315315315315303,
                    "width": 125.58558558558553
                },
                ...
              ]
        }
    ]
}

I would like to indicate to type linters (e.g. MyPy) that this dictionary as a key being a string and the values being a Position.

My current code is the following:

import json
from collections import UserDict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Union

from typing_extensions import Literal


JsonPosition = Dict[str, Union[str, float]]
JsonPage = Optional[Dict[Literal["areas"], List[JsonPosition]]]


@dataclass
class Position:
    """Information for a position"""

    name: str
    x: float
    y: float
    width: float
    height: float

    @classmethod
    def from_json(cls, dict_values: JsonPosition):
        return cls(**dict_values)  # type: ignore  # dynamic typing


class Page(UserDict):
    """Information about positions on a page"""

    @classmethod
    def from_json(cls, page: JsonPage):
        """Get positions from JSON Dictionary"""
        if page is None:
            return cls()

        return cls({cast(str, p["name"]): Position.from_json(p) for p in page["areas"]})



JSON = Path("my_positions.json").read_text()
positions = json.loads(JSON)
page_1 = Page.from_json(positions["pages"][0])

I would like MyPy (or Pylance or whatever type hinter I use), to automatically recognize page_1["My_Name"] as being a Position.

What could I change?


Solution

  • Actually, you can directly provide the type to UserDict with square brackets ([...]) like you would with a Dict:

    class Page(UserDict[str, Position]):
        ...
    

    For Python 3.6 or earlier, this will not work.

    For Python >=3.7 and <3.9, you need the following to subscript collections.UserDict and put it in a separate block specific to type checking (with constant TYPE_CHECKING):

    from __future__ import annotations
    from collections import UserDict
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        TypedUserDict = UserDict[str, Position]
    else:
        TypedUserDict = UserDict
    
    
    class Page(TypedUserDict):
        ...
    

    For Python 3.9+, no additional import or trick is necessary.