Package castfit

castfit: basic type casting

Cuddles the Cat
Cuddles the Cat
"If it fits, I sits."

Build PyPI Supported Python Versions

Why?

castfit() helps you convert things like command-line arguments (e.g., from docopt) and simple API responses into something more typed with low overhead.

Install

# modern (recommended)
uv add castfit

# classic
python -m pip install castfit

Alternatively, you can just download the single file and name it castfit.py.

Example: CLI-like Args

# Example: CLI-like args
from typing import Optional
from pathlib import Path
from castfit import castfit


class Args:
    host: str
    port: int
    timeout: Optional[float]
    log: Path


data = {
    "host": "localhost",
    "port": "8080",
    # "timeout": "5.0" # key can be missing
    "log": "app.log",
}

config = castfit(Args, data)
assert config.host == "localhost"
assert config.port == 8080
assert config.timeout is None
assert config.log == Path("app.log")

# if timeout was present:
data = {"host": "localhost", "port": "8080", "timeout": "5.0", "log": "app.log"}
config = castfit(Args, data)
assert config.host == "localhost"
assert config.port == 8080
assert config.timeout == 5.0
assert config.log == Path("app.log")

Example: Nested Types

# Example: nested types
from dataclasses import dataclass
from typing import Literal
from castfit import castfit


@dataclass
class Pet:
    name: str
    type: Literal["cat", "dog", "other"]
    age: int


@dataclass
class Owner:
    name: str
    pets: list[Pet]


owner_data = {
    "name": "Alice",
    "pets": [
        {"name": "Cuddles", "type": "cat", "age": "4"},
        {"name": "Buddy", "type": "dog", "age": "2.5"},  # age will be cast to int(2)
    ],
}

owner = castfit(Owner, owner_data)

assert owner.name == "Alice"
assert len(owner.pets) == 2
assert isinstance(owner.pets[0], Pet)
assert owner.pets[0].name == "Cuddles"
assert owner.pets[0].type == "cat"
assert owner.pets[0].age == 4
assert owner.pets[1].name == "Buddy"
assert owner.pets[1].age == 2  # Cast from "2.5" to int

Example: Custom Functions

# Example: adding a custom converter

from dataclasses import dataclass
import castfit


@dataclass
class LatLon:
    lat: float
    lon: float


@castfit.casts
def str_to_latlon(s: str) -> LatLon:
    lat, lon = map(float, s.split(","))
    return LatLon(lat, lon)


assert castfit.to_type("40.7,-74.0", LatLon) == LatLon(40.7, -74.0)

Other Projects

  • pydantic: comprehensive, but feels heavy.
  • cattrs: good simple cases, but has a complex set of converters.

License

MIT License

Global variables

var TypeForm

Type and special forms like Any, Union, etc.

var CastFn

Function signature that maps a value to a type.

Functions

def iterate(*items: T | Iterable[T]) ‑> Iterator[~T]

Return an iterator over individual or collections of items.

NOTE: Although str is Iterable, we treat it as an individual item.

Args

*items : T | Iterable[T]
one or more items to be iterated

Yields

T
an individual item

Examples:

Strings are treated like individual items.

>>> list(iterate("hello")) == ["hello"]
True

Iterate over lists or individual items:

>>> list(iterate([1, 2], 3)) == [1, 2, 3]
True

You can iterate over multiple lists:

>>> list(iterate([3, 4], [5, 6])) == [3, 4, 5, 6]
True
def setkeys(d: dict[Any, Any], *values: dict[Any, Any], **extra: Any) ‑> dict[typing.Any, typing.Any]

Return dict that has been updated by values and extra.

def setattrs(obj: T, *values: dict[str, Any], **extra: Any) ‑> ~T

Like setattr() but for multiple values and returns the object.

def type_info(item: Any, use_cache: bool = True) ‑> TypeInfo

Return type information about item.

This function is similar to builtins.type except that it returns an object that which includes additional metadata.

When item has an origin, we return:

  • origin (from typing.get_origin)
  • args (from typing.get_args)

For special forms (Any, Literal, Union, etc.) and classes we set origin to be the item.

Args

item : Any
the value to get info about
use_cache : bool, default=True
whether or not to lookup/store results in a local cache. Even when use_cache=True if item is an instance, Literal, Union, or UnionType, it will not be stored in the local cache.

Returns

TypeInfo
item type information
def type_hints(item: type[T] | Callable[..., Any]) ‑> dict[str, TypeInfo]

Returns names and inferred types for item.

