Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I type-hint a nested object in Python?

I'm currently doing a integration with WSDL, and such decided to go with Python using the Zeep library.

I'm trying to model the response with mypy, so that it works with VSCode's Intellisense, as well as some giving me hints when I'm doing careless assignments or modifications. But I hit a roadblock when the WSDL responses is in a nested object, and I can't figure a way to type-hint it.

Sample response from WSDL:

{
    'result': {
        'code': '1',
        'description': 'Success',
        'errorUUID': None
    },
    'accounts': {
        'accounts': [
            {
                'accountId': 1,
                'accountName': 'Ming',
                'availableCredit': 1
            }
        ]
    }
}

I'm using the following snippet to type-hint:

class MethodResultType:
    code: str
    description: str
    errorUUID: str

class AccountType:
    accountId: int
    accountName: str
    availableCredit: float

class getAccounts:
    result: MethodResultType
    accounts: List[AccountType] # Attempt 1
    accounts = TypedDict("accounts", {"accounts": List[AccountType]}) # Attempt 2

client = Client(os.getenv("API_URL"), wsse=user_name_token)
accountsResponse: getAccounts = client.service.getAccounts()
accounts = accountsResponse.accounts.accounts


# Attempt 1: "List[AccountType]" has no attribute "accounts"; maybe "count"?
# Attempt 2: "Type[accounts]" has no attribute "accounts"

For Attempt 1, the reason is obvious. But after trying Attempt 2, I don't know how to proceed anymore. What am I missing here?

Update: Following @Avi Kaminetzky's answer, I tried with following (playground):

from typing import List, TypedDict, Optional, Dict

class MethodResultType(TypedDict):
    code: str
    description: str
    errorUUID: Optional[str]

class AccountType(TypedDict):
    accountId: int
    accountName: str
    availableCredit: float

class getAccounts(TypedDict):
    result: MethodResultType
    accounts: Dict[str, List[AccountType]]

result: getAccounts = {
    'result': {
        'code': '1',
        'description': 'Success',
        'errorUUID': None
    },
    'accounts': {
        'accounts': [
            {
                'accountId': 1,
                'accountName': 'Ming',
                'availableCredit': 1
            }
        ]
    }
}

print(result.result)
print(result.accounts)

But I'm getting error message from mypy:

"getAccounts" has no attribute "result"
"getAccounts" has no attribute "accounts"
like image 353
Tan Jia Ming Avatar asked Dec 27 '19 09:12

Tan Jia Ming


People also ask

How do you type hint in Python?

Here's how you can add type hints to our function: Add a colon and a data type after each function parameter. Add an arrow ( -> ) and a data type after the function to specify the return data type.

Does Python have type hints?

Python's type hints provide you with optional static typing to leverage the best of both static and dynamic typing. Besides the str type, you can use other built-in types such as int , float , bool , and bytes for type hintings. To check the syntax for type hints, you need to use a static type checker tool.

Does Python enforce type hints?

First introduced in Python 3.5, Type Hints are the primary way Python developers annotate types to variables and functions. Type Hints — like the name suggests, do not enforce any type checks at the interpreter level. They are designed to be used by developers, IDEs, linters, type checkers, etc.

What is TypedDict?

TypedDict was introduced in Python 3.8 to provide type Hints for Dictionaries with a Fixed Set of Keys. The TypedDict allows us to describe a structured dictionary/map with an expected set of named string keys mapped to values of particular expected types, which Python type-checkers like mypy can further use.


1 Answers

Updates derived from conversation in comments

  1. You will need each class to be a subclass of TypedDict. Something like class Foo(TypedDict).
  2. errorUUID is an Optional[str].
  3. accounts is type Dict[str, List[AccountType]] since it has an inner (perhaps redundant) key also called accounts.
  4. You need to use square brackets with stringified keys to access the keys - accountsResponse['accounts']['accounts'].

Here is a proposed solution:

from typing import List, TypedDict, Optional, Dict

class MethodResultType(TypedDict):
    code: str
    description: str
    errorUUID: Optional[str]

class AccountType(TypedDict):
    accountId: int
    accountName: str
    availableCredit: float

class getAccounts(TypedDict):
    result: MethodResultType
    accounts: Dict[str, List[AccountType]]

result: getAccounts = {
    'result': {
        'code': '1',
        'description': 'Success',
        'errorUUID': None
    },
    'accounts': {
        'accounts': [
            {
                'accountId': 1,
                'accountName': 'Ming',
                'availableCredit': 1
            }
        ]
    }
}

See this MyPy playground: https://mypy-play.net/?mypy=latest&python=3.8&gist=dad62a9e2cecf4bad1088a2636690976

TypedDict is an extension to MyPy, make sure to install MyPy (plus extensions) and import TypedDict: from typing_extensions import TypedDict.

From Python 3.8 you can import TypedDict directly from the typing module.

https://mypy.readthedocs.io/en/latest/more_types.html#typeddict https://www.python.org/dev/peps/pep-0589/

like image 182
Avi Kaminetzky Avatar answered Oct 14 '22 01:10

Avi Kaminetzky