trycast
Trycast helps parses JSON-like values whose shape is defined by
typed dictionaries
(TypedDicts) and other standard Python type hints.
You can use either the trycast()
or isassignable()
functions below
for parsing:
trycast()
Here is an example of parsing a Point2D
object defined as a TypedDict
using trycast()
:
from bottle import HTTPResponse, request, route
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
if (point := trycast(Point2D, request_json)) is None:
return HTTPResponse(status=400)
draw_point(point)
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
radius: float
class Rect(TypedDict):
type: Literal['rect']
x: float
y: float
width: float
height: float
Shape = Circle | Rect
@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
request_json = request.json
if (shape := trycast(Shape, request_json)) is None:
return HTTPResponse(status=400)
draw_shape(shape)
return HTTPResponse(status=200)
Important: Current limitations in the mypy typechecker require that you
add an extra cast(Optional[Shape], ...)
around the call to trycast
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.
isassignable()
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
@route('/draw_shape')
def draw_shape_endpoint() -> HTTPResponse:
request_json = request.json
if not isassignable(request_json, Shape):
return HTTPResponse(status=400)
draw_shape(request_json)
return HTTPResponse(status=200)
Important: Current limitations in the mypy typechecker prevent the
automatic narrowing of the type of request_json
in the above example to
Shape
, so you must add an additional cast()
to narrow the type manually:
if not isassignable(request_json, Shape):
...
shape = cast(Shape, request_json)
draw_shape(shape)
These limitations are in the process of being resolved by
introducing TypeForm support to mypy.
A better 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.
Motivation & Alternatives
Why use trycast?
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.
Why use TypedDict?
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.
Installation
python -m pip install trycast
Recommendations while using trycast
- So that
trycast()
can recognize TypedDicts with mixed required and
not-required keys correctly:
- Use Python 3.9+ if possible.
- Prefer using
typing.TypedDict
, unless you must use Python 3.8.
In Python 3.8 prefer typing_extensions.TypedDict
instead. - Avoid using
mypy_extensions.TypedDict
in general.
Presentations & Videos
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:
Contributing
Pull requests are welcome! The Python Community Code of Conduct does apply.
You can checkout the code locally using:
git clone git@github.com: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
License
MIT
Feature Reference
Typing Features Supported
- Scalars
- bool
- int
- float
- None, type(None)
- Strings
- Raw Collections
- list, List
- tuple, Tuple
- Sequence, MutableSequence
- dict, Dict
- Mapping, MutableMapping
- Generic Collections
(including PEP 585)
- list[T], List[T]
- tuple[T, ...], Tuple[T, ...]
- Sequence[T], MutableSequence[T]
- dict[K, V], Dict[K, V]
- Mapping[K, V], MutableMapping[K, V]
- TypedDict
- typing.TypedDict, typing_extensions.TypedDict
(PEP 589)
- mypy_extensions.TypedDict (when strict=False)
- –––
- Required, NotRequired
(PEP 655)
- Tuples (Heterogeneous)
- tuple[T1], tuple[T1, T2], tuple[T1, T2, T3], etc
- Tuple[T1], Tuple[T1, T2], Tuple[T1, T2, T3], etc
- Unions
- Union[X, Y]
- Optional[T]
- X | Y
(PEP 604)
- Literals
- Callables
- Callable
- Callable[P, R] (where P=[Any]*N and R=Any)
- NewTypes (when strict=False)
- Special Types
Type Checkers Supported
Trycast does type check successfully with the following type checkers:
API Reference
trycast API
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:
- List[T]
- Dict[K, V]
- Optional[T]
- Union[T1, T2, ...]
- Literal[...]
- T extends TypedDict
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:
- strict --
- If strict=False then trycast will additionally accept
mypy_extensions.TypedDict instances and Python 3.8 typing.TypedDict
instances for the
tp
parameter. Normally these kinds of types are
rejected by trycast 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. - If strict=False then trycast will treat
NewType("Foo", T)
the same as T
. Normally NewTypes are rejected by trycast with a
TypeNotSupportedError because values of NewTypes at runtime
are indistinguishable from their wrapped supertype.
- eval --
If eval=False then trycast will not attempt to resolve string
type references, which requires the use of the eval() function.
Otherwise string type references will be accepted.
Raises:
- TypeNotSupportedError --
- If strict=True and either mypy_extensions.TypedDict or a
Python 3.8 typing.TypedDict is found within the
tp
argument. - If strict=True and a NewType is found within the
tp
argument. - If a TypeVar is found within the
tp
argument. - If an unrecognized Generic type is found within the
tp
argument.
- UnresolvedForwardRefError --
If
tp
is a type form which contains a ForwardRef. - UnresolvableTypeError --
If
tp
is a string that could not be resolved to a type.
Footnotes:
isassignable API
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(tp, value)
except that it supports many more types than isinstance
, including:
- List[T]
- Dict[K, V]
- Optional[T]
- Union[T1, T2, ...]
- Literal[...]
- T extends TypedDict
Note that unlike isinstance(), this method does NOT consider bool values
to be valid int values, as consistent with Python typecheckers:
isassignable(False, int) -> False
isinstance(False, int) -> True
Note that unlike isinstance(), this method considers every int value to
also be a valid float value, as consistent with Python typecheckers:
isassignable(1, float) -> True
isinstance(1, float) -> False
Parameters:
- eval --
If eval=False then isassignable will not attempt to resolve string
type references, which requires the use of the eval() function.
Otherwise string type references will be accepted.
Raises:
- TypeNotSupportedError --
- If strict=True and either mypy_extensions.TypedDict or a
Python 3.8 typing.TypedDict is found within the
tp
argument. - If strict=True and a NewType is found within the
tp
argument. - If a TypeVar is found within the
tp
argument. - If an unrecognized Generic type is found within the
tp
argument.
- UnresolvedForwardRefError --
If
tp
is a type form which contains a ForwardRef. - UnresolvableTypeError --
If
tp
is a string that could not be resolved to a type.
Changelog
Future
v1.1.0
- Fix
trycast()
to recognize TypedDicts with extra keys. (#19)
- This new behavior helps recognize JSON structures with arbitrary additional keys
and is consistent with how static typecheckers treat additional keys.
- Fix magic wand in logo to look more like a magic wand. (#20)
v1.0.0
- Extend
trycast()
to recognize more kinds of types:
- Extend
trycast()
to recognize set[T]
and Set[T]
values. - Extend
trycast()
to recognize frozenset[T]
and FrozenSet[T]
values. - Extend
trycast()
to recognize Callable
and Callable[P, R]
types when P
and R
only contain Any
. - Extend
trycast()
to recognize NewType
types when strict=False. - Extend
trycast()
to explicitly disallow TypeVar
types. - Extend
trycast()
to explicitly disallow unrecognized Generic
types.
- Fix issues with PEP 484 conformance: (Breaking change)
bool
values are now correctly treated as assignable to int
.bool
, int
, and float
values are now correctly treated as assignable to complex
.
- Add support for Python 3.11.
- Documentation improvements:
- Add installation instructions.
- Improve differentiation from similar libraries.
- Document supported typing features & type checkers.
- Mention that trycast() and isassignable() accept TypeFormString[T]
in addition to TypeForm[T].
- Add developer documentation.
v0.7.3
- Support X|Y syntax for Union types from
PEP 604.
- Documentation improvements:
- Improve introduction.
- Add API reference.
v0.7.2
v0.7.1
- Upgrade development status from Beta to Production/Stable: 🎉
- trycast is thoroughly tested.
- trycast has high code coverage (98%, across Python 3.7-3.10).
- trycast has been in production use for over a year
at at least one company without issues.
- trycast supports all major Python type checkers
(Mypy, Pyright/Pylance, Pyre, Pytype).
- trycast's initial API is finalized.
- Fix
coverage
to be a dev-dependency rather than a regular dependency.
v0.7.0
- Finalize the initial API:
- Alter
trycast()
to use strict=True
by default rather than
strict=False
. (Breaking change) - Define trycast's
__all__
to export only the
trycast
and isassignable
functions.
- Add support for additional type checkers, in addition to Mypy:
- Add support for the Pyright type checker and
Pylance language server extension (for Visual Studio Code).
- Add support for the Pyre type checker.
- Add support for the Pytype type checker.
- Extend
trycast()
to recognize special Any
and NoReturn
values. - Fix
trycast()
to provide better diagnostic error when given a tuple
of types as its tp
argument. Was broken in v0.6.0.
v0.6.1
- Fix
trycast(..., eval=False)
to not use typing.get_type_hints()
,
which internally calls eval()
. - Fix
trycast()
and isassignable()
to avoid swallowing KeyboardInterrupt
and other non-Exception BaseExceptions.
v0.6.0
- Extend
trycast()
to recognize a stringified type argument. - Extend
trycast()
to report a better error message when given
a type argument with an unresolved forward reference (ForwardRef
). - Fix
strict
argument to trycast
to be passed to inner calls of trycast
correctly.
- This also fixes
isassignable()
's use of strict matching to be correct.
- Alter
trycast()
to interpret a type argument of None
or "None"
as an
alias for type(None)
, as consistent with
PEP 484. - Alter
TypeNotSupportedError
to extend TypeError
rather than ValueError
.
(Breaking change)
- This is consistent with
trycast
's and isinstance
's behavior of using
a TypeError
rather than a ValueError
when there is a problem with its
tp
argument.
- Drop support for Python 3.6. (Breaking change)
- Python 3.6 is end-of-life.
v0.5.0
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.
- Extend
trycast()
to recognize Required[]
and NotRequired[]
from
PEP 655, as imported from typing_extensions
. - Extend
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).
- For now
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.
- Rename primary development branch from
master
to main
.
v0.4.0
- Upgrade development status from Alpha to Beta:
- trycast is thoroughly tested.
- trycast has high code coverage (92% on Python 3.9).
- trycast has been in production use for over a year
at at least one company without issues.
- Add support for Python 3.10.
- Setup continuous integration with GitHub Actions, against Python 3.6 - 3.10.
- Migrate to the Black code style.
- Introduce Black and isort code formatters.
- Introduce flake8 linter.
- Introduce coverage.py code coverage reports.
v0.3.0
- TypedDict improvements & fixes:
- Fix
trycast()
to recognize custom Mapping subclasses as TypedDicts.
- Extend
trycast()
to recognize more JSON-like values:
- Extend
trycast()
to recognize Mapping
and MutableMapping
values. - Extend
trycast()
to recognize tuple[T, ...]
and Tuple[T, ...]
values. - Extend
trycast()
to recognize Sequence
and MutableSequence
values.
- Extend
trycast()
to recognize tuple[T1, T2, etc]
and Tuple[T1, T2, etc]
values. - Documentation improvements:
- Improve introduction.
- Outline motivation to use trycast and note alternatives.
v0.2.0
- TypedDict improvements & fixes:
- Fix
trycast()
to recognize TypedDicts from mypy_extensions
. - Extend
trycast()
to recognize TypedDicts that contain forward-references
to other types.
- Unfortunately there appears to be no easy way to support arbitrary kinds
of types that contain forward-references.
- In particular {Union, Optional} types and collection types (List, Dict)
with forward-references remain unsupported by
trycast()
.
- Recognize TypedDicts that have mixed required and not-required keys correctly.
- Exception: Does not work for mypy_extensions.TypedDict or
Python 3.8's typing.TypedDict due to insufficient runtime
type annotation information.
- Fix recognition of a total=False TypedDict so that extra keys are disallowed.
- Alter
typing_extensions
to be an optional dependency of trycast
.
v0.1.0
- Add support for Python 3.6, 3.7, and 3.9, in addition to 3.8.
v0.0.2
- Fix README to appear on PyPI.
- Add other package metadata, such as the supported Python versions.
v0.0.1a
- Initial release.
- Supports typechecking all types found in JSON.