如何为UserDict添加类型提示?让类型检查器自动识别自定义UserDict的值为Position类型
I'm trying to define a UserDict subclass that reads data from JSON, where keys are strings and values are of type Position. The JSON structure looks like this:
{ "pages": [ { "areas": [ { "name": "My_Name", "x": 179.95495495495493, "y": 117.92792792792793, "height": 15.315315315315303, "width": 125.58558558558553 }, ... ] } ] }
My current code is as follows, but type checkers like MyPy and Pylance don't recognize that page_1["My_Name"] is of type Position. How can I modify the code to fix this?
import json from collections import UserDict from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Union, cast 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])
To get type checkers to correctly infer that page_1["My_Name"] is of type Position, you need to fix the generic typing of your Page class and refine some type annotations. Here's how to modify your code step by step:
1. Specify Generic Parameters for UserDict
The UserDict class is generic, so you need to explicitly declare that your Page subclass uses str as keys and Position as values. Update the class definition to:
class Page(UserDict[str, Position]):
This tells type checkers exactly what key-value types the dictionary holds.
2. Refine the Position.from_json Method
You can remove the # type: ignore comment by adding precise type assertions to ensure the JSON values match the Position dataclass fields. This makes the type checker confident that the input is valid:
@classmethod def from_json(cls, dict_values: JsonPosition) -> "Position": return cls( name=cast(str, dict_values["name"]), x=cast(float, dict_values["x"]), y=cast(float, dict_values["y"]), width=cast(float, dict_values["width"]), height=cast(float, dict_values["height"]) )
By explicitly casting each field, you eliminate the dynamic typing warning and make the method's type behavior clear.
3. Improve JsonPage Type Definition
Your current JsonPage type can be made more precise. If your JSON's pages array always contains objects with an areas key, adjust it to:
JsonPage = Dict[Literal["areas"], List[JsonPosition]]
If None is still a possible input for from_json, keep the Optional wrapper, but ensure the type checker knows you're handling it correctly in the method.
4. Add Return Type Annotation to Page.from_json
Explicitly annotate the return type of the from_json method as Page to reinforce that it returns an instance of your typed dictionary:
@classmethod def from_json(cls, page: JsonPage) -> "Page":
Full Modified Code
Putting it all together, here's the updated code:
import json from collections import UserDict from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Union, cast from typing_extensions import Literal JsonPosition = Dict[str, Union[str, float]] # Adjust Optional based on whether your JSON might have null pages JsonPage = 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) -> "Position": return cls( name=cast(str, dict_values["name"]), x=cast(float, dict_values["x"]), y=cast(float, dict_values["y"]), width=cast(float, dict_values["width"]), height=cast(float, dict_values["height"]) ) class Page(UserDict[str, Position]): """Information about positions on a page""" @classmethod def from_json(cls, page: JsonPage) -> "Page": """Get positions from JSON Dictionary""" return cls({cast(str, p["name"]): Position.from_json(p) for p in page["areas"]}) # If you still need to handle None pages, use this version of from_json: # @classmethod # def from_json(cls, page: Optional[JsonPage]) -> "Page": # 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]) # Now type checkers will recognize page_1["My_Name"] as Position reveal_type(page_1["My_Name"]) # MyPy will output: Revealed type is "Position"
Why This Works
- By specifying
UserDict[str, Position], you're telling type checkers that every key inPageis a string and every value is aPosition. - The refined
from_jsonmethods eliminate type ambiguity, so the type checker can trace the flow of data from JSON to your typed classes. - Explicit return type annotations reinforce the expected types, making it easier for tools like Pylance to provide accurate autocompletion and type hints.
内容的提问来源于stack exchange,提问作者Jean-Francois T.

