Let's break down what's happening here. You're not wrong that the tuple constructor seems to want a list, but it would be more precise to say that the tuple constructor seems to want a list-like object. (to be specific, any iterable works)
This is a philosophy called as Duck Typing.
The saying goes as follows:
If it walks like a duck, swims like a duck, and quacks like a duck,
then it probably is a duck.
So then, a list works
a = [1, 2, 3, 2]
tuple(a) #Output: (1, 2, 3, 2)
but so does a different iterable, such as a set
tuple(set(a)) #Output: (1, 2, 3)
So, tuple does not care whether the object it receives is a list, just that it should be able to iterate and get values from the object.
Now, The second part of the magic comes from something called list comprehension/generator expressions. They are iterables you can create which make it easy to write 1 liners that create a list or a generator expression respectively. More on the generator later, for now, it is sufficient to see how a list comprehension works.
A simple example of list comprehension
[a for a in range(4)] #Output: [0, 1, 2, 3]
[a*a for a in range(4)] #output: [0, 1, 4, 9]
We see that they produce lists. So, can we feed them to a tuple constructor? Why not!
tuple([a for a in range(4)]) #Output: (0, 1, 2, 3)
tuple([a*a for a in range(4)]) #output: (0, 1, 4, 9)
Now, what about using the same expression but wrapping it in curved brackets instead?
(a for a in range(4)) #Output: <generator object <genexpr> at 0x000000FA4FDBE728>
You just created a generator expression
They are essentially memory efficient on-demand iterables. (to be jargon specific, they have a yield and next , and only yield values as needed). Let's see it in action.
my_generator = (a for a in range(4)) #generator created.
next(my_generator) #Outputs 0
next(my_generator) #Outputs 1
next(my_generator) #outputs 2
next(my_generator) #outputs 3
next(my_generator) #Raises StopIteration Error. The generator is exhausted.
We can see that we receive the same values as with our list comprehension.
So, does a tuple accept something like a generator? Well, duck typing to the rescue! Absolutely!
tuple((a for a in range(4))) #Output: (0, 1, 2, 3)
Do i need the redundant parenthesis? Nope!
tuple(a for a in range(4)) #Output: (0, 1, 2, 3)
tuple(a*a for a in range(4)) #Output: (0, 1, 4, 9)
Now, what does this produce? (x, x*x) for x in [1, 2, 3]
Well, it is an expression, but let's get an idea of how it would look in a list comprehension instead
[(x, x*x) for x in [1, 2, 3]] #Output: [(1, 1), (2, 4), (3, 9)]
Ah, its a list of tuples? Can a generator do the same?
my_generator = ((x, x*x) for x in [1, 2, 3]) #<generator object <genexpr> at 0x000000FA4FD2DCA8>
next(my_generator) #Output: (1, 1)
next(my_generator) #Output: (2, 4)
next(my_generator) #Output: (3, 9)
next(my_generator) #Raises StopIteration
Yep, looks good. So its a generator, but its an iterable. Behaves like a duck anyways, doesn't it? So, the tuple constructor should work just fine!
tuple((x, x*x) for x in [1, 2, 3]) #Output: ((1, 1), (2, 4), (3, 9))
So, that wraps up everything. The parenthesis do not imply a tuple all the time, () are not reserved for tuples. We see here that they can be used for generator expressions as well! Similarly, the {} do not have to be tied to dictionaries always, something similar to list comprehension actually exists for dictionaries too! (Known as dict comprehension)
I highly recommend going through the links for a more thorough explanation of the individual pieces that are working together here. Hope this helps!