Skip to content

Latest commit

 

History

History
381 lines (266 loc) · 10 KB

File metadata and controls

381 lines (266 loc) · 10 KB

The Python Ellipsis (...): More Than Just a Placeholder

The three dots ... (officially the Ellipsis literal) is one of Python’s most
versatile yet underappreciated features. It is a built‑in singleton constant, just
like None, True, and False. You can type it directly in source code and it
evaluates to the object Ellipsis.

print(...)            # Ellipsis
print(type(...))      # <class 'ellipsis'>
print(... is Ellipsis)  # True

This guide walks through every professional‑grade use case, with fully runnable examples.


1. Placeholder for Unwritten Code

The most common beginner use is as a TODO marker. Unlike pass, the ellipsis visually signals “intentionally empty, not an oversight.”

def future_function():
    ...   # TODO: implement later

class FutureClass:
    ...

if 2 + 2 == 4:
    ...   # handle this case later

This code runs without errors, and many linters treat ... as an acceptable placeholder.


2. Ellipsis in Type Hints

Type hints use ... to indicate arbitrary parameters or variable‑length homogeneous tuples.

Callable[..., ReturnType]

When a callback can accept any arguments but returns a known type:

from typing import Callable

def register_callback(cb: Callable[..., str]) -> None:
    # cb might be called with anything, but we expect it to return a string
    result = cb("example", key=42)
    print(result)

# Usage
def my_callback(*args, **kwargs) -> str:
    return f"Called with {len(args)} args and {len(kwargs)} kwargs"

register_callback(my_callback)

Tuple[int, ...]

A tuple of arbitrary length where every element is of the same type:

from typing import Tuple

def sum_all(numbers: Tuple[int, ...]) -> int:
    return sum(numbers)

print(sum_all((1, 2, 3)))       # OK
print(sum_all((10, 20, 30, 40))) # OK

3. Structural Interfaces with typing.Protocol

Protocols define an interface that a class satisfies simply by having the required methods – no explicit inheritance needed. The ellipsis is the canonical body for protocol methods.

from typing import Protocol

class Greeter(Protocol):
    def greet(self) -> str:
        ...

class Person:
    def __init__(self, name: str):
        self.name = name

    def greet(self) -> str:
        return f"Hello, I am {self.name}"

class Robot:
    def greet(self) -> str:
        return "Beep boop"
    
class Dog:
    def bark(self) -> str:
        return "Woof"
    def greet(self) -> str:
        return "Woof! I'm a dog, nice to meet you!"

def welcome(g: Greeter) -> None:
    print(g.greet())

# All three classes satisfy the Greeter protocol
p = Person("Alice")
r = Robot()
d = Dog()

welcome(p)   # "Hello, I am Alice"
welcome(r)   # "Beep boop"
welcome(d)   # "Woof! I'm a dog, nice to meet you!"

The ellipsis in the protocol tells static type checkers: “This method is part of the interface; the real body is supplied by the implementing class.”


4. Abstract Base Classes (ABCs)

Similar to protocols, abstract methods in ABCs often use ... to mark “I have no concrete implementation here.”

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def connect(self) -> None:
        ...

class SQLiteDB(Database):
    def connect(self) -> None:
        print("Connecting to SQLite database...")
        # real connection logic here

# db = Database()   # Error – can't instantiate abstract class
db = SQLiteDB()
db.connect()

5. NumPy and Multi‑dimensional Slicing

In NumPy, ... means “as many : as needed to select all remaining dimensions.”

import numpy as np

arr = np.zeros((2, 3, 4, 5))
# Select first element in the first dimension, keep all others
sliced = arr[0, ...]   # shape (3, 4, 5)
print(sliced.shape)

A custom class can also handle ... inside __getitem__:

class MyTensor:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, key):
        if key is ...:
            return self   # return the whole tensor
        return self.data[key]

t = MyTensor([1, 2, 3])
print(t[...])   # <__main__.MyTensor object ...>
print(t[1])     # 2

6. Pydantic and FastAPI: Required Fields

In Pydantic models, ... makes a field required (no default value).

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = ...   # required, no default

# Works only if both fields are provided
item = Item(name="Laptop", description="A powerful machine")
print(item)
# Item(name='Laptop', description='A powerful machine')

In FastAPI, ... can mark a query parameter as required:

# This is a snippet; FastAPI needs a running server
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items/")
async def read_items(q: str = Query(..., min_length=3)):
    # q is a required query parameter
    return {"query": q}

