Three Dots in Python, What is the Ellipsis Object?

Janne Kemppainen |

You might have stumbled on the Ellipsis object (…) in Python and wondered what it is used for. It was originally introduced to be used in the Numeric Python package for matrix slicing but nothing stops you from using it for other purposes too.

The original use case for the Ellipsis object was to make it simpler to handle multidimensional arrays of data. There is also a relatively new web framework called FastAPI which has a use case for it.

Numpy indexing

Let's start with a simple case. You might already know that the colon (:) character is used for slicing lists with the syntax start:step:end. Using a single : basically returns the original list as is:

>>> a = [1, 2, 3, 4, 5, 6]
>>> a[:]
[1, 2, 3, 4, 5, 6]

The same thing can be applied for multidimensional Python arrays:

>>> b = [[1, 2, 3], [4, 5, 6]]
>>> b[:][:]
[[1, 2, 3], [4, 5, 6]]
>>> b[1][:]
[4, 5, 6]

Here I am using normal python lists so each dimension needs to be indexed separately. Let's create a numpy array c.

>>> import numpy as np
>>> c = np.array(b)
>>> c
array([[1, 2, 3],
       [4, 5, 6]])

With numpy arrays we don't have to put each dimension in their own brackets but can separate them with a comma instead. So to select both dimensions would look like this:

>>> c[:,:]
array([[1, 2, 3],
       [4, 5, 6]])

The result is the same as the original array. Now we can use the Ellipsis object:

>>> c[...]
array([[1, 2, 3],
       [4, 5, 6]])

The result is still the same. Here the Ellipsis object selected all dimensions.

Let's complicate things a bit by creating a three dimensional array:

>>> d = np.array([[[i + 2*j + 8*k for i in range(3)] for j in range(3)] for k in range(3)])
>>> d
array([[[ 0,  1,  2],
        [ 2,  3,  4],
        [ 4,  5,  6]],

       [[ 8,  9, 10],
        [10, 11, 12],
        [12, 13, 14]],
    
       [[16, 17, 18],
        [18, 19, 20],
        [20, 21, 22]]])

I used a multidimensional list comprehension to create some test data. This selects the data of the from the second index of the first dimension:

>>> d[1,...]
array([[ 8,  9, 10],
       [10, 11, 12],
       [12, 13, 14]])

As you can see the result is a matrix that is equal to the second element in the original array.

You can also select the last dimension and autofill the rest:

>>> d[...,1]
array([[ 1,  3,  5],
       [ 9, 11, 13],
       [17, 19, 21]])

The ellipsis can also be located between defined dimensions. In this case it would be equal to use :.

>>> d[1,...,1]
array([ 9, 11, 13])

However, you cannot use multiple ellipses:

>>> d[...,1,...]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: an index can only have a single ellipsis ('...')

In this case you would have to to use : for the other dimension.

Other uses

You can also use the Ellipsis object as a supplement for “no operation” as it doesn't really do anything on its own. Therefore the following stub function would be valid:

def do_something():
    ...

This is similar to using pass when you intend to implement something later but don't want the code to throw any errors.

def do_something():
    pass

And if you do want to throw an error you can use NotImplementedError:

def do_something():
    raise NotImplementedError()

I stumbled upon a totally different way to use the Ellipsis object when I was developing with the FastAPI framework (which I really like by the way).

With FastAPI you can define query and path arguments with the new type annotations that were introduced in Python 3.6. For example the following snippet would define an endpoint with a query parameter q which should be a string.

@app.get("/something/)
async def something(q: str):
    return f"Your query was {q}"

This query argument is required and an error message would be returned if the endpoint was called without one. Assigning a default value such as None to the type annotation would make the argument optional.

async def something(q: str = None):

FastAPI automatically validates the type of your argument so if you're using int instead of str the value will be converted to an integer. However, you might want to add additional validations in which case you can define the query argument explicitly with the Query object, for example to set a minimum length:

async def something(q: str = Query(None, min_length=10)):

The first parameter for the Query object is the default value which is None in this case. When called it can be omitted but if the parameter is present it should be at least 10 characters long. But what if we want to set a minimum length for the parameter while still making it required?

This is where we should use the Ellipsis object. Setting the default value as Ellipsis makes the parameter required in FastAPI. So the method definition would now look like this:

async def something(q: str = Query(..., min_length=10)):

While not immediately obvious for someone new to the framework it is quite a clever solution. The None value could not be used for this purpose because then it couldn't be used as the default value when the parameter is not present. Of course a more traditional way would have been to add a flag such as required=True as an argument.

Conclusion

So now you should have an idea what the Ellipsis object is and where it can be used. Do you know some other use cases?

Subscribe to my newsletter

What's new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy