Skip to main content

flextype

flextype makes working with types in Python more flexible. It provides primitives for writing code with conditional dispatch paths for optional dependencies.

Additionally, it extends the idea of dynamic subclass registrations provided by abc.ABCMeta to the instance-level, enabling the definition of types with dynamically defined instance sets. This is useful to define marker types, enabling conditional dispatch of objects to different implementations based on those marker types.

Install

flextype requires Python 3.12 or newer.

pip install flextype
uv add flextype

Quickstart

Lazy Type Checking

lazy_isinstance and lazy_issubclass work like isinstance and issubclass, but type references may be real classes, fully qualified string names, unions, or tuples. String references are matched against the actual runtime type name, so optional dependencies do not need to be imported just to perform a check.

from pathlib import Path

from flextype import lazy_isinstance, lazy_issubclass


path = Path("README.md")

assert lazy_isinstance(path, "pathlib.Path")
assert lazy_isinstance(path, (Path, "os.PathLike"))
assert lazy_issubclass(type(path), ("pathlib.Path", str))

flexdispatch

flexdispatch is a lazy alternative to functools.singledispatch. It accepts regular registrations, lazy string registrations, and delayed registrations.

from flextype import flexdispatch


@flexdispatch
def render(value: object) -> str:
    return "object"


@render.register("pathlib.Path")
def render_path(value: object) -> str:
    return f"path: {value}"

String registrations are resolved lazily. In the example above, pathlib.Path is converted into the real class only after a matching value is dispatched.

Delayed registrations are useful when the implementation itself lives in an optional integration module. Register a callback against a lazy type name, then import the module that defines the regular registrations only when that type is first encountered.

# myapp/render.py
from flextype import flexdispatch


@flexdispatch
def render(value: object) -> str:
    return repr(value)


@render.delayed_register("pandas.DataFrame")
def _load_pandas_handlers(seen_type: type) -> None:
    import myapp.pandas_render  # noqa: F401
# myapp/pandas_render.py
import pandas as pd

from myapp.render import render


@render.register(pd.DataFrame)
def render_dataframe(value: pd.DataFrame) -> str:
    return value.to_markdown()

The delayed callback receives the concrete matching type and runs once. After the import, dispatch uses the regular pd.DataFrame registration.

Registries

ProtocolRegistry is useful for defining marker types: runtime-only semantic categories that objects can opt into without changing their inheritance hierarchy. Unlike regular protocols, these markers can distinguish two objects of the same class. That makes them useful for conditional dispatch based on state, provenance, validation, or other facts that are not captured by the object's structural interface.

from flextype import ProtocolRegistry, flexdispatch


class Verified(ProtocolRegistry, structural_checking=False):
    pass


@flexdispatch
def publish(document: object) -> str:
    return "manual review required"


@publish.register(Verified)
def publish_verified(document: Verified) -> str:
    return "publish immediately"


class Document:
    def __init__(self, title: str) -> None:
        self.title = title


draft = Document("Draft")
release = Document("Release")

Verified.register_instance(release)

assert publish(draft) == "manual review required"
assert publish(release) == "publish immediately"

Use __instancehook__ when membership in a marker type should be dynamic. It is called during isinstance(value, MarkerType) and can classify objects by runtime state, contents, or other predicates instead of explicit registration.

from flextype import ProtocolRegistry, flexdispatch


class IntList(ProtocolRegistry, structural_checking=False):
    @classmethod
    def __instancehook__(cls, instance: object, /) -> bool:
        return isinstance(instance, list) and all(
            isinstance(item, int) for item in instance
        )


@flexdispatch
def summarize(value: object) -> str:
    return "not an int list"


@summarize.register(IntList)
def summarize_int_list(value: list[int]) -> str:
    return f"sum={sum(value)}"


assert summarize([1, 2, 3]) == "sum=6"
assert summarize([1, "2", 3]) == "not an int list"

Return NotImplemented from __instancehook__ to fall back to regular inheritance and explicit register / register_instance checks.

By default, ProtocolRegistry preserves normal protocol structural checks. Pass structural_checking=False when the type should behave as an explicit marker instead: only inheritance, explicit registrations, and __instancehook__ decide whether an object is an instance of the marker type.

ProtocolRegistry is backed by ProtocolRegistryMeta, which extends the ABC registration concept to instance-level registrations. Registry and RegistryMeta expose the same mechanism for non-protocol marker classes, and the metaclasses are exported too when direct metaclass use is the better fit.

Development

uv run ruff check
uv run ty check
uv run pytest

Pre-commit hooks are managed with prek:

uv run prek install
uv run prek run --all-files

License

This project is licensed under the Apache License 2.0.

Metadata

Release files for flextype 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flextype 0.1.2
File Size Uploaded
flextype-0.1.2.tar.gz 21.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flextype 0.1.2
File Interpreter ABI Platform
flextype-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 41.6 kB

Release files / flextype-0.1.2.tar.gz

Download URL flextype-0.1.2.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d0c671ef1fb33e1f60e1afc0b386132e84d977bbda63dc46b8021b3eef32e8c6
BLAKE2b-256 checksum
How to use checksums
bb7f83466b8152e8e9689b426a8f01126e59831aa9624f29e49576b754c4f4c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Release files / flextype-0.1.2-py3-none-any.whl

Download URL flextype-0.1.2-py3-none-any.whl
Size 20.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a61ff10dd063c2d7fcd72418e45d80e730088ea9c217a79b091024d8b9987aa7
BLAKE2b-256 checksum
How to use checksums
3cda56c3fe7f241cfd3f0a2e758b8c1169eef46bd70fae31c0fc6594fc69cc57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page