diff --git a/zookeeper/hparam.py b/zookeeper/hparam.py
index 709f2ba..5416e0e 100644
--- a/zookeeper/hparam.py
+++ b/zookeeper/hparam.py
@@ -11,14 +11,21 @@ except ImportError:  # pragma: no cover
 
 SPLIT_REGEX = re.compile(r",?(\w+)=")
 
+INDENT = "    "
+
 
 def group(sequence):
     return zip(*[iter(sequence)] * 2)
 
 
-def str_key_val(key, value, color=True):
+def str_key_val(key, value, color=True, single_line=False):
     if callable(value):
         value = "<callable>"
+    if isinstance(value, HParams):
+        if single_line:
+            value = repr(value)
+        else:
+            value = f"\n{INDENT}".join(str(value).split("\n"))
     return f"{BLUE}{key}{RESET}={YELLOW}{value}{RESET}" if color else f"{key}={value}"
 
 
@@ -103,11 +110,16 @@ class HParams(collections.abc.Mapping):
         raise AttributeError("Hyperparameters are immutable, cannot assign to field.")
 
     def __str__(self):
-        params = ",\n    ".join([str_key_val(k, v) for k, v in sorted(self.items())])
-        return f"{self.__class__.__name__}(\n    {params}\n)"
+        params = f",\n{INDENT}".join(
+            [str_key_val(k, v) for k, v in sorted(self.items())]
+        )
+        return f"{self.__class__.__name__}(\n{INDENT}{params}\n)"
 
     def __repr__(self):
         params = ",".join(
-            [str_key_val(k, v, color=False) for k, v in sorted(self.items())]
+            [
+                str_key_val(k, v, color=False, single_line=True)
+                for k, v in sorted(self.items())
+            ]
         )
         return f"{self.__class__.__name__}({params})"

Support nested HParams
If we have a model with multiple independent components, e.g. an encoder and a decoder, or a base network and some auxilliary structure, it would be nice if the `HParams` class could nest other `HParams` classes, one for each of the components.

This would facilitate easy model modularity, e.g. a `build_model` function could internally call `build_encoder(hparams.encoder_hparams)` and `build_decoder(hparams.decoder_hparams)`.
diff --git a/zookeeper/hparam_test.py b/zookeeper/hparam_test.py
index fc97ed8..91e25ea 100644
--- a/zookeeper/hparam_test.py
+++ b/zookeeper/hparam_test.py
@@ -22,6 +22,20 @@ def hyper():
     return Hyper()
 
 
+@pytest.fixture
+def hyper_with_nested():
+    class Child(HParams):
+        c = -1.5
+        d = "aeiou"
+
+    class Parent(HParams):
+        a = 4.9
+        b = "some string"
+        child = Child()
+
+    return Parent()
+
+
 def test_defaults(hyper):
     assert hyper.foo == [1, 2, 3]
     assert hyper.bar == 0.5
@@ -72,6 +86,11 @@ def test_repr(hyper):
     assert repr(hyper) == output
 
 
+def test_repr_nested(hyper_with_nested):
+    output = "Parent(a=4.9,b=some string,child=Child(c=-1.5,d=aeiou))"
+    assert repr(hyper_with_nested) == output
+
+
 def test_str(hyper):
     output = """Hyper(
     bar=0.5,
@@ -81,3 +100,15 @@ def test_str(hyper):
     foo=[1, 2, 3]
 )"""
     assert click.unstyle(str(hyper)) == output
+
+
+def test_str_nested(hyper_with_nested):
+    output = """Parent(
+    a=4.9,
+    b=some string,
+    child=Child(
+        c=-1.5,
+        d=aeiou
+    )
+)"""
+    assert click.unstyle(str(hyper_with_nested)) == output

pip install -e .[test]
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
absl-py==1.4.0
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
click==8.1.8
click-completion==0.5.2
coverage==7.2.7
cycler==0.11.0
dill==0.3.7
dm-tree==0.1.8
etils==0.9.0
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
fonttools==4.38.0
googleapis-common-protos==1.69.2
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
importlib-resources==5.12.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.1.6
kiwisolver==1.4.5
MarkupSafe==2.1.5
matplotlib==3.5.3
numpy==1.21.6
packaging @ file:///croot/packaging_1671697413597/work
Pillow==9.5.0
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
promise==2.3
protobuf==3.20.3
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing==3.1.4
pytest==7.1.2
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
requests==2.31.0
shellingham==1.5.4
six==1.17.0
tensorflow-datasets==4.8.2
tensorflow-metadata==1.12.0
termcolor==2.3.0
toml==0.10.2
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tqdm==4.67.1
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==2.0.7
wrapt==1.16.0
zipp @ file:///croot/zipp_1672387121353/work
-e git+https://github.com/larq/zookeeper.git@eb5f593db0922bde8a2a49cad35b8dbf11be390a#egg=zookeeper

name: zookeeper
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - absl-py==1.4.0
      - charset-normalizer==3.4.1
      - click==8.1.8
      - click-completion==0.5.2
      - coverage==7.2.7
      - cycler==0.11.0
      - dill==0.3.7
      - dm-tree==0.1.8
      - etils==0.9.0
      - fonttools==4.38.0
      - googleapis-common-protos==1.69.2
      - idna==3.10
      - importlib-resources==5.12.0
      - jinja2==3.1.6
      - kiwisolver==1.4.5
      - markupsafe==2.1.5
      - matplotlib==3.5.3
      - numpy==1.21.6
      - pillow==9.5.0
      - promise==2.3
      - protobuf==3.20.3
      - psutil==7.0.0
      - pyparsing==3.1.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - requests==2.31.0
      - shellingham==1.5.4
      - six==1.17.0
      - tensorflow-datasets==4.8.2
      - tensorflow-metadata==1.12.0
      - termcolor==2.3.0
      - toml==0.10.2
      - tqdm==4.67.1
      - urllib3==2.0.7
      - wrapt==1.16.0
prefix: /opt/conda/envs/zookeeper

zookeeper/hparam_test.py::test_repr_nested zookeeper/hparam_test.py::test_str_nested
zookeeper/hparam_test.py::test_defaults zookeeper/hparam_test.py::test_parse zookeeper/hparam_test.py::test_spaced_parse zookeeper/hparam_test.py::test_parse_fail zookeeper/hparam_test.py::test_immutability zookeeper/hparam_test.py::test_key_error zookeeper/hparam_test.py::test_repr zookeeper/hparam_test.py::test_str
zookeeper/hparam.py
zookeeper/hparam.py