numba: The tuple built-in is not supported in nopython mode

The tuple([iterable]) python built-in function doesn’t work in nopython mode. Is there any plan to support this in nopython mode?

This would be very helpful for some functions like numpy.ndindex(*shape) which in numba must be called with a tuple. In addition in nopython mode generating a tuple by tuple addition (concatenation) in a for loop does not work.

Minimal function for use of tuple:

@jit(nopython=True)
def g():
    tuple()

Resulting error:

Untyped global name 'tuple': cannot determine Numba type of <class 'type'>

Trying to generate a tuple using a for loop:

@jit(nopython=True)
def foo():
    t = ()
    for i in range(-10, 11):
        t += (i,)
    return t

Resulting error:

cannot unify () and (int64 x 1) for 't'

Also after reading through the documentation (section 2.6) I had expected that this would work. Here are the relevant parts of that documentation:

2.6. Supported Python features¶

Apart from the Language part below, which applies to both object mode and nopython mode, this page only lists the features supported in nopython mode.

2.6.2.3. tuple

The following operations are supported:

tuple construction
tuple unpacking
comparison between tuples
iteration and indexing over homogenous tuples
addition (concatenation) between tuples
slicing tuples with a constant slice

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 9
  • Comments: 29 (7 by maintainers)

Most upvoted comments

This strikes me as a rather odd comment. List() is supported fine, so why isn’t tuple()?

Your statement surprised me because I had tested generating an empty list by replacing tuple with list in my first example which fails with a TypingError. It seems numba doesn’t know what type list to generate and in reality there is no sane default option. If you do pass a list or tuple (with all elements of compatible types? It seems int and float work together, but not string and int or string and float.) to the list built-in then yes it does work; however, this doesn’t work for tuple.

but does it have to be a tuple? List unpacking is a thing.

For nopython mode the current answer is yes. A lot of the numpy functions require a shape parameter (e.g. numpy.zeros, numpy.ones, numpy.reshape) for which currently a tuple must be passed. Since there is no way to generate the necessary shape tuple in nopython mode curretly it has to be passed into the jitted function as a parameter. Here is an example:

@jit(nopython=True)
def g():
    np.zeros([1,2,3])

give the following error

numba.errors.TypingError: Failed at nopython (nopython frontend)
Invalid usage of Function(<built-in function zeros>) with parameters (list(int64))

This is perhaps not a big deal, but as a new user of numba and a fundamentally python user (I’m not used to using other languages) it is quite jarring to run into many situations where the pythonic way of programming has failed when attempting to use numba in nopython mode to gain the best speedup.

Your second example runs into a typing error because the numba type for a zero-length tuple is different than the numba type for a tuple of length one. So if you use a new variable, the tuple addition will work fine. Honestly, I’m not sure why the tuple type should have fixed lengths, as long as it’s immutable (for which there is a flag set for lists…).

My second example was an attempt to get around the lack of the tuple built-in. I don’t see a way of generating an arbitrary length tuple by using another variable because it seems that numba tries to infer the type and length of the declared tuple and cannot dynamically change it. So for example if I use another variable:

@jit(nopython=True)
def foo():
    t = ()
    for i in range(10):
        s = t + (i,)
        t = s
    return t

it’ll still fail with a TypingError.

I know this is an old issue, but I simply wanted to point out that since dynamic tuple creation is not supported in nopython mode, it limits what functions we can use. For instance, in the following example I’m trying to implement a numba compatible np.squeeze method:

@nb.jit
def squeeze(arr, axis=None):
    dims = arr.shape
    axis = axis if axis is not None else range(arr.ndim)
    dims = [d for i, d in enumerate(dims) if d != 1 and i in axis]
    return np.reshape(arr, dims)

This fails because the only signature available for reshape is one where the shape argument is an int or tuple of ints. Is there a workaround I’m not thinking of? Here clearly the size of the shape parameter dims is not known, but its size is upper-bounded easily as it will never be longer than arr.shape. Can this not be done statically at compile time?

Is there a workaround if the length of a tuple is known and fixed? Does PR #5169 help?

Since we can already concatenate tuples and create them from literals, I wrote a function that creates a tuple creator dispatcher, given you know the length of the tuple at compile time:

def create_tuple_creator(f, n):
    assert n > 0
    f = njit(f)
    @njit
    def creator(args):
        return (f(0, *args),)
    for i in range(1, n):
        # need to pass in creator and i to lambda to capture in scope
        @njit
        def creator(args, creator=creator, i=i):
            return creator(args) + (f(i, *args),)
    return njit(lambda *args: creator(args))

Here are some examples of it solving some of the use cases in @krrk’s post above:

range_10 = create_tuple_creator(lambda i: i, 10)
range_10_x = create_tuple_creator(lambda i, x: i*x, 10)
true_10 = create_tuple_creator(lambda _: True, 10)
false_10 = create_tuple_creator(lambda _: False, 10)
zip_10 = create_tuple_creator(lambda i, l, r: (l[i], r[i]), 10)

@njit
def foo(x):
    print(range_10()) # tuple([i for i in range(10)])
    print(range_10_x(x)) # tuple([i*x for i in range(10)])
    print(zip_10(true_10(), false_10())),  # tuple(zip((True,)*10, (False,)*10))

    
foo(2)

@ziofil I might have found an “unsafe” solution in the case in which the iterable to convert is an ndarray and with numba at version 0.52.0. The idea being to use to_fixed_tuple which can be found in numba.np.unsafe.ndarray. Although it is unsafe, it seems to work. Note that in to_fixed_tuple(arr, length), length must be a constant. If it is not, but there is only a small set of possibilities for what this length might be, I found a solution which seems to work for me. It uses functools.lru_cache to cache one (compiled) version of your njitted function making use of to_fixed_tuple, per possible tuple length. As an example, here is a way to convert a 2D ndarray X to a dictionary where each key is the tuple version of a row of X (and values are all NaN):

from functools import lru_cache

import numpy as np
from numba import njit
from numba.np.unsafe.ndarray import to_fixed_tuple

@lru_cache
def make_to_dict(length):

    @njit
    def to_dict(X):
        d = {}
        for row in X:
            d[to_fixed_tuple(row, length)] = np.nan

        return d

    return to_dict

You can try it with e.g.

to_dict = make_to_dict(2)
X = np.arange(10).reshape((5, 2))

d = to_dict(X)

On my machine at least, to_dict will only njit the first time around and a compiled cached version will be used from then on. So if I do

x = np.arange(12)

# First time with 2
to_dict = make_to_dict(2)
d = to_dict(x.reshape((6, 2)))

# First time with 3
to_dict = make_to_dict(3)
d = to_dict(x.reshape((4, 3)))

# Second time with 2
to_dict = make_to_dict(2)
d = to_dict(x.reshape((6, 2)))

# Second time with 3
to_dict = make_to_dict(3)
d = to_dict(x.reshape((4, 3)))

I get compilation overhead in the first two blocks but not the third or fourth.

Wondering what the experts think!

@ehsantn I agree with @Rik-de-Kort about the type inferencing. It seems that numba can determine the type and length of lists in nopython mode and if that is known then for at least those cases it should be easy to create a tuple from a list.

For example in the function below numba can create the list and infers that it is of type float64. It also knows its length.

@jit(nopython=True)
def foo(n):
    l = [i/2 for i in range(n)]
    return len(l)

Perhaps I am missing something, but it seems that numba could use the type and length of the list to create a tuple say if I called tuple(l) in the above function?