Repository: mit-ll-responsible-ai/hydra-zen

Issue:
Add `hydra_defaults` argument to `builds` 
Hydra supports specifying `defaults` on targeted configs:

```python
from dataclasses import dataclass, field
from typing import Any

from hydra_zen import launch

import hydra
from hydra.core.config_store import ConfigStore


class MyClass:
    def __init__(self, **kwargs):
        self.kwargs = kwargs


Default = builds(int)

cs = ConfigStore.instance()
cs.store(group="x", name="a", node=Default)

@dataclass
class Conf:
    _target_: str = "__main__.MyClass"
    defaults: Any = field(default_factory=lambda: ["_self_", {"x": "a"}])
    x: Any = None
    
cs.store(name="my_app", node=Conf)  

@hydra.main(config_path=None, config_name="my_app")
def task_function(cfg):
    print(instantiate(cfg).kwargs)
```

Launching the task function sets the default and _does not_ pass `defaults` to `MyClass(...)`

```python
>>> launch(Conf, task_function);
{'x': 0}
```


Presently, `builds` can accommodate this via meta-field:

```python
builds(MyClass, x=None, zen_meta=dict(defaults=["_self_", {"x": "a"}]))
```

But it might be nice to expose this as an explicit argument, `hydra_defaults`:

```python
builds(MyClass, x=None, hydra_defaults=["_self_", {"x": "a"}])
```

The "pros" of adding `hydra_defaults` are:
- It is explicit
- It is a Hydra feature, and thus people might expect to see parity with `hydra_convert` and other hydra features.
- It does not require users to understand meta-fields
- The config-creation code and resulting yaml will be cleaner

The "cons" are:
- It might be quite rare that people specify `defaults` on a targeted config
- The meta-field workflow already works! Plus the meta-field is explicitly excluded, by hydra-zen, from all instantiation


Discussion:

Patch:
diff --git a/docs/source/tutorials/config_groups.rst b/docs/source/tutorials/config_groups.rst
index 674fae4d..cb5cc616 100644
--- a/docs/source/tutorials/config_groups.rst
+++ b/docs/source/tutorials/config_groups.rst
@@ -114,7 +114,7 @@ default config for that group. Let's specify the ``CharConf`` config, which we n
 .. code-block:: python
    :caption: Specifying the player-group item named ``base`` as the default player-profile
 
-   Config = make_config("player", defaults=["_self_", {"player": "base"}])
+   Config = make_config("player", hydra_defaults=["_self_", {"player": "base"}])
    cs.store(name="my_app", node=Config)
 
 .. note:: 
@@ -178,7 +178,7 @@ over. Modify your ``my_app.py`` script to match the following code.
    cs.store(group="player", name="rakesh", node=rakesh_conf)
    
    # Specify default group for player to be: base
-   Config = make_config("player", defaults=["_self_", {"player": "base"}])
+   Config = make_config("player", hydra_defaults=["_self_", {"player": "base"}])
    
    cs.store(name="my_app", node=Config)
    
diff --git a/docs/source/tutorials/inject_wrapper.rst b/docs/source/tutorials/inject_wrapper.rst
index 5c2fc562..e099a8c1 100644
--- a/docs/source/tutorials/inject_wrapper.rst
+++ b/docs/source/tutorials/inject_wrapper.rst
@@ -171,7 +171,7 @@ over. Modify your ``my_app.py`` script to match the following code.
    cs.store(group="player", name="rakesh", node=rakesh_conf)
    
    
-   Config = make_config("player", defaults=["_self_", {"player": "base"}])
+   Config = make_config("player", hydra_defaults=["_self_", {"player": "base"}])
    
    cs.store(name="my_app", node=Config)
    
diff --git a/src/hydra_zen/structured_configs/_globals.py b/src/hydra_zen/structured_configs/_globals.py
index 87029d8b..67732f6c 100644
--- a/src/hydra_zen/structured_configs/_globals.py
+++ b/src/hydra_zen/structured_configs/_globals.py
@@ -15,6 +15,7 @@ PARTIAL_FIELD_NAME: Final[str] = "_partial_"
 RECURSIVE_FIELD_NAME: Final[str] = "_recursive_"
 CONVERT_FIELD_NAME: Final[str] = "_convert_"
 POS_ARG_FIELD_NAME: Final[str] = "_args_"
