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 asdict[str, int](which becomes the tuple(dict, str, int)) andlist[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.AnnotatedandLiteral).That said, not all possible type annotations are covered – see the
Typist.SPECIAL_TYPESattribute 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’sissubclass(), Typist handles subtypes of generics recursively; for example,dict[str, int]matchesMapping[str, int]andCollection[Sequence, int], but notMapping[str, str]orCollection[int].Matching results are cached using a
NestedCachefor 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, whereastuple[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 publishescheck()for individual object/type pairs, andall_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()andflexcast(), 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, andboolSeries:
list,tuple,set,deque, etc.Maps:
dict,Counter,Predicate, etc.Pydantic models: any subclass of
pyd.BaseModelTimes:
datetime,date,time, andtimedeltaEnums: standard Python
EnumtypesAnd, 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()anddistill()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()andfrom_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 (seedocs/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
flextier – every loose coercion enabled – in keeping with the package’s permissive “vibe typing” stance. Tighten it per-process by assigning a stricterpreset()bundle onto the returned instance.Examples
The packaged aliases
tyandtypistare this same instance:>>> from my import ty, Typist >>> Typist.inst() is ty True
II Primary Methods#
III Parsing#
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:
A list with the rest.
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_jsonandto_tomlwork 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
CastFlagssnapshot (or preset-level name); resolved once here so every option attempted below shares the same flag set. SeeTypeCast.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
_strictis 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(), theninvoke()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