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.
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.
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.
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.
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.
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.
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.
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__:
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).
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.
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 insideTypedDictto mark a key as required. Usetotal=Falseortyping.Requiredinstead.
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(...)):
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.
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.
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:
- the caller passed nothing for this parameter
- the caller passed a value — even if that value is
None,0,"", orFalse
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:
- the user explicitly passed
None - the user passed nothing at all
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.
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):
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:
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:
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:
- 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 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
List all Python tutorials.