ZetCode

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

last modified May 15, 2026

The three dots ... — officially the Ellipsis literal — are one of Python's most versatile yet consistently underestimated features. Like None, True, and False, the ellipsis is a built-in singleton constant. It can be written directly in source code, and every occurrence evaluates to the same unique object: Ellipsis.

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

Although many developers first encounter ... as a placeholder for unfinished code, the literal has a much broader role across the Python ecosystem. It appears in type hints, structural typing, abstract interfaces, NumPy slicing, Pydantic models, FastAPI parameters, sentinel patterns, and type stub files — each with its own precise semantics.

This guide provides a complete, professional-grade tour of every real-world use case, with runnable examples and explanations of why ... is the right tool in each context.

Placeholder for Unwritten Code

The most common beginner-level use of ... is as a clear, intentional placeholder. Where pass simply means this block is syntactically required, the ellipsis communicates something stronger: This code is not implemented yet — on purpose.

Because of that, ... is easier to spot during reviews, easier to search for, and harder to overlook than a quiet pass.

placeholder.py
def load_user_profile(user_id):
    ...   # TODO: fetch from database

class PaymentProcessor:
    ...   # interface planned, implementation pending

def handle_request(req):
    if req.method == "POST":
        ...   # validation & processing to be added

All of these examples run without errors because ... is a valid expression. Most linters and type checkers also recognize it as an intentional not yet implemented marker, making it a clean alternative to pass when the goal is to highlight unfinished work.

Ellipsis in Type Hints

Type hints use ... in several precise ways. In this context, the ellipsis is not a placeholder for unwritten code — it is part of the type system and carries specific meaning depending on where it appears.

The Callable[..., ReturnType]

When a function can accept any parameters but returns a known type, Callable[..., T] expresses exactly that. It is the type-safe way to say I don't care about the arguments, only the return type matters.

callable_ellipsis.py
from typing import Callable

# The Manager function
# It accepts any 'probe' as long as that probe returns a status string.
def run_diagnostic(probe: Callable[..., str], *args, **kwargs) -> None:
    print(f"--- Starting Diagnostic: {probe.__name__} ---")
    
    # We execute the probe with whatever specific arguments it requires
    report = probe(*args, **kwargs)
    
    # The manager only cares that the result is a string it can print
    print(f"REPORT: {report.upper()}")
    print("--- Diagnostic Complete ---\n")

# Specific 'Probes' with different signatures
def check_database(host: str, port: int) -> str:
    return f"Connected to DB at {host}:{port}. Latency is 5ms."

def check_disk_space(drive: str) -> str:
    return f"Drive {drive} is at 85% capacity."

def simple_ping() -> str:
    return "Server is alive."


run_diagnostic(check_database, host="127.0.0.1", port=5432)
run_diagnostic(check_disk_space, drive="/dev/sda1")
run_diagnostic(simple_ping)

When used in a type hint like Callable[..., str], the ellipsis acts as a wildcard for the function's parameters. It tells the type checker that the callable may accept any combination of positional or keyword arguments — the wrapper or dispatcher doesn't need to know or constrain them. Only the return type matters. Even though the inputs are completely unconstrained, the output type (str in this case) is still enforced by static analyzers such as mypy and Pyright.

This pattern makes a function like run_diagnostic genuinely extensible. You can plug in new probes with wildly different signatures — some taking no arguments, others taking ten — and the manager function never needs to change. The ellipsis gives you maximum flexibility at the call site while still preserving strong guarantees about what comes back.

$ python callable_ellipsis.py
--- Starting Diagnostic: check_database ---
REPORT: CONNECTED TO DB AT 127.0.0.1:5432. LATENCY IS 5MS.
--- Diagnostic Complete ---

--- Starting Diagnostic: check_disk_space ---
REPORT: DRIVE /DEV/SDA1 IS AT 85% CAPACITY.
--- Diagnostic Complete ---

--- Starting Diagnostic: simple_ping ---
REPORT: SERVER IS ALIVE.
--- Diagnostic Complete ---

The Tuple[int, ...]

In tuple types, ... means a homogeneous tuple of arbitrary length. This is different from Tuple[int, int], which means a tuple of exactly two integers. With Tuple[int, ...], you can have a tuple of any length.

tuple_ellipsis.py
from typing import Tuple

# This function calculates the price "swing" (max - min) 
# for any given period of history.
def calculate_volatility(prices: Tuple[int, ...]) -> int:
    if not prices:
        return 0
    
    # We don't know if 'prices' has 2 elements or 2,000.
    # The ellipsis (...) tells the type checker that's okay!
    swing = max(prices) - min(prices)
    return swing

# Case A: A short weekend of data
weekend_data = (150, 155) 

# Case B: A full business week
week_data = (148, 152, 155, 149, 153)

# Case C: A single flash-sale data point
single_point = (160,)

print(f"Weekend Volatility: ${calculate_volatility(weekend_data)}")
print(f"Weekly Volatility:  ${calculate_volatility(week_data)}")
print(f"Instant Volatility: ${calculate_volatility(single_point)}")

We are building a tool to analyze stock market trends. We don't know how many days of data the user will provide, but we want to ensure the data is immutable (cannot be changed) and consists entirely of whole-number prices.

By using Tuple[int, ...], we can accept a tuple of any length while still enforcing that all elements are integers. This allows our function to be flexible and adaptable to different use cases, from analyzing a short weekend of data to a full week of trading, or even just a single data point. The ellipsis in the type hint communicates to both developers and static type checkers that the tuple can be of arbitrary length, as long as it contains integers.

$ python tuple_ellipsis.py
Weekend Volatility: $5
Weekly Volatility:  $7
Instant Volatility: $0

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.

protocol_example.py
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())

