Skip to content

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 not patterns 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

pip install typed-sentinels

Installation using uv

uv add typed-sentinels

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.