+DEFAULTS_LIST_FIELD_NAME: Final[str] = "defaults"
 
 _names = [
     TARGET_FIELD_NAME,
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index b59b6761..4ffda23c 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -58,6 +58,7 @@ from hydra_zen.typing import (
 from hydra_zen.typing._implementations import (
     BuildsWithSig,
     DataClass_,
+    DefaultsList,
     Field,
     HasTarget,
     InstOrType,
@@ -65,6 +66,7 @@ from hydra_zen.typing._implementations import (
 
 from ._globals import (
     CONVERT_FIELD_NAME,
+    DEFAULTS_LIST_FIELD_NAME,
     GET_OBJ_LOCATION,
     HYDRA_FIELD_NAMES,
     JUST_FIELD_NAME,
@@ -567,6 +569,7 @@ def builds(
     zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
     hydra_recursive: Optional[bool] = ...,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+    hydra_defaults: Optional[DefaultsList] = ...,
     dataclass_name: Optional[str] = ...,
     builds_bases: Tuple[()] = ...,
     frozen: bool = ...,
@@ -585,6 +588,7 @@ def builds(
     zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
     hydra_recursive: Optional[bool] = ...,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+    hydra_defaults: Optional[DefaultsList] = ...,
     dataclass_name: Optional[str] = ...,
     builds_bases: Tuple[Type[DataClass_], ...] = ...,
     frozen: bool = ...,
@@ -604,6 +608,7 @@ def builds(
     zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
     hydra_recursive: Optional[bool] = ...,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+    hydra_defaults: Optional[DefaultsList] = ...,
     dataclass_name: Optional[str] = ...,
     builds_bases: Tuple[Type[DataClass_], ...] = ...,
     frozen: bool = ...,
@@ -623,6 +628,7 @@ def builds(
     zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
     hydra_recursive: Optional[bool] = ...,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+    hydra_defaults: Optional[DefaultsList] = ...,
     dataclass_name: Optional[str] = ...,
     builds_bases: Tuple[Type[DataClass_], ...] = ...,
     frozen: bool = ...,
@@ -645,6 +651,7 @@ def builds(
     zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
     hydra_recursive: Optional[bool] = ...,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+    hydra_defaults: Optional[DefaultsList] = ...,
     dataclass_name: Optional[str] = ...,
     builds_bases: Tuple[Type[DataClass_], ...] = ...,
     frozen: bool = ...,
@@ -665,6 +672,7 @@ def builds(
     populate_full_signature: bool = False,
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_defaults: Optional[DefaultsList] = None,
     frozen: bool = False,
     builds_bases: Tuple[Type[DataClass_], ...] = (),
     dataclass_name: Optional[str] = None,
@@ -674,7 +682,7 @@ def builds(
     Type[PartialBuilds[Importable]],
     Type[BuildsWithSig[Type[R], P]],
 ]:
-    """builds(hydra_target, /, *pos_args, zen_partial=False, zen_wrappers=(), zen_meta=None, populate_full_signature=False, hydra_recursive=None, hydra_convert=None, frozen=False, dataclass_name=None, builds_bases=(), **kwargs_for_target)
+    """builds(hydra_target, /, *pos_args, zen_partial=False, zen_wrappers=(), zen_meta=None, populate_full_signature=False, hydra_recursive=None, hydra_convert=None, hydra_defaults=None, frozen=False, dataclass_name=None, builds_bases=(), **kwargs_for_target)
 
     Returns a structured config, which describes how to instantiate/call
     ``<hydra_target>`` with both user-specified and auto-populated parameter values.
@@ -757,6 +765,11 @@ def builds(
 
         If ``None``, the ``_convert_`` attribute is not set on the resulting config.
 
+    hydra_defaults : List['_self_' | Dict[str, str]] | None, optional (default = None)
+        A list in an input config that instructs Hydra how to build the output config
+        [7][8]. Each input config can have a Defaults List as a top level element. The
+        Defaults List itself is not a part of output config.
+
     frozen : bool, optional (default=False)
         If ``True``, the resulting config will create frozen (i.e. immutable) instances.
         I.e. setting/deleting an attribute of an instance will raise
@@ -809,6 +822,8 @@ def builds(
     .. [4] https://hydra.cc/docs/next/advanced/instantiate_objects/overview/#parameter-conversion-strategies
     .. [5] https://docs.python.org/3/library/dataclasses.html
     .. [6] https://omegaconf.readthedocs.io/en/2.1_branch/usage.html#variable-interpolation
+    .. [7] https://hydra.cc/docs/next/tutorials/structured_config/defaults/
+    .. [8] https://hydra.cc/docs/next/advanced/defaults_list/
 
     See Also
     --------
@@ -1255,6 +1270,24 @@ def builds(
             (CONVERT_FIELD_NAME, str, _utils.field(default=hydra_convert, init=False))
         )
 
+    if hydra_defaults is not None:
+        if not _utils.valid_defaults_list(hydra_defaults):
+            raise HydraZenValidationError(
+                f"`hydra_defaults` must be type `list[str | dict[str, str | list[str]]`"
+                f", Got: {repr(hydra_defaults)}"
+            )
+        hydra_defaults = sanitize_collection(hydra_defaults)
+        base_fields.append(
+            (
+                DEFAULTS_LIST_FIELD_NAME,
+                List[Any],
+                _utils.field(
+                    default_factory=lambda: list(hydra_defaults),
+                    init=False,
+                ),
+            )
+        )
+
     if _pos_args:
         base_fields.append(
             (
diff --git a/src/hydra_zen/structured_configs/_make_config.py b/src/hydra_zen/structured_configs/_make_config.py
index bba25a18..faff8e20 100644
--- a/src/hydra_zen/structured_configs/_make_config.py
+++ b/src/hydra_zen/structured_configs/_make_config.py
@@ -14,10 +14,11 @@ from typing_extensions import Literal
 
 from hydra_zen._compatibility import PATCH_OMEGACONF_830
 from hydra_zen.structured_configs import _utils
+from hydra_zen.structured_configs._implementations import sanitize_collection
 from hydra_zen.typing import SupportedPrimitive
-from hydra_zen.typing._implementations import DataClass, DataClass_, Field
+from hydra_zen.typing._implementations import DataClass, DataClass_, DefaultsList, Field
 
-from ._globals import CONVERT_FIELD_NAME, RECURSIVE_FIELD_NAME
+from ._globals import CONVERT_FIELD_NAME, DEFAULTS_LIST_FIELD_NAME, RECURSIVE_FIELD_NAME
 from ._implementations import _retain_type_info, builds, sanitized_field
 
 __all__ = ["ZenField", "make_config"]
@@ -105,6 +106,7 @@ def make_config(
     *fields_as_args: Union[str, ZenField],
     hydra_recursive: Optional[bool] = None,
     hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+    hydra_defaults: Optional[DefaultsList] = None,
     config_name: str = "Config",
     frozen: bool = False,
     bases: Tuple[Type[DataClass_], ...] = (),
@@ -146,6 +148,13 @@ def make_config(
 
         If ``None``, the ``_convert_`` attribute is not set on the resulting config.
 
+
+    hydra_defaults : List['_self_' | Dict[str, str]] | None, optional (default = None)
+        A list in an input config that instructs Hydra how to build the output config
+        [7][8]. Each input config can have a Defaults List as a top level element. The
+        Defaults List itself is not a part of output config.
+
+
     bases : Tuple[Type[DataClass], ...], optional (default=())
         Base classes that the resulting config class will inherit from.
 
@@ -203,6 +212,8 @@ def make_config(
     .. [4] https://docs.python.org/3/library/dataclasses.html
     .. [5] https://hydra.cc/docs/next/tutorials/structured_config/intro/#structured-configs-supports
     .. [6] https://docs.python.org/3/library/dataclasses.html#default-factory-functions
+    .. [7] https://hydra.cc/docs/next/tutorials/structured_config/defaults/
+    .. [8] https://hydra.cc/docs/next/advanced/defaults_list/
 
     Examples
     --------
@@ -352,12 +363,23 @@ def make_config(
                 )
         del all_names
 
+    if "defaults" in fields_as_kwargs:
+        if hydra_defaults is not None:
+            raise TypeError(
+                "`defaults` and `hydra_defaults` cannot be specified simultaneously"
+            )
+        _defaults = fields_as_kwargs.pop("defaults")
+
+        if not isinstance(_defaults, ZenField):
+            hydra_defaults = _defaults  # type: ignore
+
     # validate hydra-args via `builds`
     # also check for use of reserved names
     builds(
         dict,
         hydra_convert=hydra_convert,
         hydra_recursive=hydra_recursive,
+        hydra_defaults=hydra_defaults,
         **fields_as_kwargs,
     )
 
@@ -430,6 +452,16 @@ def make_config(
             (CONVERT_FIELD_NAME, str, _utils.field(default=hydra_convert, init=False))
         )
 
+    if hydra_defaults is not None:
+        hydra_defaults = sanitize_collection(hydra_defaults)
+        config_fields.append(
+            (
+                DEFAULTS_LIST_FIELD_NAME,
+                List[Any],
+                _utils.field(default_factory=lambda: list(hydra_defaults), init=False),
+            )
+        )
+
     return cast(
         Type[DataClass],
         make_dataclass(
diff --git a/src/hydra_zen/structured_configs/_make_custom_builds.py b/src/hydra_zen/structured_configs/_make_custom_builds.py
index e80fe25c..67a044de 100644
--- a/src/hydra_zen/structured_configs/_make_custom_builds.py
+++ b/src/hydra_zen/structured_configs/_make_custom_builds.py
@@ -269,7 +269,7 @@ def make_custom_builds_fn(
     <Validation error: "c" is not "a" or "b">
     """
 
-    excluded_fields = {"dataclass_name"}
+    excluded_fields = {"dataclass_name", "hydra_defaults"}
     LOCALS = locals()
 
     # Ensures that new defaults added to `builds` must be reflected
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index 01837288..5f721dc6 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -31,6 +31,7 @@ from hydra_zen._compatibility import (
     HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES,
     PATCH_OMEGACONF_830,
 )
+from hydra_zen.errors import HydraZenValidationError
 from hydra_zen.typing._implementations import DataClass_, Field, InterpStr
 
 try:
@@ -430,3 +431,39 @@ def mutable_default_permitted(bases: Iterable[DataClass_], field_name: str) -> b
             # see https://github.com/omry/omegaconf/issues/830
             return False
     return True
+
+
+def valid_defaults_list(hydra_defaults: Any) -> bool:
+    """
+    Raises
+    ------
+    HydraZenValidationError: Duplicate _self_ entries"""
+    if not isinstance(hydra_defaults, list):
+        return False
+
+    has_self = False
+    for item in hydra_defaults:
+
+        if item == "_self_":
+            if not has_self:
+                has_self = True
+                continue
+            raise HydraZenValidationError(
+                "`hydra_defaults` cannot have more than one '_self_' entry"
+            )
+
+        if not isinstance(item, (dict, str)):
+            return False
+
+        if isinstance(item, dict) and any(
+            not isinstance(k, str) or not isinstance(v, (str, list))
+            for k, v in item.items()
+        ):
+            return False
+
+    if not has_self:
+        warnings.warn(
+            "Defaults list is missing `_self_`. See https://hydra.cc/docs/upgrades/1.0_to_1.1/default_composition_order for more information",
+            category=UserWarning,
+        )
+    return True
diff --git a/src/hydra_zen/typing/_builds_overloads.py b/src/hydra_zen/typing/_builds_overloads.py
index 5995631e..43d034b3 100644
--- a/src/hydra_zen/typing/_builds_overloads.py
+++ b/src/hydra_zen/typing/_builds_overloads.py
@@ -22,6 +22,7 @@ from ._implementations import (
     Builds,
     BuildsWithSig,
     DataClass_,
+    DefaultsList,
     Importable,
     PartialBuilds,
     SupportedPrimitive,
@@ -48,6 +49,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[()] = ...,
         frozen: bool = ...,
@@ -66,6 +68,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -85,6 +88,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -104,6 +108,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -126,6 +131,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -147,6 +153,7 @@ class StdBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = None,
         hydra_recursive: Optional[bool] = None,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+        hydra_defaults: Optional[DefaultsList] = None,
         frozen: bool = False,
         builds_bases: Tuple[Type[DataClass_], ...] = (),
         dataclass_name: Optional[str] = None,
@@ -174,6 +181,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[()] = ...,
         frozen: bool = ...,
@@ -192,6 +200,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -211,6 +220,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -230,6 +240,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         frozen: bool = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         dataclass_name: Optional[str] = ...,
@@ -249,6 +260,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         frozen: bool = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         dataclass_name: Optional[str] = ...,
@@ -270,6 +282,7 @@ class FullBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = None,
         hydra_recursive: Optional[bool] = None,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+        hydra_defaults: Optional[DefaultsList] = None,
         frozen: bool = False,
         builds_bases: Tuple[Type[DataClass_], ...] = (),
         dataclass_name: Optional[str] = None,
@@ -297,6 +310,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -316,6 +330,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[()] = ...,
         frozen: bool = ...,
@@ -334,6 +349,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -356,6 +372,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_wrappers: ZenWrappers[Callable[..., Any]] = ...,
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -379,6 +396,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = ...,
         hydra_recursive: Optional[bool] = ...,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = ...,
+        hydra_defaults: Optional[DefaultsList] = ...,
         dataclass_name: Optional[str] = ...,
         builds_bases: Tuple[Type[DataClass_], ...] = ...,
         frozen: bool = ...,
@@ -400,6 +418,7 @@ class PBuilds(Protocol):  # pragma: no cover
         zen_meta: Optional[Mapping[str, SupportedPrimitive]] = None,
         hydra_recursive: Optional[bool] = None,
         hydra_convert: Optional[Literal["none", "partial", "all"]] = None,
+        hydra_defaults: Optional[DefaultsList] = None,
         frozen: bool = False,
         builds_bases: Tuple[Type[DataClass_], ...] = (),
         dataclass_name: Optional[str] = None,
diff --git a/src/hydra_zen/typing/_implementations.py b/src/hydra_zen/typing/_implementations.py
index 4296dcca..665b87c7 100644
--- a/src/hydra_zen/typing/_implementations.py
+++ b/src/hydra_zen/typing/_implementations.py
@@ -16,6 +16,7 @@ from typing import (
     Dict,
     FrozenSet,
     Generic,
+    List,
     Mapping,
     NewType,
     Optional,
@@ -249,3 +250,6 @@ else:
     # cleans up annotations for REPLs
     class ZenWrappers(Generic[T2]):  # pragma: no cover
         pass
+
+
+DefaultsList = List[Union[str, Mapping[str, Union[str, List[str]]]]]


Test patch:
diff --git a/tests/annotations/declarations.py b/tests/annotations/declarations.py
index 1ff16c3b..e80ab089 100644
--- a/tests/annotations/declarations.py
+++ b/tests/annotations/declarations.py
@@ -1022,3 +1022,24 @@ def check_instantiate_overrides(
     reveal_type(instantiate(complex_), expected_text="complex")
     reveal_type(instantiate(path_), expected_text="Path")
     reveal_type(instantiate(dataclass_), expected_text="Any")
+
+
+def check_hydra_defaults(
+    partial_builds: PBuilds, full_builds: FullBuilds, std_builds: StdBuilds
+):
+
+    builds(int, hydra_defaults=["_self_", {"a": "b"}])
+    partial_builds(int, hydra_defaults=["_self_", {"a": "b"}])
+    full_builds(int, hydra_defaults=["_self_", {"a": "b"}])
+    std_builds(int, hydra_defaults=["_self_", {"a": "b"}])
+    make_config(hydra_defaults=["_self_", {"a": "b"}])
+
+    builds(int, hydra_defaults=1)  # type: ignore
+    partial_builds(int, hydra_defaults=1)  # type: ignore
+    full_builds(int, hydra_defaults=1)  # type: ignore
+    std_builds(int, hydra_defaults=1)  # type: ignore
+    make_config(hydra_defaults=1)  # type: ignore
+
+    builds(int, hydra_defaults=["_self_", {"a": "b"}, {1: 1}])  # type: ignore
+    builds(int, hydra_defaults={"a": "b"})  # type: ignore
+    builds(int, hydra_defaults="_self_")  # type: ignore
diff --git a/tests/conftest.py b/tests/conftest.py
index df11f47d..ddb88ccc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,10 +1,12 @@
 # Copyright (c) 2022 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
 
+
 import logging
 import os
 import sys
 import tempfile
+from typing import Iterable
 
 import hypothesis.strategies as st
 import pkg_resources
@@ -43,7 +45,7 @@ if sys.version_info < (3, 7):
 
 
 @pytest.fixture()
-def cleandir():
+def cleandir() -> Iterable[str]:
     """Run function in a temporary directory."""
     with tempfile.TemporaryDirectory() as tmpdirname:
         old_dir = os.getcwd()  # get current working directory (cwd)
diff --git a/tests/custom_strategies.py b/tests/custom_strategies.py
index eefdec28..71d3b6c2 100644
--- a/tests/custom_strategies.py
+++ b/tests/custom_strategies.py
@@ -53,8 +53,8 @@ def _compat_frozen(conf: Dict[str, Any]):
 def valid_builds_args(*required: str, excluded: Sequence[str] = ()):
     """Generates valid inputs for all nameable args in `builds`, except `dataclass_name`."""
     assert len(required) == len(set(required))
-    _required = set(required)  # type: ignore
-    _excluded = set(excluded)  # type: ignore
+    _required = set(required)
+    _excluded = set(excluded)
     assert _required.isdisjoint(_excluded)
 
     assert _required <= set(_valid_builds_strats), _required - set(_valid_builds_strats)
diff --git a/tests/test_custom_strategies.py b/tests/test_custom_strategies.py
index 0b1ac0d0..90a82a1a 100644
--- a/tests/test_custom_strategies.py
+++ b/tests/test_custom_strategies.py
@@ -26,7 +26,9 @@ def test_valid_build_strats_are_exhaustive():
         for n, p in inspect.signature(builds).parameters.items()
         if p.kind is p.KEYWORD_ONLY
     )
-    assert nameable_builds_args - {"dataclass_name"} == set(_valid_builds_strats)
+    assert nameable_builds_args - {"dataclass_name", "hydra_defaults"} == set(
+        _valid_builds_strats
+    )
 
 
 @given(req_excl=partitions(tuple(_valid_builds_strats), ordered=False), data=st.data())
diff --git a/tests/test_defaults_list.py b/tests/test_defaults_list.py
new file mode 100644
index 00000000..5e654c85
--- /dev/null
+++ b/tests/test_defaults_list.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2022 Massachusetts Institute of Technology
+# SPDX-License-Identifier: MIT
+
+import random
+from typing import Any
+
+import hypothesis.strategies as st
+import pytest
+from hydra.core.config_store import ConfigStore
+from hypothesis import given
+
+from hydra_zen import builds, instantiate, launch, make_config
+from hydra_zen.errors import HydraZenValidationError
+
+
+def test_hydra_defaults_work_builds():
+    config_store = ConfigStore.instance()
+    config_store.store(group="x", name="a", node=builds(int, 10))
+    Conf = builds(dict, x=None, y="hi", hydra_defaults=["_self_", {"x": "a"}])
+    job = launch(Conf, instantiate)
+    assert job.return_value == {"x": 10, "y": "hi"}
+
+
+def test_hydra_defaults_work_make_config():
+    config_store = ConfigStore.instance()
+    config_store.store(group="x", name="a", node=builds(int, 10))
+    Conf = make_config(x=None, y="hi", hydra_defaults=["_self_", {"x": "a"}])
+    job = launch(Conf, instantiate)
+    assert job.return_value == {"x": 10, "y": "hi"}
+
+
+invalid_defaults = st.sampled_from(
+    [1, [1], [{"a": 1}], {"a": 1}, {1: "a"}, {1: 1}, False, True]
+)
+valid_defaults = st.sampled_from(["a", {"a": "b"}, {"a": ["a", "b"]}])
+
+
+@pytest.mark.filterwarnings("ignore::UserWarning")
+@given(
+    invalids=invalid_defaults | st.lists(invalid_defaults, min_size=1),
+    valids=st.lists(valid_defaults, min_size=0),
+    include_self=st.sampled_from([None, "pre", "post"]),
+)
+def test_hydra_defaults_validation(invalids: Any, valids: list, include_self):
+    if isinstance(invalids, list):
+        defaults = invalids + valids
+        random.shuffle(defaults)
+        if include_self == "pre":
+            defaults = ["_self_"] + defaults
+        elif include_self == "pose":
+            defaults.append("_self_")
+    else:
+        defaults = invalids
+
+    with pytest.raises(HydraZenValidationError):
+        builds(dict, hydra_defaults=defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        make_config(hydra_defaults=defaults)
+
+
+@given(
+    selfs=st.lists(st.just("_self_"), min_size=2, max_size=5),
+    others=st.lists(st.sampled_from([dict(a="a"), dict(b="b")]), max_size=5),
+)
+def test_hydra_defaults_raises_multiple_self(selfs: list, others: list):
+    defaults = selfs + others
+    random.shuffle(defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        builds(dict, hydra_defaults=defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        make_config(hydra_defaults=defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        make_config(defaults=defaults)
+
+
+def test_no_self_in_defaults_warns():
+    with pytest.warns(UserWarning):
+        builds(dict, hydra_defaults=[{"a": "b"}])
+
+    with pytest.warns(UserWarning):
+        make_config(hydra_defaults=[{"a": "b"}])
+
+
+def test_redundant_defaults_in_make_config_raises():
+    with pytest.raises(TypeError):
+        make_config(defaults=["_self_"], hydra_defaults=["_self_"])
diff --git a/tests/test_hydra_behaviors.py b/tests/test_hydra_behaviors.py
index 7fd7bc5b..a32c9f7d 100644
--- a/tests/test_hydra_behaviors.py
+++ b/tests/test_hydra_behaviors.py
@@ -1,6 +1,6 @@
 # Copyright (c) 2022 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
-
+import random
 from dataclasses import dataclass
 from typing import Any, List, Tuple
 
@@ -10,7 +10,14 @@ from hypothesis import given
 from omegaconf import DictConfig, ListConfig
 from omegaconf.errors import ValidationError
 
-from hydra_zen import builds, hydrated_dataclass, instantiate, mutable_value
+from hydra_zen import (
+    builds,
+    hydrated_dataclass,
+    instantiate,
+    make_config,
+    mutable_value,
+)
+from hydra_zen.errors import HydraZenValidationError
 from hydra_zen.structured_configs._utils import get_obj_path
 
 
@@ -202,3 +209,60 @@ def test_type_checking():
 
     # should be ok
     instantiate(builds(g2, x=[conf_C, conf_C]))
+
+
+invalid_defaults = st.sampled_from(
+    [1, [1], [{"a": 1}], {"a": 1}, {1: "a"}, {1: 1}, False, True]
+)
+valid_defaults = st.sampled_from(["a", {"a": "b"}, {"a": ["a", "b"]}])
+
+
+@pytest.mark.filterwarnings("ignore::UserWarning")
+@given(
+    invalids=invalid_defaults | st.lists(invalid_defaults, min_size=1),
+    valids=st.lists(valid_defaults, min_size=0),
+    include_self=st.sampled_from([None, "pre", "post"]),
+)
+def test_hydra_defaults_validation(invalids: Any, valids: list, include_self):
+    if isinstance(invalids, list):
+        defaults = invalids + valids
+        random.shuffle(defaults)
+        if include_self == "pre":
+            defaults = ["_self_"] + defaults
+        elif include_self == "pose":
+            defaults.append("_self_")
+    else:
+        defaults = invalids
+
+    with pytest.raises(HydraZenValidationError):
+        builds(dict, hydra_defaults=defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        make_config(hydra_defaults=defaults)
+
+
+@given(
+    selfs=st.lists(st.just("_self_"), min_size=2, max_size=5),
+    others=st.lists(st.sampled_from([dict(a="a"), dict(b="b")]), max_size=5),
+)
+def test_hydra_defaults_raises_multiple_self(selfs: list, others: list):
+    defaults = selfs + others
+    random.shuffle(defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        builds(dict, hydra_defaults=defaults)
+
+    with pytest.raises(HydraZenValidationError):
+        make_config(hydra_defaults=defaults)
+
+
+def test_no_self_in_defaults_warns():
+    with pytest.warns(UserWarning):
+        builds(dict, hydra_defaults=[{"a": "b"}])
+
+    with pytest.warns(UserWarning):
+        make_config(hydra_defaults=[{"a": "b"}])
+
+
+def test_redundant_defaults_in_make_config_raises():
+    pass