I think the simplest is to iterate the Iterators.product, which gives Tuples (that is, it loses name information). Then you can convert on the fly to named tuples, and pass the resulting iterator to DataFrame. For example
function expand_grid(; kws...)
names, vals = keys(kws), values(kws)
return DataFrame(NamedTuple{names}(t) for t in Iterators.product(vals...))
end
Another method is using crossjoin from DataFrames:
julia> (Dict(x) for x in pairs((;a = 1:2, b = 'a':'c', c=[1,2,3,4]))) .|>
splat(DataFrame) |> splat(crossjoin)
24×3 DataFrame
Row │ a b c
│ Int64 Char Int64
─────┼────────────────────
1 │ 1 a 1
2 │ 1 a 2
3 │ 1 a 3
4 │ 1 a 4
⋮ │ ⋮ ⋮ ⋮
Note the easy no-nonsense syntax specifying a,b,c.