typed-sentinels¶
Statically-typed sentinel objects for Python 3.9+.
Overview¶
Sentinel instances provide unique placeholder objects for a given type. They enable type-safe default values when
None isn't suitable, especially for custom or complex types that require runtime parameters.
Key Features¶
- Type safety: Sentinels always appear as their target type to static type checkers.
- Singleton behavior: Only one instance per type, ensuring identity consistency.
- Always falsy: Natural
if notpatterns work as expected. - Lightweight & thread-safe: Minimal memory footprint and overhead, while remaining thread-safe.
- No external dependencies: Written entirely using Python's standard libary.
Installation¶
Installation using pip¶
Installation using uv¶
Usage¶
Basic Usage¶
from typed_sentinels import Sentinel
# Create a sentinel that appears as a `str` type to the type checker
SNTL = Sentinel(str)
def process_data(value: str = SNTL) -> str:
if not value: # Sentinels are always falsy
return 'No value provided'
return f'Processing: {value}'
# Usage
result = print(process_data()) # Prints "No value provided"
result = print(process_data('data')) # Prints "Processing: data"
Custom Classes¶
Perfect for types requiring runtime parameters:
class DatabaseConfig:
def __init__(self, host: str, port: int) -> None:
self.host = host
self.port = port
# No need to instantiate the class
SNTL_DB = Sentinel(DatabaseConfig)
def connect(config: DatabaseConfig = SNTL_DB) -> str:
if not config:
return 'Using default connection'
return f'Connecting to {config.host}:{config.port}'
Syntax Variants¶
# Equivalent ways to create the same sentinel
SNTL_A = Sentinel(tuple[str, ...])
SNTL_B = Sentinel[tuple[str, ...]]()
# Both create the same singleton instance
print(SNTL_A is SNTL_B) # True
A Note on Linting¶
To avoid linter warnings (like Ruff B008), always define sentinels at module level rather than in-line:
# Module-level definition
EMPTY_LIST = Sentinel(list[str])
def process_items(items: list[str] = EMPTY_LIST) -> list[str]:
if not items:
return []
return [*items, 'processed']
Rather than doing it this way, with the Sentinel instance being created in-line as the parameter default:
# Inline definition (triggers Ruff B008)
def process_items(items: list[str] = Sentinel(list[str])) -> list[str]:
if not items:
return []
return [*items, 'processed']
Note, however, that this will technically work fine, without linter complaints, in cases where the type itself is
considered to be immutable, e.g., tuple.
Reference¶
For complete API documentation, see the API Reference.