Quickstart

alternative gives one function a set of named implementations. The first implementation is the reference. Extra implementations are registered from the reference object or from any registered implementation.

Register Implementations

Use alternative.reference() on the implementation you trust most.

import alternative


@alternative.reference
def normalise_name(name: str) -> str:
    return " ".join(part.capitalize() for part in name.split())


@normalise_name.add
def normalise_name_title(name: str) -> str:
    return name.title()

normalise_name is now an alternative.Alternatives object, and normalise_name_title is an alternative.Implementation object. Both are callable:

assert normalise_name("ada lovelace") == "Ada Lovelace"
assert normalise_name_title("ada lovelace") == "Ada Lovelace"

Choose a Default

If no explicit default is registered, calling the reference object uses the reference implementation. Use default=True to make a candidate the normal runtime implementation:

import alternative


@alternative.reference
def display_name(name: str) -> str:
    return " ".join(part.capitalize() for part in name.split())


@display_name.add(default=True)
def display_name_fast(name: str) -> str:
    return name.title()


assert display_name("grace hopper") == "Grace Hopper"

Only one explicit default can be registered. This catches accidental import order changes where two modules both try to choose the active implementation.

Use Decorator or Direct Call Syntax

All registration helpers support decorator and direct-call forms:

@alternative.reference
def reference_impl() -> int:
    return 1


def candidate_impl() -> int:
    return int(True)


candidate = reference_impl.add(candidate_impl)

Use the decorator style when defining implementations together. Use direct calls when importing a candidate from another module.

Add Through an Implementation

An alternative.Implementation forwards .add(...) to its parent alternatives set. This makes chained registration convenient:

@alternative.reference
def parse_count(text: str) -> int:
    return int(text.strip())


@parse_count.add
def parse_count_decimal(text: str) -> int:
    return int(text, 10)


@parse_count_decimal.add(default=True)
def parse_count_fast(text: str) -> int:
    return int(text)

The three implementations still belong to the same alternatives set.

Use Methods

alternative follows Python descriptor binding rules, so the same decorator also works on methods. Put @alternative.reference outside @classmethod or @staticmethod when those decorators are needed:

class Parser:
    @alternative.reference
    def parse(self, value: str) -> int:
        return int(value.strip())

    @parse.add(default=True)
    def parse_fast(self, value: str) -> int:
        return int(value)

    @alternative.reference
    @classmethod
    def from_text(cls, value: str) -> "Parser":
        return cls(value.strip())

    @from_text.add(default=True)
    @classmethod
    def from_text_fast(cls, value: str) -> "Parser":
        return cls(value)

    @alternative.reference
    @staticmethod
    def is_valid(value: str) -> bool:
        return value.strip().isdigit()

    @is_valid.add(default=True)
    @staticmethod
    def is_valid_fast(value: str) -> bool:
        return value.isdigit()

Calling through an instance or class binds self and cls normally. Direct alternative implementations also bind normally, so parser.parse_fast("1") or Parser.from_text_fast("1") call that implementation directly.