Yup. My main gripe with Ansible is exactly this. Too much mutable state, too imperative, too much reliance on "gathering facts". There's no singular source of truth. It tries to be declarative, but every Ansible deployment I've maintained (granted, not many) always accretes corner cases and config drift. Maybe that's just the nature of the domain - mutating live systems will always be more imperative than (re)deploying the whole thing from scratch.
I'd much rather use python/jinja/gomplate/what-have-you, do all my fancy expressive stuff with code I can check in, and get a single snapshot of plain, structured text (which I can also check in). Not to mention locking down your invariants (I can't recall how many times I've gotten bit by interpolating docker image uris with the wrong TAG). It's actually pushed me towards first Make and then later python cli tools (cause Makefile syntax is...ugh) to generate configs, rather than relying on any runtime business logic, cause it's just so much saner, traceable, auditable, etc.
... again, exactly. I just use Bash, sacrifice the usual few virgins at certain midnights per year and get on with my life. It's much faster to just type it, easier to debug, no problems with python modules, no YAML.
Sure, it is still Bash, it still requires selling your soul, but it's not like I was going to Haskell heaven anyway.
The important part is that the interface has to have the same semantics as a config file: atomic/transactional. For example the builder pattern does this. You can enjoy all the expressiveness and power of the high-level language, but at some point you create the final config/pipeline/job-graph, call whatever needs to be called, and the provider of the interface needs to validate that.
That's why the semantics of Terraform is better than Ansible's.