10

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"
Tan Jia Ming
  • 435
  • 1
  • 6
  • 22

2 Answers2

15

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/

Avi Kaminetzky
  • 1,489
  • 2
  • 19
  • 42
  • 1
    The python version I'm using is Python 3.8 and I already having Mypy installed. If I subclass getAccounts(TypedDict), I can't even type-hint `result` properly ("getAccounts" has no attribute "result"). Please note that in sample response, `accounts` is a nested object. Can you provide a sample code on how would you do it? – Tan Jia Ming Jan 02 '20 at 03:54
  • Thanks for the sample code, using your example, I will get 2 error messages: 1. `"getAccounts" has no attribute "result" 2. `"getAccounts" has no attribute "accounts"` – Tan Jia Ming Jan 03 '20 at 01:42
  • It worked for me here https://mypy-play.net/?mypy=latest&python=3.8 – Avi Kaminetzky Jan 03 '20 at 02:53
  • https://mypy-play.net/?mypy=latest&python=3.8&gist=f4450307f0a8ffb6f6285d145e63c014 I have tried the website, and including the playground that has the error message – Tan Jia Ming Jan 03 '20 at 03:09
  • See this link: https://mypy-play.net/?mypy=latest&python=3.8&gist=80d462e1db7dfa04c2f152b8cb557956 – Avi Kaminetzky Jan 03 '20 at 03:49
  • Thanks, but the example that you provided doesn't type hinting correctly, because whenever I try to access the `result` or `accounts` attribute, mypy would report error (as per my link) – Tan Jia Ming Jan 03 '20 at 08:56
  • You need to access the keys with square brackets and stringified keys: `result['result']`, since TypedDict is treated like a regular dictionary not a class variable (which can be access with dot notation). I updated the mypy playground link. – Avi Kaminetzky Jan 03 '20 at 14:29
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/205300/discussion-between-avi-kaminetzky-and-tan-jia-ming). – Avi Kaminetzky Jan 03 '20 at 14:34
0

Using this answer as reference, the following works for my case (Intellisense in VSCode, not working if directly assign into a variable):

Update: Using another answer as reference, I have updated my code to be able to work both ways. (Intellisense in VSCode, directly assign into a variable)

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

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

class accounts:
    accounts: List[AccountType]

class getAccounts:
    def __init__(self):
        self.accounts = accounts()
    result: MethodResultType
    @property
    def accounts(self):
        return self.accounts


client = Client(os.getenv("API_URL"), wsse=user_name_token)


# Getting real response from WSDL
accountsResponse: getAccounts = client.service.getAccounts()


# For testing using sample response
sampleResponse: getAccounts = getAccounts({
    'result': {
        'code': '1',
        'description': 'Success',
        'errorUUID': None
    },
    'accounts': {
        'accounts': [
            {
                'accountId': 1,
                'accountName': 'Ming',
                'availableCredit': 1
            }
        ]
    }
})

Tan Jia Ming
  • 435
  • 1
  • 6
  • 22