Typist: Powerful, Varied Typing Utilities#

class my.typing.Typist.Typist(*, data: T0 = None, root: TypeArg = None, firsts: bool = True, atomics: bool = True, splits: bool = True, wraps: bool = True)#

Semi-singleton interface for building systems that are resilient to slight inconsistencies.

Specifically, this class provides runtime type introspection, parsing, and coercion capabilities that extend Python’s static type hints into the runtime domain.

This class was originally built as an extension of a Minksian Frame data structure, which needed to flexibly translate between untyped LLM outputs and strongly-typed in-memory data structures. It contains a large variety of functionality-for and examples-of working with types at runtime, but I suspect that this sort of “Vibe Typing” usecase will remain its shining capability.

Tip

Typist is written as an instanced class only for situations where configuration differs across a single project. If that’s not you, just use the global instance typist!

I. Parsing

The features of Typist that most diverge from what’s capable with the standard library rely on the parse() method, which decomposes a given type so that other methods can intelligently handle each part in turn. By far the most likely usecase is for containers such as dict[str, int] (which becomes the tuple (dict, str, int)) and list[int] (which becomes (list, int, None)), but it’s useful for other generics, unions (e.g. string | int), and special non-type forms (e.g. Annotated and Literal).

That said, not all possible type annotations are covered – see the Typist.SPECIAL_TYPES attribute for a best-effort list of unhandled annotations.

II. Comparison

Type Comparison (“matching”)

Type matching (mostly via match()) determines whether a value or type is a valid subset of another type. As opposed to the stdlib’s issubclass(), Typist handles subtypes of generics recursively; for example, dict[str, int] matches Mapping[str, int] and Collection[Sequence, int], but not Mapping[str, str] or Collection[int].

Matching results are cached using a NestedCache for performance.

A small number of non-atomic yet common types are handled with custom logic: tuple[int, str, float] only matches another tuple with the same length and member types, whereas tuple[int, ...] matches any-length tuples of ints.

Object Comparison (“checking”)

Runtime data can be compared to other data using match_instances(), but obviously the primary usecase is to bring type-checking functionality into runtime in an ergonomic, idiomatic way. For this, Typist publishes check() for individual object/type pairs, and all_are() for asserting the types of the contents of containers.

All of these methods use the TypeGuard protocol to enable type-narrowing in conditional statements, complementing static type-checkers like mypy or ty.

III. Coercion

The core functionality is intelligent type coercion via cast() and flexcast(), which both try their absolute hardest to find a reasonable mapping between any two types. Obviously this is definitionally impossible to do perfectly for all possible types, but it has been tested extensively on the types that make up the vast majority of usecases (AI or otherwise):

  • Atomic types: str, int, float, and bool

  • Series: list, tuple, set, deque, etc.

  • Maps: dict, Counter, Predicate, etc.

  • Pydantic models: any subclass of pyd.BaseModel

  • Times: datetime, date, time, and timedelta

  • Enums: standard Python Enum types

  • And, most importantly: nested combinations of the above!

Some of the decisions made within this class are arbitrary, but if the system is used consistently for both reading and writing, the implied instability/inconsistency can be minimized.

IV. Transformation

Typist provides more than just type coercion, which is ideally a minimally-semantic process. Namely, the serialize(), assemble() and distill() methods are built to flatten, combine together, and split apart complex nested data structures composed of sequences, mappings, and even Pydantic objects.

V. Persistence

For reading and writing typed data to and from disk, Typist provides to_file() and from_file(). In just one short statement, users can interface with three file formats–YAML, JSON, and Pickle–using the very highly performant srsly library.

VI. Invocation

Finally, invoke() provides safe function calling: it inspects the target’s signature to bind the given arguments (unpacking a lone mapping or sequence argument when that helps), type-checks the binding against the annotations, and only then calls the function – returning None instead of raising when no safe call can be made. This enables seamless integration of typed functions into dynamic workflows.

