Gavin Kistner
09/15/2025, 7:21 PMdef foo(some_node: SomeNode, foo_cfg: FooConfig) -> int:
…
# This is procedurally created with dynamic names at runtime
foo_cfgs = {"a": FooConfig(…), "b": FooConfig(…)}
dr = driver.Builder().with_config({"foo_cfgs": foo_cfgs}).build()
print(dr.execute(final_vars=[*foo_cfgs.keys()]))
#=> {"a":42, "b":17}
I have a working approach (details in thread) using @resolve and @parameterize, but I'm wondering if there's a better way to do this using pipelines or subdags or something. Basically:
What's the Hamilton way (or ways) to accomplish the equivalent of this procedural code:
for name, setup in some_input.items():
final_result[name] = some_node(…other_attrs_from_dag…, some_attr=setup)Gavin Kistner
09/15/2025, 7:24 PM@resolve(
when=ResolveAt.CONFIG_AVAILABLE,
decorate_with=lambda foo_cfgs={}: parameterize(
**{
name: {"foo_cfg": value(c)}
for name, c in foo_cfgs.items()
}
),
)
def foo(some_node: SomeNode, foo_cfg: FooConfig) -> int:
…
.with_config({"foo_cfgs": foo_cfgs, "hamilton.enable_power_user_mode": True})
• If foo_cfgs is not specified, the ={} default value will set it to an empty dict, and no parameterization will occur.
• Otherwise, it will use parameterize to create new node names.Gavin Kistner
09/15/2025, 7:29 PMdef foos(some_node: SomeNode, foo_cfgs: dict[str, FooConfig]) -> dict[str, int]:
return {name:foo(some_node, c) for name, c in foo_cfgs.items()}
result = driver.execute(["foos"], inputs={"foo_cfgs": foo_cfgs})
print(result)
#=> {"foos": {"a":42, "b":17}}
# Manually loft all the values to the top
if "foos" in result:
for k,v in result["foos"].items():
result[k] = vThierry Jean
09/15/2025, 7:47 PMkey: value mapping outside of hamilton; let's call it it config
2. passing config to the DAG via .with_config()
3. this config determines what foo nodes are created (foo__bar, foo__baz)
4. you want to execute the DAG to get foo__bar , foo__baz, and all other foo__ derivatives configured
## What you're currently doing
• using @resolve to parameterization of foo at config time
• using the config you pass to @resolve in your Driver.execute() statement
# My question
This seems like a working solution, albeit complex. What's the friction point you're looking to solve?
In my experience, @resolve adds a lot of complexity and I never had to reach for it. It's typically useful for platform engineers that define reusable templates for workflows. It's common in feature engineering workflows that use Hamilton dataframe result builders for instance
## "What's the Hamilton way"
> What's the Hamilton way (or ways) to accomplish the equivalent of this procedural code:
> for name, setup in some_input.items():
> final_result[name] = some_node(…other_attrs_from_dag…, some_attr=setup)
It's not obvious what "feature" is useful to you in this for loop. Regarding Hamilton's design, the idea "you know exactly what nodes exist and how they depend on each other before `.execute()`" is at the core; @resolve is the main escape hatch.
## "parameterize vs for loop"
I have code that generates burgers: regular, cheese and vege.
### config + for loop (what I would do
This has a single burger node that dispatches implementations
def burger(ingredients: list[str]) -> Burger:
"""Builds a burger based on ingredients"""
# could have if/else to have multiple implementations
# burger could have `recipe_name" as kwarg
#...
recipes = {
"regular": [...],
"cheese": [...],
"vege": [...],
}
dr = Builder().with_modules(...).build()
burgers = {}
for name, ingredients in recipes.items():
results = dr.execute(["burger"], inputs={"ingredients": ingredients})
burgers[name] = results["burger"]
You can even wrap this inside a function
def make_burger(recipe_name: str) -> Burger:
dr = Builder().with_modules(...).build()
recipes = {
"regular": [...],
"cheese": [...],
"vege": [...],
}
results = dr.execute(["burger"], inputs={"ingredients": reciptes["recipe_name"]})
return results["burger"]
### parameterize + resolve
This approach creates multiple nodes regular cheese, vege, at you can reference by name at execution. It could make things clearer or not.
@resolve_at_config(
decorate_with=...
)
def burger_base(incredients) -> Burger:
"""Builds a burger based on ingredients"""
# note, this is likely the same function as before
# ...
recipes = {
"regular": [...],
"cheese": [...],
"vege": [...],
}
dr = Builder().with_config(**recipes).with_modules(...).build()
results = dr.execute(["cheese"], inputs={"ingredients": recipe["cheese"])
burger = results["cheese]
Wrapped inside a function, it looks the same to a user, but probably more complex to maintain
def make_burger(recipe_name: str) -> Burger:
recipes = {
"regular": [...],
"cheese": [...],
"vege": [...],
}
dr = Builder().with_config(**recipes).with_modules(...).build()
results = dr.execute(["cheese"], inputs={"ingredients": recipe["cheese"])
return results["cheese"]Thierry Jean
09/15/2025, 7:52 PM.with_config() , @config.when and config inside @pipe_input steps are mostly useful to "reshape" the DAG and include/exclude node.
@parameterize helps create "new named objects, but with the same implementation". Combined with @resolve it allows to "create new named objects that you only know at runtime".
In the scenario described above, it would be useful if you wanted to request any burger recipe by name, but you don't know all the recipes when authoring the DAG (i.e., writing the .py file), or these recipe will evolves.
Though, maybe you don't need to request them by name at the Driver.execute() level and you could simply ask for Driver.execute(["burger"]) and provide the relevant context in the "wrapper" above HamiltonGavin Kistner
09/15/2025, 7:56 PM.execute() N times for the outputs will be a performance problem. The execute call actually has .execute(final_vars=[*all_the_other_outputs_requested, *foo_cfgs.keys()]) , which is to say other things are also being dynamically computed on the penultimate SomeNode for the result.
I appreciate the reinforcement that "clarity of execution path and implementation" is a non-trivial benefit that Hamilton provides. The @resolve/@parameterize approach, which creates node names dynamically, seems far less good (on that metric) than my wrapper approach.Gavin Kistner
09/15/2025, 7:57 PMfoos()? (For example, it theoretically bypasses the DAG and so would prevent a future parallelization optimization.)Thierry Jean
09/15/2025, 8:02 PM.execute() in a loop. For good performance, you can do dr.execute(["burger", "some_expensive_step"]), and pass "some_expensive_step" as dr.execute(..., overrides={"some_expensive_step": previous_results["some_expensive_step"]}) OR use caching.
I personally prefer your wrapper approach. I think Hamilton is very powerful for "platform / data teams", but you can wrap its features inside simpler functions and classes when shipping features to your colleagues. This way, don't even need to learn anything about Hamilton.
I believe that's the original pattern that Elijah & Stefan employed when creating Hamilton at Stitch FixGavin Kistner
09/15/2025, 8:08 PMThierry Jean
09/15/2025, 8:09 PMGavin Kistner
09/15/2025, 8:11 PMStefan Krawczyk
09/16/2025, 1:17 AMGavin Kistner
09/16/2025, 2:56 PM