How do I pass arguments to custom static type hints in Python 3?

Viewed 1573

I'm a big fan and advocate for static type hints in Python 3. I've been using them for a while with no problems.

I just ran into a new edge case that I can't seem to compile. What if I want to define a custom type, then define its parameters?

For example, this is common in Python 3:

from typing import List, NewType
CustomObject = NewType('CustomObject', List[int])

def f(data: List[CustomObject]):
    # do something

But this won't compile:

class MyContainer():
    # some class definition ...

from typing import NewType
SpecialContainer = NewType('SpecialContainer', MyContainer)

def f(data: SpecialContainer[str]):
    # do something

I realize that SpecialContainer is technically a function in this case, but it shouldn't be evaluated as one in the context of a type signature. The second code snippet fails with TypeError: 'function' object is not subscriptable.

1 Answers

Compiling My Code Sample

You have to design your classes from the ground up to accept static type hints. This didn't satisfy my original use case, since I was trying to declare special subtypes of 3rd party classes, but it compiles my code sample.

from typing import Generic, TypeVar, Sequence, List

# Declare your own accepted types for your container, required
T = TypeVar('T', int, str, float)

# The custom container has be designed to accept types hints
class MyContainer(Sequence[T]):
    # some class definition ...

# Now, you can make a special container type
# Note that Sequence is a generic of List, and T is a generic of str, as defined above
SpecialContainer = TypeVar('SpecialContainer', MyContainer[List[str]])

# And this compiles
def f(data: SpecialContainer):
    # do something

Subtyping a 3rd Party Class

My original intention was to create a type hint that explained how a function, f(), took a pd.DataFrame object that was indexed by integers and whose cells were all strings. Using the above answer, I came up with a contrived way of expressing this.

from typing import Mapping, TypeVar, NewType, NamedTuple
from pandas import pd

# Create custom types, required even if redundant
Index = TypeVar('Index')
Row = TypeVar('Row')

# Create a child class of pd.DataFrame that includes a type signature
# Note that Mapping is a generic for a key-value store
class pdDataFrame(pd.DataFrame, Mapping[Index, Row]):
    pass

# Now, this compiles, and explains what my special pd.DataFrame does
pdStringDataFrame = NewType('pdDataFrame', pdDataFrame[int, NamedTuple[str]])

# And this compiles
def f(data: pdStringDataFrame):
    pass

Was it worth it?

  • If you are writing a custom class that resembles a container generic like Sequence, Mapping, or Any, then go for it. It is free to add the type variable to your class definition.

  • If you are trying to notate a specific usage of a 3rd party class that doesn't implement type hints:

    • Try using an existing type variable to get your point across, e.g. MyOrderedDictType = NewType('MyOrderedDictType', Dict[str, float])
    • If that doesn't work, you'll have to clutter your namespace with trivial child classes and type variables to get the type hint to compile. Better to use a docstring or comment to explain your situation.
Related