The three chambers Typist mixes in are documented on their own pages: casting on cast (ty.cast, ty.flexcast, …), value checking on check (ty.check, ty.is_atom, …), and type matching on match (ty.match, ty.is_atom_type, …).

I Initial Methods#

classmethod Typist.preset(level: CastPreset = 'basic') → dict[str, bool]#

Get a preset bundle of cast-flag values for a strictness tier.

Delegates to CastFlags.preset, the canonical per-call value-object equivalent of this bundle (see docs/DESIGN-cast-flags.md).

Parameters:

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

Returns:

A mapping of cast-flag names to booleans, suitable for Typist(**preset(...)) or for assigning onto an existing instance.

Examples

Build a stricter, private Typist alongside the permissive global one:

>>> from my import Typist
>>> strict = Typist(**Typist.preset('strict'))
>>> strict.wraps
False
classmethod Typist.inst() → Typist#

Get the global instance of Typist.

The global singleton defaults to the flex tier – every loose coercion enabled – in keeping with the package’s permissive “vibe typing” stance. Tighten it per-process by assigning a stricter preset() bundle onto the returned instance.

Examples

The packaged aliases ty and typist are this same instance:

>>> from my import ty, Typist
>>> Typist.inst() is ty
True

II Primary Methods#

static Typist.specify(tvar: type[F]) → type[F]#

Convert a type variable to its generic form if it’s a generic alias.

Examples

Strip the parameters off a generic alias:

>>> from my import ty
>>> ty.specify(list[int])
<class 'list'>

III Parsing#

static Typist.parse(tvar: Any) → MyType#

Parse a type annotation into a MyType instance. See MyType.parse() for details.

Examples

Decompose a parameterized generic into its parts:

>>> from my import ty
>>> ty.parse(dict[str, int]).summarize()
(<class 'dict'>, <class 'str'>, <class 'int'>)

IV Comparison#

classmethod Typist.all_are(iterable: Iterable, tvar: type[T]) → TypeGuard[Iterable]#

Check if all values in an iterable match a type variable.

Examples

Assert the contents of a container, wholly or partially:

>>> from my import ty
>>> ty.all_are([1, 2, 3], int)
True
>>> ty.any_are([1, 'a'], str)
True
classmethod Typist.any_are(iterable: Iterable, tvar: type[T]) → TypeGuard[Iterable[E | T]]#

Check if any value in an iterable matches a type variable.

Typist.match_instances(t0: object, t1: object, intersect: bool = False) → bool#

Check if two instances have matching types.

Parameters:
  • t0 – First instance.

  • t1 – Second instance.

  • intersect – If True, check for intersection; if False, check for subset.

Returns:

True if the instances’ types match.

Examples

Compare two values’ full (inferred) types:

>>> from my import ty
>>> ty.match_instances([1, 2], [3])
True
>>> ty.match_instances([1], ['x'])
False
classmethod Typist.type_partition(data: Iterable[T0 | T1], tvar: type[T0]) → tuple[list[T0], list[T1]]#

Separate out all the items matching the given type in the given container.

Parameters:
  • data – The iterable to partition.

  • tvar – The type to partition by.

Returns:

  1. A list with the rest.

  2. A list of only items that are (subclasses of) the first type.

Examples

Split mixed data by type:

>>> from my import ty
>>> ty.type_partition([1, 'a', 2, 'b'], int)
(['a', 'b'], [1, 2])
Typist.seek_usage(target: TypeArg, container: type | MyType) → bool#

Check if a type is used anywhere within another type’s structure.

Parameters:
  • target – The type to search for.

  • container – The type to search within.

Returns:

True if target is used within container’s type structure.

Examples

Search nested annotations for a type:

>>> from my import ty
>>> ty.seek_usage(int, dict[str, list[int]])
True
>>> ty.seek_usage(bytes, dict[str, list[int]])
False

V Coercion#

Typist.flex_deserialize(values: Sequence[str] | str) → list[str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum]#

Convert a list of strings to their most appropriate Atomic types.

Examples

Give each string its own best-fit scalar type:

