cast: The Coercion Chamber#

class my.typing.cast.TypeCast#

Coerce data between arbitrary type pairs via a table of registered transforms.

Casting routes through transforms keyed by (source, target) type bounds and tried from most- to least-specific; the first candidate that neither declines nor fails decides the result. cast is the primary entry point, with multicast and flexcast as convenience facades; the per-cast state lives in the ephemeral Transform class.

Examples

Coerce stringly-typed data into shape:

>>> from my import ty
>>> ty.cast({'a': '1'}, dict[str, int])
{'a': 1}

I Registration#

static TypeCast.register(fn: F) → F#

Register a function as a cast transform based on its type parameters.

The decorated function declares its source and target bounds as PEP 695 type parameters (e.g. def _string_to_scalar[S: String, T: Scalar](self: Transform) -> ...); the pair is inserted into the transform table just before any more-general entry, so specific transforms are always tried first. Definitions discovered during module import queue for the initial setup; later registrations install immediately and invalidate memoized dispatch candidates, so the next cast sees the extension.

classmethod TypeCast.setup() → None#

Install every queued transform into the registry; only the first call has any effect.

II Interface#

static TypeCast.cast(data: A, target: AnyType[B], default: B, *, flags: CastFlags | CastPreset | None = None) → B#
static TypeCast.cast(data: A, target: AnyType[B], default: C, *, flags: CastFlags | CastPreset | None = None) → B | C
static TypeCast.cast(data: A, target: AnyType[B], *, flags: CastFlags | CastPreset | None = None) → B | None
static TypeCast.cast(data: A, *, source: AnyType[A], target: AnyType[B], flags: CastFlags | CastPreset | None = None) → B | None
static TypeCast.cast(data: A, target: AnyType[B], *, source: AnyType[A] | None = None, flex: True, flags: CastFlags | CastPreset | None = None) → B | A

Cast data to the target type, trying registered transforms from specific to general.

Parameters:
  • data – The source data to cast.

  • target – The target type to cast to.

  • default – The default value to return if casting fails.

  • source – An explicit type for the source data, inferred from data when omitted.

  • flex – Whether to use “flexcasting”, which falls back to the original input data rather than returning None.

  • flags – An explicit CastFlags snapshot (or preset-level name) to use for this cast; None (the default) snapshots the global Typist singleton’s current flags once, at this entry point, so the whole cast (and any nested casts it triggers) sees one consistent flag set even if the singleton is mutated mid-flight.

Returns:

The cast data on success; otherwise default (if given), the original data (if flex is set), or None.

Raises:

Decline – If the data contains a reference cycle, which cannot be cast.

Examples

Coerce a stringly-typed record into shape:

>>> from my import ty
>>> ty.cast({'a': '1'}, dict[str, int])
{'a': 1}
>>> ty.cast('1, 2, 3', list[int])
[1, 2, 3]
>>> from datetime import date
>>> ty.cast('2026-07-21', date)
datetime.date(2026, 7, 21)

A failed cast returns the default (if any), else None:

>>> ty.cast('hello', int, 0)
0
>>> ty.cast('hello', int) is None
True

Tighten (or loosen) the leniency flags for just this call:

>>> ty.cast('3', list[int])
[3]
>>> ty.cast('3', list[int], flags='strict') is None
True
classmethod TypeCast.multicast(data: Iterable, target: AnyType, flags: CastFlags | CastPreset | None = None) → list[B | None]#

Cast each element of an iterable to target, preserving None elements.

Parameters:
  • data – The iterable whose elements to cast.

  • target – The type each (non-None) element is cast to.

  • flags – An explicit CastFlags snapshot (or preset-level name); resolved once here so every element casts against the same flag set. See TypeCast.cast.

Returns:

A list of cast elements, with None elements kept as-is.

Examples

Cast elements individually, preserving None placeholders:

>>> from my import ty
>>> ty.multicast(['1', None, '3'], int)
[1, None, 3]
classmethod TypeCast.flexcast(data: A, target: AnyType, flags: CastFlags | CastPreset | None = None) → B | A#

Cast data to target, falling back to the original data when the cast fails.

Parameters:
  • data – The source data to cast.

  • target – The type to cast to.

  • flags – An explicit CastFlags snapshot (or preset-level name). See TypeCast.cast.

Returns:

The cast value on success, otherwise the original data unchanged.

Examples

Keep the original value when no coercion is possible:

>>> from my import ty
>>> ty.flexcast('42', int)
42
>>> ty.flexcast('hello', int)
'hello'
classmethod TypeCast.normalize(data: str | bytes | bytearray | memoryview | IO) → str#
classmethod TypeCast.normalize(data: VecT[V] | _Iter[V]) → list[V]
classmethod TypeCast.normalize(data: MapT[K, V]) → dict[K, V]
classmethod TypeCast.normalize(data: V) → V

Normalize the input data into a more workable form for casting.

Strings decode to str, mappings (and item views) become dicts, and other iterables become lists; everything else passes through untouched.

Examples

Collapse the many container interfaces down to a few workable ones:

>>> from my import ty
>>> ty.normalize({'a': 1}.items())
{'a': 1}
>>> ty.normalize((1, 2))
[1, 2]
>>> ty.normalize(b'hi')
'hi'
classmethod TypeCast.read_scalars(data: str | bytes | bytearray | memoryview | IO, tvar: type[S] | MyType[S]) → list[S]#
classmethod TypeCast.read_scalars(data: str | bytes | bytearray | memoryview | IO) → list[int | float | complex | bool]

Attempt to read scalar values out of a string using regex patterns.

