57

I was just looking up the way to go for defining an abstract base class in Python, when I found the abc module (https://docs.python.org/3/library/abc.html).

After a bit of reading I saw the following class:

class C(ABC):
    @abstractmethod
    def my_abstract_method(self, ...):
        ...
    @classmethod
    @abstractmethod
    def my_abstract_classmethod(cls, ...):
        ...

Wondering about the triple dots, which I found it is called the Ellipsis (https://docs.python.org/3/library/constants.html#Ellipsis). I've seen and used it so far only in combination with type hints, where it maked perfectly sense.

But why would one use the Ellipsis in the definition of an abstract method? Personally, I would either do

def my_abstract_method(self):
    raise RuntimeError("NotImplemented")

or

def my_abstract_method(self):
    pass

So why is the Ellipsis preferred over pass in the official documentation? Is it just opiniated?

samwise
  • 1,907
  • 1
  • 21
  • 24
  • Possible duplicate of [What does the Python Ellipsis object do?](https://stackoverflow.com/questions/772124/what-does-the-python-ellipsis-object-do) – Alec Mar 21 '19 at 06:47
  • 24
    My question is not 'what does it do?' but 'why is the Ellipsis preferred over pass in the official documentation?' which goes more in the direction of best practices / understanding / howto. – samwise Mar 21 '19 at 06:57
  • 1
    The docs are not using a literal `...`. It's a placeholder where you're supposed to put real code. – jpmc26 Mar 21 '19 at 07:01
  • Then the docs are a bit confusing, because when would an abstract method contain actual code? – samwise Mar 21 '19 at 07:02
  • Your own question contains an example of it. – jpmc26 Mar 21 '19 at 10:36
  • True. I meant except from `pass` or throwing an exception. – samwise Mar 21 '19 at 11:23
  • 3
    The documentation mentions that you can have an implementation, that the overriding classes can call via `super`. It can make sense to do so in a class that expects its children to sometimes do multiple inheritance (e.g. there's one grandparent class, many children classes that all call `super()` so that they can collaborate, and a few grandchildren classes that inherit from multiple parents. – Blckknght Mar 21 '19 at 19:14
  • 1
    Ewww.. feels quite embarassing now, to have forgotten such a basic OOP conecept... – samwise Mar 22 '19 at 08:05

5 Answers5

45

Using the Ellipsis literal as the body of a function does nothing. It's purely a matter of style if you use it instead of pass or some other statement. If you give your function a docstring, you don't even need to put any statement after the line with the docstring.

If the official Python documentation is inconsistent, it's probably because the Python core developers who write the docs don't themselves have a consistent opinion on which looks best. PEP 8 does not give any recommendation on what to use for the body of dummy functions (though it does use lots of ...s to stand in for things in its examples, sometimes in places where you can't actually use an Ellipsis in real code).

So for your own code, use whatever you like best. If you want to be formal about it, write down your preferences in your style guide and then always do whatever the guide says. You don't need to follow the same style as anyone else (unless that person is your boss).

One final note: You can't use ... as a parameter name in a function definition (def my_abstract_method(self, ...): raises a SyntaxError, regardless of what's on the next line).

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • 9
    `...` is not idiomatic. – jpmc26 Mar 21 '19 at 14:38
  • 1
    Do you know if core developers made statements why they use one or the other? So besides "I'm used to it" are there any arguments for one or the other at all? – Martin Thoma Jun 10 '20 at 11:49
  • 4
    @MartinThoma I'm not aware of any developer comments on this, but I haven't gone looking either. I suspect the `...` notation in documentation started as a textual thing, marking where extra stuff could go, but omitting it for brevity. But since the literal *is* valid code in some places, it sometimes gets used for real. – Blckknght Jun 10 '20 at 19:04
  • 3
    There is some agreement to recommend usage of `...` in stubs: https://github.com/python/typing/issues/109 Not sure how widely it is adopted though. – greatvovan Apr 01 '21 at 14:12
23

I almost only ever use ellipses in abstract classes, because of how abstract classes work.

To me, pass means "do nothing" or "NOOP", but ... means "details to be defined elsewhere".

An abstract method literally cannot not be overridden. Now, you can technically call super().abstractmeth() and run code that's defined in the abstractmethod definition, but the usual pattern I use is to use ellipses here to indicate when reading that the content of the method should be completely redefined somewhere else.

If you just put a pass someone might think that this is a stub method to be filled out later, but the ellipses usually makes people realize that something a little different is going on.

noharmpun
  • 231
  • 2
  • 3
19

I looked into python (3.7) docs and

pydoc ...
The Ellipsis Object
*******************

This object is commonly used by slicing (see Slicings).  It supports
no special operations.  There is exactly one ellipsis object, named
"Ellipsis" (a built-in name).  "type(Ellipsis)()" produces the
"Ellipsis" singleton.

It is written as "Ellipsis" or "...".

Related help topics: SLICINGS

and

pydoc pass
The "pass" statement
********************

   pass_stmt ::= "pass"

"pass" is a null operation Ś when it is executed, nothing happens. It
is useful as a placeholder when a statement is required syntactically,
but no code needs to be executed, for example:

   def f(arg): pass    # a function that does nothing (yet)

   class C: pass       # a class with no methods (yet)

So it looks like that at least from pydoc point of view pass is way to define function which does nothing.

Daweo
  • 31,313
  • 3
  • 12
  • 25
3

I agree with @Blckknght and @noharmpun. The resulting bytecode is pretty much the same.

In [13]: dis.dis("def B(): ...; B()")


  1           0 LOAD_CONST               0 (<code object B at 0x111bf6130, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('B')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (B)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Disassembly of <code object B at 0x111bf6130, file "<dis>", line 1>:
  1           0 LOAD_GLOBAL              0 (B)
              2 CALL_FUNCTION            0
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE
In [14]: dis.dis("def A(): pass; A()")


  1           0 LOAD_CONST               0 (<code object A at 0x111bf59a0, file "<dis>", line 1>)
              2 LOAD_CONST               1 ('A')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (A)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE

Disassembly of <code object A at 0x111bf59a0, file "<dis>", line 1>:
  1           0 LOAD_GLOBAL              0 (A)
              2 CALL_FUNCTION            0
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Jonathan Simon Prates
  • 1,122
  • 2
  • 12
  • 28
  • 1
    Define the function outside the `dis` call and just call `dis.dis(A)` or `dis.dis(B)` and you'll simplify the output dramatically (right now, you're showing the work of constructing the function itself, which is not really important). Makes it more clear that they compile to exactly the same thing, with the `...` using function optimizing out the unused load (if the optimizer didn't handle this, the code would include a single `LOAD_CONST`/`POP_TOP` instruction pair to load and then discard the ellipsis object). – ShadowRanger May 26 '22 at 18:31
  • Simpler example: [Try it online!](https://tio.run/##K6gsycjPM/7/PzO3IL@oRCEls5iLKyU1TSEgsbhYQ9OKSwEICoBsiKhrTk5mAUxYT08PKJpZrAfEGiD1mgguWKHm//8A "Python 3 – Try It Online") – ShadowRanger May 26 '22 at 18:32
0

One more little note:
I'm using pass over ... because the last one is not so easy to exclude in coverage reports.
Both pass and ... will be reported as uncovered lines inside @abstractmethod.
coverage has special syntax to exclude lines, but ... considerated as regex "skip all text".
Probably it can be solved somehow by escape symbols or special syntax...
P.S. Another workaround is to exclude @abstractmehod lines in coverage.

Давид Шико
  • 362
  • 1
  • 4
  • 13