Skip to content

Instantly share code, notes, and snippets.

@kenballus
Last active September 11, 2022 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kenballus/1b9416311fd177aa2f32f8d4ec3b6eac to your computer and use it in GitHub Desktop.
Save kenballus/1b9416311fd177aa2f32f8d4ec3b6eac to your computer and use it in GitHub Desktop.

I recently challenged myself to write a function in Python with the type signature make_tuple_type(T: type, n: int) -> type that returns the type of tuples consisting of n elements of type T. For example, make_tuple_type(str, 4) should return typing.Tuple[str, str, str, str]. My first thought was to use a starred expression:

>>> from typing import Tuple
>>> def make_tuple_type(T: type, n: int) -> type:
...     return Tuple[*[int] * n]
  File "<stdin>", line 2
    return Tuple[*[int] * n]
                 ^
SyntaxError: invalid syntax
>>> # Note that that wasn't a TypeError saying I can't multiply a type by an int,
>>> # because unary * has lower precedence than binary *.

Crumbs. I don't really understand where I can and can't use starred expressions. What about something with eval?

from typing import Tuple
def make_tuple_type(T: type, n: int) -> type:
    return eval(f"Tuple[{(T.__name__ + ', ') * n}]", globals(), locals())

This seems to work, but it's gross, and a clever person might do something like this:

>>> class T:
...     pass
>>> T.__name__ = "int"
>>> make_tuple_type(T, 1)
typing.Tuple[int]

Of course, __name__ is just a class attribute, and can therefore be messed with. I sent this problem over to my friend Blake and he suggested the following (more or less):

def make_tuple_type(T: type, n: int) -> type:
    return Tuple[tuple([T] * n)]

This is starting to look a lot better, but why is there a call to the tuple constructor? Why does Tuple's __getitem__ automatically unpack tuples? This really seems like a natural place for a starred expression, instead of whatever that tuple junk is. Let's play around with this.

>>> Tuple[int, int, int]
typing.Tuple[int, int, int]
>>> # Phew
>>> Tuple[*[int] * 3]
  File "<stdin>", line 1
    Tuple[*[int] * 3]
          ^
SyntaxError: invalid syntax
>>> # Same as above.
>>> Tuple[(*[int] * 3)]
  File "<stdin>", line 1
    Tuple[(*[int] * 3)]
           ^^^^^^^^^^
SyntaxError: cannot use starred expression here
>>> # Okay, but I can definitely use a starred expression in a function call,
>>> # so I'll just call __getitem__ directly.
>>> Tuple.__getitem__(*[int] * 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.10/typing.py", line 312, in inner
    return func(*args, **kwds)
TypeError: _TupleType.__getitem__() takes 2 positional arguments but 4 were given
>>> # Why didn't that work? Shouldn't the function's arguments all end up in args?
>>> # This isn't a problem with other variadic functions, like print:
>>> print(*[int] * 3)
<class 'int'> <class 'int'> <class 'int'>
>>> # I am now confused.
>>> Tuple.__getitem__((*[int] * 3,))
typing.Tuple[int, int, int]
>>> # It worked! But why?
>>> # I think this exaplins Blake's call to the `tuple` constructor

At this point, I think I have a mental model.

Places I can use starred expressions:

  • inside of list brackets,
  • inside of function call parentheses,
  • inside of tuple parentheses if the starred expression is followed by a comma.

Places I can't use starred expressions:

  • everywhere else

For some reason, print and Tuple.__getitem__ don't treat starred expressions the same way, even though they both take a variable number of arguments. I don't know why. If you do, please let me know.

I now present what I imagine is thought was the smallest and least readable answer to my original challenge:

def make_tuple_type(T, n):
    return Tuple[(*[T]*n,)]

Blake later explained that the tuple in his implementation gets automatically unpacked because there is no distinction between x[a, b] and x[(a, b)]. Try it. When you pass multiple things to a __getitem__, they get packed up into a tuple. When you pass one thing to a __getitem__, it's left alone. This explains why there is a distinction between x[a] and x[(a,)].

🤮

EDIT: I sent this to my friend Nate, and he pointed out that you can just return Tuple[(T,) * n]. This is probably the best way to do it, and makes the problem a whole lot less interesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment