• Home
  • Publications
  • Subscribe
  • ...

    Ellipsis is probably the least know literal in whole Python. I would guess most people don't even know it exists, let alone what it's good for. Let's learn everything we can about it together.

    Python documentation is very good and should be the first place to look for explanation of concepts you don't understand. Unfortunately, in case of Ellipsis, it doesn't tell us much.

    The same as the ellipsis literal "...". Special value used mostly in conjunction with extended slicing syntax for user-defined container data types. https://docs.python.org/library/constants.html#Ellipsis

    When documentation is lacking, we have to do some explorations ourselves.


    Documentation says Ellipsis is equivalent to ... (three dots). Interactive python shows us, it's true.

    >>> ...
    >>> Ellipsis is ...

    We can do some other stuff with it. As I mentioned in my article about metaclasses - everything is an object. Therefore, even Ellipsis should have some methods and attributes we can inspect.

    >>> dir(...)

    This can lead to some interesting syntax weirdness.

    >>> ....__eq__(...)

    Class of Ellipsis

    Another interesting thing about Ellipsis is its class. Usually all classes of standard objects are directly callable.

    >>> type(False)
    <class 'bool'>

    This isn't true for Ellipsis. The class seems to be inaccessible by name. But we can use a little trick.

    >>> type(...)
    <class 'ellipsis'>
    >>> ellipsis()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'ellipsis' is not defined
    >>> type(...)()

    I was wondering, where does the class comes from. I was expecting builtins and inspection of the class would suggest so.

    >>> type(...).__module__

    But it cannot be imported from there.

    >>> from builtins import ellipsis
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ImportError: cannot import name 'ellipsis'

    And it's not even listed in the module. It only contains its instance.

    >>> import builtins
    >>> 'Ellipsis' in dir(builtins)
    >>> 'ellipsis' in dir(builtins)

    Definition of Ellipsis is not very consistent with other constants. I believe it's because it's special status of being both a literal and an object at the same time.

    Not intended usage

    Documentation of Ellipsis says its intended purpose is in slicing of array and matrices. We will go through that usage in detail in next article, before we do so, let's look at some example of bad usage I found on the internet.

    ... instead of pass

    Everyone knows they can use pass to denotate an empty code block.

    def empty_function():

    Some people suggest we could use ... instead. I have to say that purely based on esthetics, I like it more.

    def empty_function():

    This is possible not because of some magical properties of ... but because Python will accept almost anything, as long as the block isn't empty.

    def empty_function():

    Even just docstring is good enough.

    def empty_function():
    	""" Does nothing """

    ... used in imports

    This is not exactly bad usage, just a technical detail that might be a bit confusing. Imports in Python are parsed a bit differently from other parts of the code. We already established that ... is Ellipsis. Therefore, import Ellipsis should be exactly the same as import .... But it isn't.

    >>> import Ellipsis
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ModuleNotFoundError: No module named 'Ellipsis'
    >>> import ...
      File "<stdin>", line 1
    	import ...
    SyntaxError: invalid syntax

    The reason is simple. When importing, token ... is translated to "two steps up in the package tree". We can use it to import modules from package by their relative path.

    Imagine a directory structure like this:

    $ tree .
    └── a
    	├── a_mod.py
    	└── b
    		└── c
    			└── c_mod.py

    c_mod.py needs to import a_mod.py from package a. One way to do it is like this:

    # module a/b/c/c_mod.py
    from ... import a_mod
    # module a/a_mod.py
    print('module "a" was imported")

    ... in this case doesn't mean Ellipses but a relative path from current module two steps up.

    >>> from a.b.c import c_mod
    module "a" was imported

    ... used as Undefined

    Even authors of Python got trapped by ... before, namely when implementing Exception Chaining. It's a mechanism making it possible to mark one exception as a direct cause of another.

    >>> try:
    ...	 1/0
    ... except ZeroDivisionError as e:
    ...	 raise Exception() from e
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
    ZeroDivisionError: division by zero
    The above exception was the direct cause of the following exception:
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>

    Python deals with this by setting a __cause__ attribute on the second exception witch contains the exception that caused it. This all works fine but there is a dilemma; you can raise Exception() from None or you can just raise Exception. How do you differentiate between exceptions that don't have cause at all and exception caused by None?

    PEP-409 tried to solve this by setting the __cause__ to Ellipsis if it wasn't set. PEP-415 removed this behavior. You can read the reasoning behind these decisions in the PEPs.

    In next part we'll look at some practical examples and usage.