Skip to main content

Making types flexible.

Project description

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.13 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flextype-0.1.1.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flextype-0.1.1-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

File details

Details for the file flextype-0.1.1.tar.gz.

File metadata

  • Download URL: flextype-0.1.1.tar.gz
  • Upload date:
  • Size: 20.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for flextype-0.1.1.tar.gz
Algorithm Hash digest
SHA256 dfe12138039816badf26b57c6944112949884045e4ebfef51c7b7d19bc4a0264
MD5 a5b71fba59b43048eb93b73de1ac92c5
BLAKE2b-256 9985f1b45b7d586d9dda4e0e16625c416099591131cbc1dbc17995a6118abb3a

See more details on using hashes here.

Provenance

The following attestation bundles were made for flextype-0.1.1.tar.gz:

Publisher: publish-release.yml on Cortys/flextype

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flextype-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: flextype-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for flextype-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a86f12428bfdfdf5a52c02b6c53c08d5c4f02bce75d8d180182df0c538d03674
MD5 a0929c87b8714dad4729450b0103190a
BLAKE2b-256 fb6f8b045677127b6fa5a64b2ebcd1cec8d6f03dbcacdebc4a14be92e5df8fa0

See more details on using hashes here.

Provenance

The following attestation bundles were made for flextype-0.1.1-py3-none-any.whl:

Publisher: publish-release.yml on Cortys/flextype

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page