p = Person("Alice")
r = Robot()
d = Dog()

welcome(p)
welcome(r)
welcome(d)

The ... in the Greeter protocol indicates that the method has no implementation in the protocol itself. Any class that defines a greet method with the correct signature will be considered a Greeter, even if it doesn't explicitly inherit from the protocol.

$ python protocol_example.py
Hello, I am Alice
Beep boop
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.

Abstract Base Classes (ABCs)

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

shapes.py
from abc import ABC, abstractmethod
import math

# The "Contract"
class Shape(ABC):
    @abstractmethod
    def calculate_area(self) -> float:
        ...  # "I'm a concept. I don't have an area yet!"

# Following the contract
class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def calculate_area(self) -> float:
        return math.pi * (self.radius ** 2)

# Following the contract
class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def calculate_area(self) -> float:
        return self.side * self.side


# This would crash! You can't hold a "concept" in your hand.
# generic_shape = Shape() 

shapes = [Circle(5), Square(4)]

for s in shapes:
    print(f"Area: {s.calculate_area():.2f}")

The ... in the Shape class's calculate_areamethod indicates that this method is abstract and has no implementation in the base class. Subclasses must provide their own implementation of calculate_area, and the ellipsis serves as a clear marker of this fact.

$ python shapes.py
Area: 78.54
Area: 16.00

NumPy and Multi-dimensional Slicing

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

numpy_ellipsis.py
import numpy as np

arr = np.zeros((2, 3, 4, 5))
sliced = arr[0, ...]   # shape (3, 4, 5)
print(sliced.shape)

The ... in the slice expands to : for all remaining dimensions, so arr[0, ...] is equivalent to arr[0, :, :, :].

$ python numpy_ellipsis.py
(3, 4, 5)

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

custom_getitem.py
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[...])
print(t[1])

The ... in the __getitem__ method allows the custom class to handle the ellipsis syntax, similar to NumPy arrays.

$ python custom_getitem.py
<__main__.MyTensor object at 0x...>
2

Pydantic and FastAPI: Required Fields

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

pydantic_required.py
from pydantic import BaseModel

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

item = Item(name="Laptop", description="A powerful machine")
print(item)

If you try to create an Item without a description, it raises a validation error: ValidationError: 1 validation error for Item.

$ python pydantic_required.py
name='Laptop' description='A powerful machine'

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

fastapi_query.py
from fastapi import FastAPI, Query

app = FastAPI()

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

When making a request to /items/?q=abc it works, but /items/ or /items/?q=ab returns a 422 error because the required query parameter is missing or too short.

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

FastAPI Dependency Injection

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

fastapi_depends.py
from fastapi import Depends, FastAPI

app = FastAPI()

def get_api_key():
    return "secret"

@app.get("/data")
async def get_data(key: str = Depends(...)):
    return {"key": key}

The key parameter is marked as required using Depends(...). If the dependency is not provided, FastAPI will raise an error.

typing.Concatenate with ParamSpec

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

concatenate_ellipsis.py
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

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

log("Hello")

The ... in the Callable signature is a placeholder for the parameters captured by ParamSpec. This allows the decorator to work with any function signature while still providing type safety.

$ python concatenate_ellipsis.py
LOG: Hello

TypeVarTuple Default

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

typevartuple_default.py
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

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

The ... in TypeVarTuple means that if no type parameters are provided, it defaults to an empty tuple. This allows you to create a generic class that can be used with or without explicit type parameters, improving usability while maintaining type safety when parameters are provided.

Sentinel: Distinguishing Not Provided from None

A sentinel is a special, one-of-a-kind value used to signal a specific condition that cannot be confused with any legitimate argument a user might pass. In Python, sentinels are commonly used to distinguish between:

Using None as a default is often ambiguous, because None may itself be a meaningful, intentional input. A proper sentinel must therefore be something the caller would never pass accidentally.

Because ... (the Ellipsis object) is a unique, globally shared singleton, it makes an excellent sentinel. Unlike None, which is frequently used as a real argument, the ellipsis literal is almost never supplied intentionally in application-level code. This gives you a reliable way to distinguish between:

Using ... as a sentinel avoids collisions with valid values, keeps the function signature clean, and removes the need to create a custom sentinel object manually. Since Ellipsis is guaranteed to be unique across the entire interpreter, identity checks (is ...) are fast, unambiguous, and safe.

sentinel_ellipsis.py
from types import EllipsisType

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

update_profile()
update_profile(None)
update_profile("new_user")

This pattern is especially useful in PATCH-style update functions where omitting an argument means leave this field alone, passing None means clear this field, and passing a value means set it to this. Without the sentinel, None would have to serve double duty, and the two cases become indistinguishable. Note that EllipsisType requires Python 3.10 or later.

$ python sentinel_ellipsis.py
No change requested
Username explicitly deleted
Username changed to new_user

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.

The actual implementation (utils.py):

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

The cube function is not type-annotated in the implementation. The type annotations are provided in a separate stub file.

The type stub is in the the (utils.pyi) file:

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

The ... in the stub file indicates that the function has no implementation in the stub itself; it's just a declaration of the function's signature and docstring for type checkers and IDEs.

The test code uses the cube function:

test_cube.py
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}.")

When you run test_cube.py, it imports the actual implementation from utils.py, not the stub. The stub is only used for type checking and IDE features, while the real code in utils.py is executed at runtime.

$ python test_cube.py
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 or pyright. The ellipsis is the standard placeholder for a stub function body.

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

Conclusion

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

Understanding where and why to use ... makes your Python code more expressive, more precise, and more in line with the conventions of the Python ecosystem. The next time you see ..., you'll know it's not just a placeholder — it's a versatile tool with specific meaning in every context.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.