>>> from my import ty
>>> ty.flex_deserialize(['1', '2.5', 'yes', 'word'])
[1, 2.5, True, 'word']
Typist.setattr(obj: object, key: str, value: Any, tvar: TypeArg = None) → bool#

Set an attribute on an object, casting the value to the appropriate type.

Parameters:
  • obj – The object to set the attribute on.

  • key – The attribute name.

  • value – The value to set (will be cast if needed).

  • tvar – Optional explicit type to cast to (inferred from obj if None).

Returns:

True if successful, False if casting failed.

Examples

Set a typed attribute from a stringly value:

>>> import pydantic as pyd
>>> from my import ty
>>> class Point(pyd.BaseModel):
...     x: int = 0
>>> point = Point()
>>> ty.setattr(point, 'x', '9')
True
>>> point.x
9
Typist.cast_file_data(data: str | int | float | bool | list | dict | None, tvar: type[F]) → F#

Cast raw file data (JSON/YAML) to a specific type with smart conversions.

Parameters:
  • data – Raw data from file (str, int, float, bool, list, dict, or None).

  • tvar – The target type to cast to.

Returns:

Cast data of the target type.

Raises:

TypeError – If casting fails.

Examples

Repair the shape mismatches that JSON/YAML files commonly produce:

>>> from my import ty
>>> ty.cast_file_data(5, list)
[5]
>>> ty.cast_file_data({'content': [1]}, list)
[1]

VI Transformation#

Typist.serialize(data: int | float | complex | bool, full: bool = False, cases: dict[CaseKey, CaseVal] | None = None) → int | float | complex | bool#
Typist.serialize(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum, full: bool = False, cases: dict[CaseKey, CaseVal] | None = None) → str
Typist.serialize(data: Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, full: bool = False, cases: dict[CaseKey, CaseVal] | None = None) → dict
Typist.serialize(data: list | tuple | Set | deque | array | range | Iterable | AsyncIterable, full: bool = False, cases: dict[CaseKey, CaseVal] | None = None) → list
Typist.serialize(data: object, full: bool = False, cases: dict[CaseKey, CaseVal] | None = None) → object

Recursively simplify the given object into serialization-ready, standardized types.

This method is undeniably an opinionated way of preparing data for export, but it should be easy enough to change or add some of these decisions using cases.

The following rules are applied:

  • All enums and times are converted to strings.

  • All other atomics are left as-is

  • All series are cast to lists.

  • All maps are cast to dicts.

  • All models are converted to dicts using their model_dump() method.

Parameters:
  • data – The source data to serialize.

  • full – Whether to include unset and default-valued Pydantic fields. The default emits only explicitly populated, non-default fields.

  • cases – Optional special-case handlers, keyed by type or predicate, that trigger at all depths.

Returns:

The simplified data, recursively composed of dicts, lists, and atoms.

Examples

Simplify nested data into serialization-ready shape:

>>> from datetime import date
>>> from my import ty
>>> ty.serialize({'when': date(2026, 7, 21), 'tags': ('a', 'b')})
{'when': '2026-07-21', 'tags': ['a', 'b']}

Override any depth with a special-case handler:

>>> ty.serialize({'pi': 3.7}, cases={float: round})
{'pi': 4}
Typist.assemble(base: T, *args: T, copy: bool = True, sort: bool = True, dups: bool = False) → T#

Combine dictionaries, recursively merging nested structures wherever possible.

Parameters:
  • base – The base dictionary to merge into.

  • *args – Additional dictionaries to merge in order.

  • copy – If True, make a deep copy of the base before merging.

  • sort – If True, sort lists after merging.

  • dups – If False, remove duplicates from lists after merging.

Returns:

Merged dictionary.

Examples

Merge nested structures instead of overwriting them:

>>> from my import ty
>>> ty.assemble({'a': [1], 'b': {'x': 1}}, {'a': [2], 'b': {'y': 2}})
{'a': [1, 2], 'b': {'x': 1, 'y': 2}}
Typist.distill(models: list[dict], exclude: set[str] | None = None) → dict#

