Understanding hydra-zen’s Automatic Type Refinement Behavior#

hydra-zen’s config-creation functions will automatically refine user-supplied type annotations so that they are made compatible with Hydra’s limited type support. This eliminates a friction that will otherwise force users to modify their code’s type annotations. Furthermore, hydra-zen preserves type information such that third-party type checkers can be used to provide more robust support for more general type annotations.

Hydra’s Limited Type Support#

Hydra permits only a narrow subset of type annotations to be present in a config:

  • Any

  • Primitive types (int, bool, float, str, Enums)

  • Nesting of Structured Configs

  • Containers (List and Dict) containing primitives or Structured Configs

  • Optional fields

  • Nested containers (added in OmegaConf v2.2.0)

Annotations that fall outside of this subset will cause Hydra to raise an error. This means that the following config:

from typing_extensions import Literal
from dataclasses import dataclass

@dataclass
class A:
    x: Literal[1, 2]

will cause Hydra to raise an error during instantiation, due to the presence of an unsupported type-annotation (Literal[1, 2]). E.g.

>>> from hydra_zen import instantiate
>>> instantiate(A, x=1)
ConfigTypeError   Traceback (most recent call last) [...]

This behavior can be highly undesirable as it requires users to modify their code and use less-accurate type-annotations.

Leveraging Automatic Type-Refinement (without having to think about it)#

To eliminate the friction that occurs here, hydra-zen’s config-creation functions will automatically-broaden the type annotations on the configs that they produce. This means that users will not need to change their type-annotations for the sake of Hydra.

For example, to address the incompatibility seen in the previous example, we can use builds().

>>> from hydra_zen import builds
>>> BuildsA = builds(A, populate_full_signature=True)
>>> instantiate(BuildsA, x=1)
A(x=1)

This type-broadening behavior will try to preserve as much type information as it can without creating compatibility issues with Hydra. For example, suppose that we want to configure the following function:

from typing import List

def func(ones_and_twos: List[Literal[1, 2]]):
    return ones_and_twos

As we saw above, Literal[1, 2] is not supported by Hydra. That being said, List is supported, thus builds() will “broaden” List[Literal[1, 2]] to List[Any].

# signature: `Builds_func(ones_and_twos: List[Any])`
Builds_func = builds(func, populate_full_signature=True)

In this way, we can still configure and build this function, but we also retain some level of type-validation

>>> instantiate(Builds_func, ones_and_twos="not a list")
---------------------------------------------------------------------------------
ValidationError: Invalid value assigned : str is not a ListConfig, list or tuple.
 full_key:
 object_type=None

>>> instantiate(Builds_func, ones_and_twos=[1, 2, 3])
[1, 2, 3]

In general, hydra-zen will broaden types as-needed so that dynamically-generated configs will never include annotations that would cause Hydra to raise an error due to lack of support for that type.

Using Third-Party Runtime Type-Checkers#

Although hydra-zen will broaden the types that get exposed to Hydra, the original type-information of a target that is provided to builds() is still preserved. This means that third-party type checkers like pydantic and beartype can be used to provide higher quality type-checking functionality.

E.g. let’s return to the original example involving the dataclass A. Assuming that we have installed pydantic, we can use it to recreate this dataclass so that it will perform general, runtime type-checking for us.

from pydantic.dataclasses import dataclass as pyd_dataclass

@pyd_dataclass
class A:
    x: Literal[1, 2]

BuildsA = builds(A, populate_full_signature=True)

As we saw earlier, Hydra will no longer complain about this type-annotation.

>>> instantiate(BuildsA, x=1)
A(x=1)

But now pydantic will actually ensure that x is either 1 or 2.

>>> instantiate(BuildsA, x=-10)
ValidationError: 1 validation error for A
x
unexpected value; permitted: 1, 2 (type=value_error.const; given=-10; permitted=(1, 2)  )

hydra-zen also provides support for leveraging these third-party type-checkers directly, via the zen_wrappers feature of builds(). See Runtime Data Validation for more details.