How to define Python 'Missing' type annotation?

Viewed 129

let's say I have a simple HTTP endpoint accepting JSON payload form the user:

  • here the user passed None explicitly:
payload = {
    "name": "Paul",
    "option": None
}
  • and here the user didn't provide option at all:
payload = {
    "name": "Paul",
}

For some reason, I want to 'normalize' such payloads into

payload = {
    "name": "Paul",
    "option": Missing(),
}

where Missing() is an instance of a class:

class Missing:
    """Represents missing value in a dictionary"""
    pass

so I can differentiate between user passing None explicitly and not passing option key at all. What I'm struggling to define is a custom "type constructor", such that I could then annotate function argument like so:

def process_user_option(option: Missing[Optional[str]]):
   ...do something with 'option'

and the semantics of Missing would be something like this:

Missing(x) -> Union[Optional[x], Missing]

Any suggestions? Thank you!

2 Answers

One suggestion:

from typing import  NewType, Optional, TypeVar

class MissingType: pass
Missing = MissingType()

T = TypeVar('T')
PossiblyMissing = T | MissingType


Name   = NewType("Name", str)
Option = NewType("Option", dict)

def process_user_option(option: PossiblyMissing[Optional[Option]]):
    if option is Missing:
        ...

    if option is None:
        ...

Additionally, you might want to declare optional keys in a TypedDict explicitly with total=False*.

from typing import TypedDict


class _PayloadBase(TypedDict):
    name: str


class Payload(_PayloadBase, total=False):
    option: Optional[str]  # optional key

in order to safely normalize and process the payload:

@final
class MissingType:
    pass


Missing = MissingType()


class NormalizedPaylaod(_PayloadBase):
    option: str | MissingType


def normalize_payload(payload: Payload) -> NormalizedPaylaod:
    option = Missing if (option := payload.get("option")) is None else option
    normalized_payload: NormalizedPaylaod = {
        "name": payload["name"],
        "option": option,
    }
    return normalized_payload


def process_payload(payload: NormalizedPaylaod) -> None:
    if payload["option"] is Missing:
        print("No option specified!")

*It will be much easier in Python 3.11 with PEP-655 (Required and NotRequired).

Related