This function merges some of the behavior of typing.get_type_hints for classes and inspect.signature for other callables.

Args

item : type[T] | Callable[..., Any]
a class, lambda, or function

Returns

(dict[str, TypeInfo]): mapping of field/parameter names to hint information See:

def is_subtype(left: TypeForm[T], right: TypeForm[K], *, covariant: bool = False, contravariant: bool = False, implicit: bool = True) ‑> bool

Return True if left is a subtype of right.

In many ways this function is an extension of issubclass to certain special forms:

  • Any
  • Never, NoReturn
  • NoneType
  • Literal
  • Union, UnionType
  • Callable
  • Sequence, Mapping, list, dict, set, tuple
  • Generic (using TypeVar to set covariant and contravariant)

NOTE: This is not an exhaustive type checker.

This function also supports variance comparisons: - implicit (default enabled): left is a numeric subtype of right as defined by PEP 3141 - invariant (default enabled): left and right must match exactly (unless implicit matched first) - covariant (default disabled): left is a subclass of right - contravariant (default disabled): left is a superclass of right

Args

left : TypeForm[T]
a supported type form.
right : TypeForm[K]
a supported type form.
covariant : bool, default=False
if True, left may be a subclass of right.
contravariant : bool, default=False
if True, left may be a superclass of right.
implicit : bool, default=True
if True, support the PEP 3141 numeric tower implicit conversions, even when otherwise invariant comparisons are being made.

Returns

bool
True if left is a subtype of right, False otherwise.

See

def is_type(value: Any, kind: TypeForm[T]) ‑> bool

Return True if value is of a type compatible with kind.

def to_type(value: Any, kind: TypeForm[T], casts: Optional[Casts] = None) ‑> ~T

Try to cast value to the type of kind.

Args

value : Any
the value to convert
kind : TypeForm[T]
the type to convert to
casts : Optional[Casts], default=None
if provided, this mapping of tuple(source type, result type) to converter functions will be used instead of the registered converters.

Returns

Any
the requested type

Raises

TypeError
if there are any problems converting value to kind
def casts(*args: Any, **kwargs: Any) ‑> Any

Register a function to convert from/to one or more types.

Zero Argument Form
Converts from first arg type to return type.

@casts
def f(x: int) -> str: return str(x)

One Argument Form
Converts from Any to to types.

@casts(to=str)
def f(x): return str(x)

Two Argument Form
Converts from src types to to types.

@casts(int, to=str)
def f(x): return str(x)

Functional Form
Register the function explicitly.

casts(int, str, lambda x: str(x))

Args

src (TypeForm[T] | Iterable[TypeForm[T]]): one ore more types to convert from

to (TypeForm[T] | Iterable[TypeForm[T]]): one ore more types to convert to

func : CastFn[Any]
converter function

Returns

(CastFn[Any] | Callable[[CastFn[Any]], CastFn[Any]]): converter function or a function that returns the converter function

def castfit(spec: type[T], data: dict[str, Any], *, casts: Optional[Casts] = None) ‑> ~T

Return an instance of spec using data to set field values.

We use type hints to help us figure out how to convert the provided data into the field values.

Args

spec : type[T]
plain class or dataclass; use type hints and defaults to define how the data should be converted.
data : dict[str, Any]
field names mapped to values
casts : Optional[Casts], default=None
if provided, this mapping of tuple(source type, result type) to converter functions will be used instead of the registered converters.

Returns

Any
an instance of spec with fields set

Raises

TypeError
if there is a problem converting any of the fields

Classes

class TypeInfo (...)

Type information.

Expand source code
@dataclass(frozen=True)
class TypeInfo:
    """Type information."""

    name: str = ""
    """Name of the field or parameter."""

    hint: TypeForm[Any] = Any
    """Type hint or inferred type."""

    default: Any = Parameter.empty
    """Default value, if any."""

    origin: Any = Any
    """Type origin.

    See: [`typing.get_origin`](https://docs.python.org/3/library/typing.html#typing.get_origin)
    """

    args: tuple[Any, ...] = field(default_factory=tuple)
    """Type arguments.

    See: [`typing.get_args`](https://docs.python.org/3/library/typing.html#typing.get_args)
    """

Class variables

var args : tuple[typing.Any, ...]

Type arguments.

See: typing.get_args

var name : str

Name of the field or parameter.

var hint : type[Any] | Any

Type hint or inferred type.

var default : Any

Default value, if any.

var origin : Any

Type origin.

See: typing.get_origin