Python is in no way "type free" (see "Is Python strongly typed?"). All Python objects have a specific type that determines many of their properties and behaviors. For those that are represented as literals, that type is determined by their lexical form. For other objects, it is determined by how they were computed.
What Python does not have is type declarations or any other built-in form of limiting or predetermining the type of objects that a variable or aggregate may contain. Thus it does not make sense to talk about the type of a variable, such as your x, but only about the type of the value that it contains. And that is what the type() function (for example) computes. So when you ask
how does python work under the hood(in terms of its implementation in C) when it comes to determining what type of variable it is
the answer is in one sense simple: Python doesn't do that at all. Variables don't have types -- only their values do. When you call, say, type(x), you are not determining the type of the variable, but rather the type of its current value.
And Python supports that by ensuring that every Python object, such as one represented by the literal 5, contains rather a lot of data. Significantly, it contains information about the object's value and type, but it also contains various other details as well, such as a reference count and much more. The details are much too broad for an SO answer, but you can find a lot of them in the Python / C API Reference Manual. For the deepest and most intimate secrets, you would want to study the cpython headers and source.