Search code examples
pythonmypypython-typingtype-alias

How to create a type alias with a throw-away generic?


I am looking to create a type alias that can take a generic that is not actually used in the alias. The reason is for self-documenting code, I am aware that no type checker will be able to actually check this. Here is what I mean:

from typing import TypeVar, Dict, Any

from dataclasses import dataclass
from dataclasses_json import dataclass_json, DataClassJsonMixin


JSON = Dict[str, Any]  # Good enough json type for this demo
T = TypeVar("T", bound=DataClassJsonMixin)
JSONOf = JSON[T]  # <-- mypy Problem: JSON takes no generic arguments

@dataclass_json   # this just provides to_json/from_json to dataclasses, not important for question
@dataclass
class Person:
    name: str
    age: int

def add_person_to_db(person: JSONOf[Person]):
    # just from reading the signature the user now knows what the input is
    # If I just put JSON as a type, it is unclear
    ...

The problem is that the JSON type alias does not use the generic parameter, so it can not receive it. I would need to use it in the alias definition without actually doing something with it. I tried doing this with the PEP-593 Annotated type, but it does not work either (I can define it like below, but it still does not expect the generic argument):

from typing import Annotated

JSONOf = Annotated[JSON, T]

I would need to do something like (this does not exist):

from typing import ThrowAwayGeneric

JSONOf = ThrowAwayGeneric[JSON, T]

Is it possible to get something similar to type check? Once again: I am not actually interested in type checking the json data, I just want to provide a readable signature of the function. Using Dict[str, Any] when actually type checking if completely fine.

The kind of brute-force approach would of course be to define an alias for each type, but that becomes tedious as there are many such types.

JSONOfPerson = JSON

Solution

  • I've came up with a few solutions:

    • Make JSONOf a union of JSON with another type that takes a Generic, crafting that type a way that it will never match anything, such as a Dict with mutable keys:
    JSONOf = Union[JSON, Dict[List, T]] # List could be any mutable, including JSON itself
    
    • Same approach, uniting with a custom generic class:
    class JSONFrom(Generic[T]): pass  # Name was chosen to look nice on mypy errors
    JSONOf = Union[JSON, JSONFrom[T]]
    

    Let's test it:

    from typing import Generic, TypeVar, Dict, Any, Union
    
    class DataClassJsonMixin: ...
    class Person(DataClassJsonMixin): ...
    JSON = Dict[str, Any]  # Good enough json type for this demo
    T = TypeVar("T", bound=DataClassJsonMixin)
    
    # Solution 1: Union with impossible type that takes a Generic
    # JSONOf = Union[JSON, Dict[JSON, T]]
    
    # Solution 2: Union with custom generic class
    class JSONFrom(Generic[T]):
        # just a crude failsafe to prohibit instantiation
        # could be improved with __new__, __init_subclasses__, etc
        def __init__(self):
            raise TypeError("Just dont!")
    
    JSONOf = Union[JSON, JSONFrom[T]]
    
    def add_person_to_db(person: JSONOf[Person]):
        print(person)
        try: reveal_type(person)
        except NameError: pass
    
    add_person_to_db({'id': 1234, 'name': 'Someone'})  # Checks
    add_person_to_db("Not a JSON")       # Check error by JSON
    add_person_to_db(Person())           # Make sure it does not accept Person
    try: someone: JSONFrom = JSONFrom()  # Make sure it does not accept JSONFrom
    except TypeError as e: print(e)      # Nice try!
    
    $ python3 test.py 
    {'id': 1234, 'name': 'Someone'}
    Not a JSON
    <__main__.Person object at 0x7fa258a7d128>
    Just dont!
    
    $ mypy test.py 
    test.py:22: note: Revealed type is "Union[builtins.dict[builtins.str, Any], test.JSONFrom[test.Person]]"
    test.py:26: error: Argument 1 to "add_person_to_db" has incompatible type "str"; expected "Union[Dict[str, Any], JSONFrom[Person]]"
    test.py:27: error: Argument 1 to "add_person_to_db" has incompatible type "Person"; expected "Union[Dict[str, Any], JSONFrom[Person]]"
    Found 2 errors in 1 file (checked 1 source file)