Recursively extract common fields from multiple models; the inverse of assemble().

Note that the input dictionaries are mutated in place: every extracted field is removed from them, leaving only their unique remainders behind.

Parameters:
  • models – List of dictionaries to distill.

  • exclude – Set of field names to exclude from distillation (used during recursion).

Returns:

Distilled dictionary of shared fields.

Examples

Pull the shared fields out, leaving each model’s remainder in place:

>>> from my import ty
>>> models = [{'lang': 'py', 'v': 1}, {'lang': 'py', 'v': 2}]
>>> ty.distill(models)
{'lang': 'py'}
>>> models
[{'v': 1}, {'v': 2}]

VII Persistence#

Typist.from_file(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None) → dict#
Typist.from_file(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None, tvar: type[F], cast: bool = True) → F

Load & cast data from a local JSON/YAML/TOML/Pickle file. See ut.from_file().

Typist.from_json(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None) → dict#
Typist.from_json(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None, tvar: type[F], cast: bool = True) → F

Load & cast data from a JSON file or string. See ut.from_json().

Typist.from_yaml(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None) → dict#
Typist.from_yaml(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None, tvar: type[F], cast: bool = True) → F

Load & cast data from a YAML file or string. See ut.from_yaml().

Examples

Parse a string (or file path) and coerce the result in one step:

>>> from my import ty
>>> ty.from_yaml('a: [1, 2]')
{'a': [1, 2]}
>>> ty.from_yaml('[1, 2]', list[int])
[1, 2]
Typist.from_toml(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None) → dict#
Typist.from_toml(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None, tvar: type[F], cast: bool = True) → F

Load & cast data from a TOML file or string. See ut.from_toml().

Typist.from_pickle(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None) → dict#
Typist.from_pickle(file: str | bytes | Annotated[Path, PathType(path_type=file)] | None, tvar: type[F], cast: bool = True) → F

Load & cast data from a Pickle file or bytes. See ut.from_pickle().

Typist.to_file(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum | list | tuple | Set | deque | array | range | Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, file: str | Annotated[Path, PathType(path_type=file)]) → None#

Save data to a local JSON/YAML/TOML/Pickle file. See ut.to_file().

Typist.to_yaml(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum | list | tuple | Set | deque | array | range | Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, wrap: bool = False, **kwargs) → str#

Serialize data to a YAML string. See ut.to_yaml().

Examples

Serialize a structure straight to text (to_json and to_toml work alike):

>>> from my import ty
>>> ty.to_yaml({'a': [1, 2]})
'a:\n    - 1\n    - 2\n'
Typist.to_json(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum | list | tuple | Set | deque | array | range | Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, wrap: bool = False, **kwargs) → str#

Serialize data to a JSON string. See ut.to_json().

Typist.to_toml(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum | list | tuple | Set | deque | array | range | Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, wrap: bool = False, **kwargs) → str#

Serialize data to a TOML string. See ut.to_toml().

Typist.to_pickle(data: str | bytes | bytearray | memoryview | IO | int | float | complex | bool | date | time | datetime | timedelta | Enum | list | tuple | Set | deque | array | range | Mapping[Hashable, Any] | Iterable[tuple[Hashable, Any]] | ItemsView | BaseModel | object, **kwargs) → bytes#

Serialize data to Pickle bytes. See ut.to_pickle().

VIII Invocation#

Typist.get_str_method(obj: object, *extra_methods: str) → Callable[[...], str] | None#

Get a string conversion method from an object.

Parameters:
  • obj – The object to search for string methods.

  • *extra_methods – Additional method names to check before standard ones.

Returns:

First found string conversion method, or None if none found.

Typist.get_method(obj: object, *methods: str) → Callable | None#

Get the first available method from an object by name.

Parameters:
  • obj – The object to search for methods.

  • *methods – Method names to search for in order.

Returns:

First found callable method, or None if none found.

Examples

Take the first match, in argument order:

