tl;dr: Yes, you want unification. The question I think you should consider is "How are you going to use it?"
The fun part about unification is that it's very applicable.
Some variant of unification is going to ultimately be the thing you end up implementing, because it's the best tool for the task you're trying to solve - whether you need fully fledged Hindley-Milner/HM inference or not. (which is what I believe is what you're referring to with "because it was originally used for languages where you didn't have to specify the types anywhere" - unification is an algorithm, HM is another algorithm that utilizes unification for full-program type synthesis)
While HM may not be applicable to your ultimate solution in it's entirety, it does seem like a variant of it would be able to do what you want, for example, within the scope of a single function. This is what Rust does, if I recall correctly - You must provide types in various locations, but within the body of a function, it can deduce types through something very similar to HM.
Overall, I would recommend reading up on HM and an implementation of it, like algorithm J, and then consider how that might be applicable within the body of a function. (A hint: it'll be considerably simpler, because the complex part is function type inference, and you don't need that at all if I understand correctly)
Depending on your language, it might be as simple as assigning each unknown type a unifiable reference (commonly referred to as a metavariable in the literature, I believe) as a placeholder for it's type, and then unifying that reference with the real type when something like print_string(s) is found. Then, at the end of a scope of a function, you make sure no variable have an unknown type.
This means you might end up with something like:
let a = []; // type of a is `List $metavariable_0`
a.push(1); // unify `$metavariable_0` with `int`
// now a's type is `List int`
You'll note how that would work across the whole function with minimal issues due to the use of mutable references - when you change it once inside the unification function, it'll be changed everywhere, so you'll catch any sort of nasty type errors like
let a = []; // type of a is `List $meta0`
a.push(1) // unify `$meta0` with `int`
// type of a is now `List int`
a.push("hi") // type error
I hope this can provide you some assistance.
ps. HM isn't that complex of an algorithm, most of the time! It's technically not great on worst case, but that worst case simply doesn't exist in good code, so it's really fine most of the time. My typechecker does unification similar to what I think you want to do, and it's ~200 LOC of OCaml.