Note that a bool target currently yields the raw regex matches (truthy for matched true-forms, None otherwise) rather than proper booleans.

Parameters:
  • data – The source data to read from.

  • tvar – The scalar type to seek out.

Returns:

A list of the scalar values found, empty when nothing matches.

Examples

Extract every int embedded in a sentence:

>>> from my import ty
>>> ty.read_scalars('I have 3 cats and 12 dogs', int)
[3, 12]

III CastFlags#

class my.typing.cast.CastFlags(*, firsts: bool = True, atomics: bool = True, splits: bool = True, wraps: bool = True)#

Frozen, hashable snapshot of the cast chamber’s four leniency flags.

Mirrors Typist’s firsts/atomics/splits/wraps fields, but as a value object that is resolved once per public cast() entry point and threaded through the Transform recursion, instead of being read live off the mutable global Typist singleton mid-cast. See docs/DESIGN-cast-flags.md for the full rationale.

model_config = {'frozen': True}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

firsts: bool#

Vec -> Atom Collapse a multi-element series to its first element ([1, 2] -> 1).

atomics: bool#

Vec -> Atom Unwrap a single-element series ([1] -> 1).

splits: bool#

String -> Struct Split a string before casting it to a collection ('a.b' -> ['a', 'b']).

wraps: bool#

Atom <-> Struct Wrap an atom into a collection, and vice-versa ('a' -> ['a']).

classmethod preset(level: CastPreset = 'basic') → CastFlags#

Build a CastFlags bundle for a strictness tier.

Parameters:

level – The strictness tier – ‘strict’ disables every loose coercion, ‘basic’ enables the everyday conveniences, and ‘flex’ additionally wraps atoms into collections.

Returns:

A CastFlags instance for the given tier.

Examples

Bundle a full strictness tier in one call:

>>> from my import CastFlags
>>> CastFlags.preset('strict')
CastFlags(firsts=False, atomics=False, splits=False, wraps=False)
classmethod resolve(flags: CastFlags | CastPreset | None) → CastFlags#

Resolve a per-call flags= argument to a concrete snapshot.

An explicit CastFlags instance or preset-level string wins outright; None snapshots the live Typist singleton’s current fields – the compatibility seam that keeps direct ty.splits = ... mutation (and friends) working as the process-wide default source, while still giving each cast one consistent flag set from start to finish.

Examples

Resolve a preset name, or pass an explicit snapshot through untouched:

>>> from my import CastFlags
>>> CastFlags.resolve('basic').wraps
False
>>> flags = CastFlags(splits=False)
>>> CastFlags.resolve(flags) is flags
True

IV Transform#

class my.typing.cast.Transform(data: T0, target: AnyType[T1], source: AnyType[T0] | None = None, flags: CastFlags | None = None)#

An ephemeral class that represents a single attempted coercion.

Deliberately a plain class – not a pydantic model. A Transform is constructed on every single cast() call and never crosses a serialization boundary, so it wants none of pydantic’s per-construction validation. It previously fought that machinery anyway – passing plain MyType instances to dodge the deep re-validation that recurses forever through the self-referential POS sentinel. Construction is now just a few attribute writes, which is all this object ever needed.

property Transform.ty: Typist#

The shared Typist facade for match/check/cast (mirrors _TypingBase.ty).

property Transform.map_items: list[tuple[Any, Any]] | None#

val pairs, or None for any non-mapping type.

Type:

An already-typecast list of key

Transform.to_union() → T1 | None#

Cast data to a union (split) target.

Returns the data unchanged if it already satisfies any member of the union (a NOOP, e.g. a str passed to int | str); otherwise coerces it to a best-fit member: members are ranked by Typist.sort_options fitness, with score ties broken by declaration order (earlier members preferred – for constraint/bound unions from TypeVars the first-listed member is the canonical choice, and hailmary coercions like bool(...) truthiness must not beat an exact numeric parse just because bool was listed last). Casting to the NoneType member naturally yields None and is skipped over.

Returns:

The (possibly coerced) data, or None if no member could be satisfied.

Examples

Satisfied targets are a NOOP; otherwise members are tried best-fit-first:

>>> from my import ty
>>> ty.cast('5', int | str)
'5'
>>> ty.cast('5', float | int)
5
Transform.to_literal() → T1 | None#

Cast data to a literal type or literal tuple.

Fails silently if the passed data is a sequence that differs in length from the target type’s expectations.

Returns:

Cast data if it matches the literal, None otherwise.

classmethod Transform.flex_deserialize(text: str) → int | float | complex | bool | None#

Parse text as whichever scalar type’s pattern it matches, or None if none match.

Examples

Give a string its most literal scalar reading:

>>> from my.typing.cast import Transform
>>> Transform.flex_deserialize(' 42 ')
42
>>> Transform.flex_deserialize('hi') is None
True
classmethod Transform.concretize(target: MyType, data: object) → MyType#

Convert abstract container types to concrete ones based on data.

Parameters:
  • target – The target MyType.

  • data – The source data to inform concretization.

Returns:

The target as-is when already concrete, otherwise a copy with a concrete origin.

Transform.to(t1: AnyType) → B | None#

Shorthand for casting our current data to an interim type.

Transform.proxy(data: Any) → T1 | None#

Shorthand for casting new data to our target type.

Transform.by(*args: AnyType) → T1 | None#

Shorthand casting to the target type through one or more intermediary types.

Each intermediate is an actual cast target in turn (the source of each hop is inferred from the running value), so by(list) truly materializes a list before casting onward – which also stops hailmary fallbacks from re-selecting themselves.

Transform.__call__(new_data: T0 | None = None) → T1 | None#

Main entrypoint for casting a value to a new type.