>>> from my import ty
>>> ty.get_method([], 'push', 'append').__name__
'append'
>>> ty.get_method([], 'push') is None
True
classmethod Typist.invocable(sig: Callable | Signature, *args: object, **kwargs: object) → BoundArguments | None#

Check if a function can be called with the given arguments.

This method validates that the provided arguments can be bound to the function’s signature and optionally performs type checking on the arguments.

Parameters:
  • sig – The function or signature to inspect.

  • *args – Positional arguments to validate.

  • **kwargs – Keyword arguments to validate.

String annotations (PEP 563, i.e. modules using from __future__ import annotations) are resolved before checking; a name that cannot be resolved at runtime (e.g. a TYPE_CHECKING-only import) leaves the raw string in place, which then fails validation.

Args (annotations) are checked, never cast.

Returns:

The bound arguments to call the function with if binding and type-checking succeed, or None if the function cannot be called with the given arguments.

Examples

Bind and type-check without calling:

>>> from my import ty
>>> def area(width: int, height: int) -> int:
...     return width * height
>>> ty.invocable(area, 4, 5)
<BoundArguments (width=4, height=5)>
>>> ty.invocable(area, 'x', 5) is None
True
Typist.upper_cast(data: object, tvar: type[V], flags: CastFlags | CastPreset | None = None) → V | None#
Typist.upper_cast(data: object, tvar: Any, flags: CastFlags | CastPreset | None = None) → Any | None

Attempt to cast/coerce the data to the given type, returning None if unsuccessful.

Compared to plain cast, this facade also concretizes abstract targets and tries the best-fitting members of a union target first.

Parameters:
  • data – The source data to cast.

  • tvar – The target type to cast to.

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

Returns:

The cast data on success, None otherwise.

Examples

Pick the best-fitting member of a union target:

>>> from my import ty
>>> ty.upper_cast('42', int | list[int])
42
classmethod Typist.invoke(func: Callable[..., V], *args, _strict: True, **kwargs) → V#
classmethod Typist.invoke(func: Callable[..., V], *args, **kwargs) → V | None

Attempt to call a function with the given arguments.

This method first validates the arguments using invocable() – binding them to the signature (unpacking a lone mapping or sequence argument when that helps) and type-checking the binding – then calls the function only if validation succeeds. The arguments are checked, never cast.

Parameters:
  • func – The function to call.

  • *args – Positional arguments to pass to the function.

  • _strict – If set, this method will raise a ValueError rather than returning None.

  • **kwargs – Keyword arguments to pass to the function.

Returns:

The function’s return value, or None if the call could not be made (or itself failed).

Raises:

ValueError – If _strict is set and the function doesn’t exist or couldn’t be called.

Examples

Call safely, unpacking a lone sequence across the parameters:

>>> from my import ty
>>> def area(width: int, height: int) -> int:
...     return width * height
>>> ty.invoke(area, [4, 5])
20
>>> ty.invoke(area, 'x', 5) is None
True
Typist.try_method(obj: object, methods: str | Iterable[str], *args, _tvar: type[T], _strict: True, **kwargs) → T#
Typist.try_method(obj: object, methods: str | Iterable[str], *args, _tvar: None = None, _strict: True, **kwargs) → object
Typist.try_method(obj: object, methods: str | Iterable[str], *args, _tvar: type[T], **kwargs) → T | None
Typist.try_method(obj: object, methods: str | Iterable[str], *args, **kwargs) → object | None

A thin wrapper that calls get_method(), then invoke() if successful.

Parameters:
  • obj – The object to search for methods.

  • methods – One or more method names to try, in order.

  • *args – Positional arguments to pass to the found method.

  • _tvar – An optional type to cast the result to (results already of that type pass through untouched).

  • _strict – If set, raise a ValueError rather than returning None on any failure.

  • **kwargs – Keyword arguments to pass to the found method.

Returns:

The (possibly cast) result, or None if no method was found or the call failed.

Examples

Probe for a method and call it in one step:

>>> from my import ty
>>> ty.try_method('hi there', 'split')
['hi', 'there']
>>> ty.try_method(42, 'split') is None
True