When using an optional import, i.e. the package is only imported inside a function as I want it to be an optional dependency of my package, is there a way to type hint the return type of the function as one of the classes belonging to this optional dependency?
To give a simple example with pandas
as an optional dependency:
def my_func() -> pd.DataFrame:
import pandas as pd
return pd.DataFrame()
df = my_func()
In this case, since the import
statement is within my_func
, this code will, not surprisingly, raise:
NameError: name 'pd' is not defined
If the string literal type hint were used instead, i.e.:
def my_func() -> 'pd.DataFrame':
import pandas as pd
return pd.DataFrame()
df = my_func()
the module can now be executed without issue, but mypy
will complain:
error: Name 'pd' is not defined
How can I make the module execute successfully and retain the static type checking capability, while also having this import be optional?
Python has support for optional "type hints". These "type hints" are a special syntax that allow declaring the type of a variable. By declaring types for your variables, editors and tools can give you better support.
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.
Use Optional to indicate that an object is either one given type or None . For example: from typing import Dict, Optional, Union dict_of_users: Dict[int, Union[int,str]] = { 1: "Jerome", 2: "Lewis", 3: 32 } user_id: Optional[int] user_id = None # valid user_id = 3 # also vald user_id = "Hello" # not valid!
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.
Try sticking your import inside of an if typing.TYPE_CHECKING
statement at the top of your file. This variable is always false at runtime but is treated as always true for the purposes of type hinting.
For example:
# Lets us avoid needing to use forward references everywhere
# for Python 3.7+
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import pandas as pd
def my_func() -> pd.DataFrame:
import pandas as pd
return pd.DataFrame()
You can also do if False:
, but I think that makes it a little harder for somebody to tell what's going on.
One caveat is that this does mean that while pandas will be an optional dependency at runtime, it'll still be a mandatory one for the purposes of type checking.
Another option you can explore using is mypy's --always-true
and --always-false
flags. This would give you finer-grained control over which parts of your code are typechecked. For example, you could do something like this:
try:
import pandas as pd
PANDAS_EXISTS = True
except ImportError:
PANDAS_EXISTS = False
if PANDAS_EXISTS:
def my_func() -> pd.DataFrame:
return pd.DataFrame()
...then do mypy --always-true=PANDAS_EXISTS your_code.py
to type check it assuming pandas is imported and mypy --always-false=PANDAS_EXISTS your_code.py
to type check assuming it's missing.
This could help you catch cases where you accidentally use a function that requires pandas from a function that isn't supposed to need it -- though the caveats are that (a) this is a mypy-only solution and (b) having functions that only sometimes exist in your library might be confusing for the end-user.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With