Note: ... does not work inside TypedDict to mark a key as required. Use total=False or typing.Required instead.


7. FastAPI Dependency Injection

... inside Depends() tells FastAPI that a dependency is required and will be injected automatically.

from fastapi import Depends, FastAPI

app = FastAPI()

def get_api_key():
    return "secret"

@app.get("/data")
async def get_data(key: str = Depends(...)):
    # key is injected by FastAPI, not passed by the client
    return {"key": key}

8. typing.Concatenate with ParamSpec

When wrapping a function that adds or removes parameters, ... in the final Callable signature represents the captured parameter specification.

from typing import Callable, Concatenate, ParamSpec

P = ParamSpec("P")

def with_prefix(prefix: str) -> Callable[[Callable[Concatenate[str, P], None]], Callable[P, None]]:
    def decorator(func: Callable[Concatenate[str, P], None]) -> Callable[P, None]:
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
            return func(prefix, *args, **kwargs)
        return wrapper
    return decorator

# Usage
@with_prefix("LOG:")
def log(prefix: str, message: str) -> None:
    print(f"{prefix} {message}")

log("Hello")
# Prints: LOG: Hello

9. TypeVarTuple Default (Python 3.13+)

In Python 3.13+, you can give a TypeVarTuple a default using ... so that it can be omitted when the generic class is instantiated.

import sys
if sys.version_info >= (3, 13):
    from typing import Generic, TypeVarTuple, Unpack

    Ts = TypeVarTuple("Ts", default=...)

    class Stack(Generic[Unpack[Ts]]):
        def __init__(self, *items: Unpack[Ts]):
            self.items = items

    # Without default, Stack() would be an error
    s = Stack(1, 2, 3)   # OK, Ts inferred as (int, int, int)
    print(s.items)
else:
    print("TypeVarTuple default requires Python 3.13+")

10. Sentinel: Distinguishing “Not Provided” from None

Because ... is a unique object, it makes an excellent sentinel to separate “the user passed None” from “the user passed nothing at all.”

from typing import Optional

def update_profile(username: Optional[str] = ...):
    if username is ...:
        print("No change requested")
    elif username is None:
        print("Username explicitly deleted")
    else:
        print(f"Username changed to {username}")

update_profile()                # No change requested
update_profile(None)            # Username explicitly deleted
update_profile("new_user")      # Username changed to new_user

11. Stub Files (.pyi) and the Ellipsis

Type stub files (.pyi) contain type annotations for existing code. In a stub, the function body is almost always ... because only the signature matters for static analysis.

utils.py – the actual implementation:

def cube(n):
    if not isinstance(n, (int, float)):
        raise TypeError("Expected int or float")
    return n * n * n

utils.pyi – the type stub:

def cube(n: int | float) -> int | float:
    """Returns the cube of a number."""
    ...

Now type checkers can understand the exact signature, while the ellipsis acts as a syntactically correct no‑op body.

A small test script that uses the function:

from utils import cube

n = cube(3)
print(f"The cube of 3 is {n}.")

n2 = cube(4.4)
print(f"The cube of 4.4 is {n2}.")

Output:

The cube of 3 is 27.
The cube of 4.4 is 85.18400000000001.

The .pyi file is never executed; it’s a contract for type checkers like mypy.
The ellipsis is the standard placeholder for a stub function body.


12. Summary Table

Context Meaning of ...
Function / class body “Intentionally empty, to be implemented later”
Protocols & ABCs “This is an interface; the body is elsewhere”
Type hints: Callable[..., R] “Any number of parameters”
Type hints: Tuple[int, ...] “Homogeneous tuple of arbitrary length”
NumPy slicing “Expand all remaining dimensions”
Pydantic / FastAPI fields “This field is required (no default)”
FastAPI Depends(...) “Dependency is required and will be injected”
Custom __getitem__ A special key for “select everything”
TypeVarTuple default “Default to an empty type tuple”
Sentinel value “Argument was not provided”
Stub files (.pyi) “Signature only; implementation elsewhere”

13. Conclusion

The ellipsis literal is far more than a lazy placeholder. It is a first‑class Python object that serves as:

  • A clean stub in incomplete code
  • A fundamental building block of type hints and structural interfaces
  • A domain‑specific operator in scientific Python (NumPy)
  • A required‑ness marker in modern web frameworks (FastAPI)
  • A powerful sentinel for advanced function design
  • The standard body in type stub files

Understanding where and why to use ... makes your Python code more expressive, more precise, and — dare we say — more Pythonic.