trycast parses JSON-like values whose shape is defined by TypedDicts and other standard Python type hints.
Trycast helps parses JSON-like values whose shape is defined by typed dictionaries (TypedDicts) and other standard Python type hints.
You can use the trycast()
, checkcast()
, or isassignable()
functions below
for parsing:
Here is an example of parsing a Point2D
object defined as a TypedDict
using trycast()
:
from bottle import HTTPResponse, request, route # Bottle is a web framework
from trycast import trycast
from typing import TypedDict
class Point2D(TypedDict):
x: float
y: float
name: str
@route('/draw_point')
def draw_point_endpoint() -> HTTPResponse:
request_json = request.json # type: object
if (point := trycast(Point2D, request_json)) is None:
return HTTPResponse(status=400) # Bad Request
draw_point(point) # type is narrowed to Point2D
return HTTPResponse(status=200)
def draw_point(point: Point2D) -> None:
...
In this example the trycast
function is asked to parse a request_json
into a Point2D
object, returning the original object (with its type narrowed
appropriately) if parsing was successful.
More complex types can be parsed as well, such as the Shape
in the following
example, which is a tagged union that can be either a Circle
or Rect
value:
from bottle import HTTPResponse, request, route
from trycast import trycast
from typing import Literal, TypedDict
class Point2D(TypedDict):
x: float
y: float
class Circle(TypedDict):
type: Literal['circle']
center: Point2D # a nested TypedDict!
radius: float
class Rect(TypedDict):
type: Literal['rect']
x: float
y: float
width: float
height: float
Shape = Circle | Rect # a Tagged Union!
@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
request_json = request.json # type: object
if (shape := trycast(Shape, request_json)) is None:
return HTTPResponse(status=400) # Bad Request
draw_shape(shape) # type is narrowed to Shape
return HTTPResponse(status=200) # OK
Important: Current limitations in the mypy typechecker require that you add an extra
cast(Optional[Shape], ...)
around the call totrycast
in the example so that it is accepted by the typechecker without complaining:shape = cast(Optional[Shape], trycast(Shape, request_json)) if shape is None: ...
These limitations are in the process of being resolved by introducing TypeForm support to mypy.
checkcast()
is similar to trycast()
but instead of returning None
when parsing fails it raises an exception explaining why and where the
parsing failed.
Here is an example of parsing a Circle
object using checkcast()
:
>>> from typing import Literal, TypedDict
>>> from trycast import checkcast
>>>
>>> class Point2D(TypedDict):
... x: float
... y: float
...
>>> class Circle(TypedDict):
... type: Literal['circle']
... center: Point2D # a nested TypedDict!
... radius: float
...
>>> checkcast(Circle, {"type": "circle", "center": {"x": 1}, "radius": 10})
Traceback (most recent call last):
...
trycast.ValidationError: Expected Circle but found {'type': 'circle', 'center': {'x': 1}, 'radius': 10}
At key 'center': Expected Point2D but found {'x': 1}
Required key 'y' is missing
>>>
ValidationError
only spends time generating a message if you try to print it
or stringify it, so can be cheaply caught if you only want to use it for
control flow purposes.
Here is an example of parsing a Shape
object defined as a union of
TypedDict
s using isassignable()
:
class Circle(TypedDict):
type: Literal['circle']
...
class Rect(TypedDict):
type: Literal['rect']
...
Shape = Circle | Rect # a Tagged Union!
@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
request_json = request.json # type: object
if not isassignable(request_json, Shape):
return HTTPResponse(status=400) # Bad Request
draw_shape(request_json) # type is narrowed to Shape
return HTTPResponse(status=200) # OK
Important: Current limitations in the mypy typechecker prevent the automatic narrowing of the type of
request_json
in the above example toShape
, so you must add an additionalcast()
to narrow the type manually:if not isassignable(request_json, Shape): ... shape = cast(Shape, request_json) # type is manually narrowed to Shape draw_shape(shape)
These limitations are in the process of being resolved by introducing TypeForm support to mypy.
isinstance()
isassignable(value, T)
is similar to Python's builtin isinstance()
but
additionally supports checking against arbitrary type annotation objects
including TypedDicts, Unions, Literals, and many others.
Formally, isassignable(value, T)
checks whether value
is consistent with a
variable of type T
(using PEP 484 static
typechecking rules), but at runtime.
The trycast module is primarily designed for recognizing JSON-like structures that can be described by Python's typing system. Secondarily, it can be used for recognizing arbitrary structures that can be described by Python's typing system.
Please see Philosophy for more information about how trycast differs from similar libraries like pydantic.
Typed dictionaries are the natural form that JSON data comes in over the wire. They can be trivially serialized and deserialized without any additional logic. For applications that use a lot of JSON data - such as web applications - using typed dictionaries is very convenient for representing data structures.
If you just need a lightweight class structure that doesn't need excellent support for JSON-serialization you might consider other alternatives for representing data structures in Python such as dataclasses (recommended), named tuples, attrs, or plain classes.
python -m pip install trycast
trycast()
can recognize TypedDicts with mixed required and
not-required keys correctly:
typing.TypedDict
, unless you must use Python 3.8.
In Python 3.8 prefer typing_extensions.TypedDict
instead.mypy_extensions.TypedDict
in general.A presentation about using trycast to parse JSON was given at the 2021 PyCon US Typing Summit:
A presentation describing tools that use Python type annotations at runtime, including trycast, was given at the 2022 PyCon US Typing Summit:
Pull requests are welcome! The Python Community Code of Conduct does apply.
You can checkout the code locally using:
git clone [email protected]:davidfstr/trycast.git
cd trycast
Create your local virtual environment to develop in using Poetry:
poetry shell
poetry install
You can run the existing automated tests in the current version of Python with:
make test
You can also run the tests against all supported Python versions with:
make testall
See additional development commands by running:
make help
Trycast does type check successfully with the following type checkers:
def trycast(
tp: TypeForm[T]† | TypeFormString[T]‡,
value: object,
/, failure: F = None,
*, strict: bool = True,
eval: bool = True
) -> T | F: ...
If value
is in the shape of tp
(as accepted by a Python typechecker
conforming to PEP 484 "Type Hints") then returns it, otherwise returns
failure
(which is None by default).
This method logically performs an operation similar to:
return value if isinstance(tp, value) else failure
except that it supports many more types than isinstance
, including:
Similar to isinstance(), this method considers every bool value to also be a valid int value, as consistent with Python typecheckers:
trycast(int, True) -> True
isinstance(True, int) -> True
Note that unlike isinstance(), this method considers every int value to also be a valid float or complex value, as consistent with Python typecheckers:
trycast(float, 1) -> 1
trycast(complex, 1) -> 1
isinstance(1, float) -> False
isinstance(1, complex) -> False
Note that unlike isinstance(), this method considers every float value to also be a valid complex value, as consistent with Python typecheckers:
trycast(complex, 1.0) -> 1
isinstance(1.0, complex) -> False
Parameters:
tp
parameter. Normally these kinds of types are
rejected with a TypeNotSupportedError because these
types do not preserve enough information at runtime to reliably
determine which keys are required and which are potentially-missing.NewType("Foo", T)
will be treated
the same as T
. Normally NewTypes are rejected with a
TypeNotSupportedError because values of NewTypes at runtime
are indistinguishable from their wrapped supertype.Raises:
tp
argument.tp
argument.tp
argument.tp
argument.tp
is a type form which contains a ForwardRef.tp
is a string that could not be resolved to a type.Footnotes:
† TypeForm[T] is a type annotation object. For example: list[str]
‡ TypeFormString[T] is a stringified type annotation object. For example: "list[str]"
def checkcast(
tp: TypeForm[T]† | TypeFormString[T]‡,
value: object,
/, *, strict: bool = True,
eval: bool = True
) -> T: ...
If value
is in the shape of tp
(as accepted by a Python typechecker
conforming to PEP 484 "Type Hints") then returns it, otherwise
raises ValidationError.
This method logically performs an operation similar to:
if isinstance(tp, value):
return value
else:
raise ValidationError(tp, value)
except that it supports many more types than isinstance
, including:
See trycast.trycast() for information about parameters, raised exceptions, and other details.
Raises:
value
is not in the shape of tp
.def isassignable(
value: object,
tp: TypeForm[T]† | TypeFormString[T]‡,
/, *, eval: bool = True
) -> TypeGuard[T]: ...
Returns whether value
is in the shape of tp
(as accepted by a Python typechecker conforming to PEP 484 "Type Hints").
This method logically performs an operation similar to:
return isinstance(value, tp)
except that it supports many more types than isinstance
, including:
See trycast.trycast(..., strict=True) for information about parameters, raised exceptions, and other details.
checkcast()
, an alternative to trycast()
which raises a
ValidationError
upon failure instead of returning None
.
(#16)ReadOnly[]
from PEP 705.
(#25)type
statements from PEP 695.
(#29)Never
values.
(#26)trycast()
and isassignable()
pass the
first 2 arguments in positional fashion and not in a named fashion:
(#18)
(Breaking change)
trycast(T, value)
, isassignable(value, T)
trycast(tp=T, value=value)
, isassignable(value=value, tp=T)
trycast()
to recognize TypedDicts with extra keys. (#19)
trycast()
to recognize more kinds of types:
trycast()
to recognize set[T]
and Set[T]
values.trycast()
to recognize frozenset[T]
and FrozenSet[T]
values.trycast()
to recognize Callable
and Callable[P, R]
types when P
and R
only contain Any
.trycast()
to recognize NewType
types when strict=False.trycast()
to explicitly disallow TypeVar
types.trycast()
to explicitly disallow unrecognized Generic
types.bool
values are now correctly treated as assignable to int
.bool
, int
, and float
values are now correctly treated as assignable to complex
.coverage
to be a dev-dependency rather than a regular dependency.trycast()
to use strict=True
by default rather than
strict=False
. (Breaking change)
__all__
to export only the
trycast
and isassignable
functions.trycast()
to recognize special Any
and NoReturn
values.trycast()
to provide better diagnostic error when given a tuple
of types as its tp
argument. Was broken in v0.6.0.trycast(..., eval=False)
to not use typing.get_type_hints()
,
which internally calls eval()
.trycast()
and isassignable()
to avoid swallowing KeyboardInterrupt
and other non-Exception BaseExceptions.trycast()
to recognize a stringified type argument.trycast()
to report a better error message when given
a type argument with an unresolved forward reference (ForwardRef
).strict
argument to trycast
to be passed to inner calls of trycast
correctly.
isassignable()
's use of strict matching to be correct.trycast()
to interpret a type argument of None
or "None"
as an
alias for type(None)
, as consistent with
PEP 484.TypeNotSupportedError
to extend TypeError
rather than ValueError
.
(Breaking change)
trycast
's and isinstance
's behavior of using
a TypeError
rather than a ValueError
when there is a problem with its
tp
argument.isassignable()
is introduced to the API:
isassignable()
leverages trycast()
to enable type-checking
of values against type objects (i.e. type forms) provided at
runtime, using the same PEP 484 typechecking rules used by
typecheckers such as mypy.trycast()
to recognize Required[]
and NotRequired[]
from
PEP 655, as imported from typing_extensions
.trycast()
to support a strict
parameter that controls whether it
accepts mypy_extensions.TypedDict
or Python 3.8 typing.TypedDict
instances (which lack certain runtime type information necessary for
accurate runtime typechecking).
strict=False
by default for backward compatibility
with earlier versions of trycast()
, but this default is expected
to be altered to strict=True
when/before trycast v1.0.0 is released.master
to main
.trycast()
to recognize custom Mapping subclasses as TypedDicts.trycast()
to recognize more JSON-like values:
trycast()
to recognize Mapping
and MutableMapping
values.trycast()
to recognize tuple[T, ...]
and Tuple[T, ...]
values.trycast()
to recognize Sequence
and MutableSequence
values.trycast()
to recognize tuple[T1, T2, etc]
and Tuple[T1, T2, etc]
values.trycast()
to recognize TypedDicts from mypy_extensions
.trycast()
to recognize TypedDicts that contain forward-references
to other types.
trycast()
.typing_extensions
to be an optional dependency of trycast
.