Package: trycast
trycastTrycast helps parses JSON-like values whose shape is defined bytyped dictionaries(TypedDicts) and other standard Python type hints.You can use either thetrycast()orisassignable()functions below
for parsing:trycast()Here is an example of parsing aPoint2Dobject defined as aTypedDictusingtrycast():frombottleimportHTTPResponse,request,route# Bottle is a web frameworkfromtrycastimporttrycastfromtypingimportTypedDictclassPoint2D(TypedDict):x:floaty:floatname:str@route('/draw_point')defdraw_point_endpoint()->HTTPResponse:request_json=request.json# type: objectif(point:=trycast(Point2D,request_json))isNone:returnHTTPResponse(status=400)# Bad Requestdraw_point(point)# type is narrowed to Point2DreturnHTTPResponse(status=200)defdraw_point(point:Point2D)->None:...In this example thetrycastfunction is asked to parse arequest_jsoninto aPoint2Dobject, returning the original object (with its type narrowed
appropriately) if parsing was successful.More complex types can be parsed as well, such as theShapein the following
example, which is a tagged union that can be either aCircleorRectvalue:frombottleimportHTTPResponse,request,routefromtrycastimporttrycastfromtypingimportLiteral,TypedDictclassPoint2D(TypedDict):x:floaty:floatclassCircle(TypedDict):type:Literal['circle']center:Point2D# a nested TypedDict!radius:floatclassRect(TypedDict):type:Literal['rect']x:floaty:floatwidth:floatheight:floatShape=Circle|Rect# a Tagged Union!@route('/draw_shape')defdraw_shape_endpoint()->HTTPResponse:request_json=request.json# type: objectif(shape:=trycast(Shape,request_json))isNone:returnHTTPResponse(status=400)# Bad Requestdraw_shape(shape)# type is narrowed to ShapereturnHTTPResponse(status=200)# OKImportant:Current limitations in the mypy typechecker require that you
add an extracast(Optional[Shape], ...)around the call totrycastin the example so that it is accepted by the typechecker without complaining:shape=cast(Optional[Shape],trycast(Shape,request_json))ifshapeisNone:...These limitations are in the process of being resolved byintroducing TypeForm support to mypy.isassignable()Here is an example of parsing aShapeobject defined as a union ofTypedDicts usingisassignable():classCircle(TypedDict):type:Literal['circle']...classRect(TypedDict):type:Literal['rect']...Shape=Circle|Rect# a Tagged Union!@route('/draw_shape')defdraw_shape_endpoint()->HTTPResponse:request_json=request.json# type: objectifnotisassignable(request_json,Shape):returnHTTPResponse(status=400)# Bad Requestdraw_shape(request_json)# type is narrowed to ShapereturnHTTPResponse(status=200)# OKImportant:Current limitations in the mypy typechecker prevent the
automatic narrowing of the type ofrequest_jsonin the above example toShape, so you must add an additionalcast()to narrow the type manually:ifnotisassignable(request_json,Shape):...shape=cast(Shape,request_json)# type is manually narrowed to Shapedraw_shape(shape)These limitations are in the process of being resolved byintroducing TypeForm support to mypy.A betterisinstance()isassignable(value, T)is similar to Python's builtinisinstance()but
additionally supports checking against arbitrary type annotation objects
including TypedDicts, Unions, Literals, and many others.Formally,isassignable(value, T)checks whethervalueis consistent with a
variable of typeT(usingPEP 484static
typechecking rules), but atruntime.Motivation & AlternativesWhy use trycast?The trycast module is primarily designed forrecognizing JSON-like structuresthat can be described by Python's typing system. Secondarily, it can be used
forrecognizing arbitrary structuresthat can be described by
Python's typing system.Please seePhilosophyfor 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 asdataclasses(recommended),named tuples,attrs, or plain classes.Installationpython-mpipinstalltrycastRecommendations while using trycastSo thattrycast()can recognize TypedDicts with mixed required and
not-required keys correctly:Use Python 3.9+ if possible.Prefer usingtyping.TypedDict, unless you must use Python 3.8.
In Python 3.8 prefertyping_extensions.TypedDictinstead.Avoid usingmypy_extensions.TypedDictin general.Presentations & VideosA presentation aboutusing trycast to parse JSONwas given at the
2021 PyCon US Typing Summit:A presentation describingtools that use Python type annotations at runtime,
including trycast, was given at the 2022 PyCon US Typing Summit:ContributingPull requests are welcome! ThePython Community Code of Conductdoes apply.You can checkout the code locally using:git clone git@github.com:davidfstr/trycast.git
cd trycastCreate your local virtual environment to develop in usingPoetry:poetry shell
poetry installYou can run the existing automated tests in the current version of Python with:make testYou can also run the tests againstallsupported Python versions with:make testallSee additional development commands by running:make helpLicenseMITFeature ReferenceTyping Features SupportedScalarsboolintfloatNone, type(None)StringsstrRaw Collectionslist, Listtuple, TupleSequence, MutableSequencedict, DictMapping, MutableMappingGeneric Collections
(includingPEP 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]TypedDicttyping.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], etcTuple[T1], Tuple[T1, T2], Tuple[T1, T2, T3], etcUnionsUnion[X, Y]Optional[T]X | Y
(PEP 604)LiteralsLiteral[value]
(PEP 586)CallablesCallableCallable[P, R] (where P=[Any]*N and R=Any)NewTypes (when strict=False)Special TypesAnyNoReturnType Checkers SupportedTrycast does type check successfully with the following type checkers:MypyPyright/PylancePyrePytypeAPI Referencetrycast APIdef trycast(
    tp: TypeForm[T]† | TypeFormString[T]‡,
    value: object,
    /, failure: F = None,
    *, strict: bool = True,
    eval: bool = True
) -> T | F: ...Ifvalueis in the shape oftp(as accepted by a Python typechecker
conforming to PEP 484 "Type Hints") then returns it, otherwise returnsfailure(which is None by default).This method logically performs an operation similar to:return value if isinstance(tp, value) else failureexcept that it supports many more types thanisinstance, including:List[T]Dict[K, V]Optional[T]Union[T1, T2, ...]Literal[...]T extends TypedDictSimilar 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) -> TrueNote 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) -> FalseNote 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) -> FalseParameters:strict--If strict=False then trycast will additionally accept
mypy_extensions.TypedDict instances and Python 3.8 typing.TypedDict
instances for thetpparameter. 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 treatNewType("Foo", T)the same asT. 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 thetpargument.If strict=True and a NewType is found within thetpargument.If a TypeVar is found within thetpargument.If an unrecognized Generic type is found within thetpargument.UnresolvedForwardRefError--
Iftpis a type form which contains a ForwardRef.UnresolvableTypeError--
Iftpis a string that could not be resolved to a type.Footnotes:† TypeForm[T] is atype annotation object. For example:list[str]‡ TypeFormString[T] is a stringifiedtype annotation object. For example:"list[str]"isassignable APIdef isassignable(
    value: object,
    tp: TypeForm[T]† | TypeFormString[T]‡,
    *, eval: bool = True
) -> TypeGuard[T]: ...Returns whethervalueis in the shape oftp(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 thanisinstance, including:List[T]Dict[K, V]Optional[T]Union[T1, T2, ...]Literal[...]T extends TypedDictNote 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) -> TrueNote 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) -> FalseParameters: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 thetpargument.If strict=True and a NewType is found within thetpargument.If a TypeVar is found within thetpargument.If an unrecognized Generic type is found within thetpargument.UnresolvedForwardRefError--
Iftpis a type form which contains a ForwardRef.UnresolvableTypeError--
Iftpis a string that could not be resolved to a type.ChangelogFutureSee theRoadmap.v1.1.0Fixtrycast()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.0Extendtrycast()to recognize more kinds of types:Extendtrycast()to recognizeset[T]andSet[T]values.Extendtrycast()to recognizefrozenset[T]andFrozenSet[T]values.Extendtrycast()to recognizeCallableandCallable[P, R]types whenPandRonly containAny.Extendtrycast()to recognizeNewTypetypes when strict=False.Extendtrycast()to explicitly disallowTypeVartypes.Extendtrycast()to explicitly disallow unrecognizedGenerictypes.Fix issues with PEP 484 conformance:(Breaking change)boolvalues are now correctly treated as assignable toint.bool,int, andfloatvalues are now correctly treated as assignable tocomplex.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.3Support X|Y syntax for Union types fromPEP 604.Documentation improvements:Improve introduction.Add API reference.v0.7.2Add logo.v0.7.1Upgrade 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
atat least one companywithout issues.trycast supports all major Python type checkers
(Mypy, Pyright/Pylance, Pyre, Pytype).trycast's initial API is finalized.Fixcoverageto be a dev-dependency rather than a regular dependency.v0.7.0Finalize the initial API:Altertrycast()to usestrict=Trueby default rather thanstrict=False.(Breaking change)Define trycast's__all__to export only thetrycastandisassignablefunctions.Add support for additional type checkers, in addition toMypy:Add support for thePyrighttype checker andPylancelanguage server extension (for Visual Studio Code).Add support for thePyretype checker.Add support for thePytypetype checker.Extendtrycast()to recognize specialAnyandNoReturnvalues.Fixtrycast()to provide better diagnostic error when given a tuple
of types as itstpargument. Was broken in v0.6.0.v0.6.1Fixtrycast(..., eval=False)to not usetyping.get_type_hints(),
which internally callseval().Fixtrycast()andisassignable()to avoid swallowing KeyboardInterrupt
and other non-Exception BaseExceptions.v0.6.0Extendtrycast()to recognize a stringified type argument.Extendtrycast()to report a better error message when given
a type argument with an unresolved forward reference (ForwardRef).Fixstrictargument totrycastto be passed to inner calls oftrycastcorrectly.This also fixesisassignable()'s use of strict matching to be correct.Altertrycast()to interpret a type argument ofNoneor"None"as an
alias fortype(None), as consistent withPEP 484.AlterTypeNotSupportedErrorto extendTypeErrorrather thanValueError.(Breaking change)This is consistent withtrycast's andisinstance's behavior of using
aTypeErrorrather than aValueErrorwhen there is a problem with itstpargument.Drop support for Python 3.6.(Breaking change)Python 3.6 is end-of-life.v0.5.0isassignable()is introduced to the API:isassignable()leveragestrycast()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.Extendtrycast()to recognizeRequired[]andNotRequired[]fromPEP 655, as imported fromtyping_extensions.Extendtrycast()to support astrictparameter that controls whether it
acceptsmypy_extensions.TypedDictor Python 3.8typing.TypedDictinstances (which lack certain runtime type information necessary for
accurate runtime typechecking).For nowstrict=Falseby default for backward compatibility
with earlier versions oftrycast(), but this default is expected
to be altered tostrict=Truewhen/before trycast v1.0.0 is released.Rename primary development branch frommastertomain.v0.4.0Upgrade 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
atat least one companywithout 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.0TypedDict improvements & fixes:Fixtrycast()to recognize custom Mapping subclasses as TypedDicts.Extendtrycast()to recognize more JSON-like values:Extendtrycast()to recognizeMappingandMutableMappingvalues.Extendtrycast()to recognizetuple[T, ...]andTuple[T, ...]values.Extendtrycast()to recognizeSequenceandMutableSequencevalues.Extendtrycast()to recognizetuple[T1, T2, etc]andTuple[T1, T2, etc]values.Documentation improvements:Improve introduction.Outline motivation to use trycast and note alternatives.v0.2.0TypedDict improvements & fixes:Fixtrycast()to recognize TypedDicts frommypy_extensions.Extendtrycast()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 bytrycast().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.Altertyping_extensionsto be an optional dependency oftrycast.v0.1.0Add support for Python 3.6, 3.7, and 3.9, in addition to 3.8.v0.0.2Fix README to appear on PyPI.Add other package metadata, such as the supported Python versions.v0.0.1aInitial release.Supports typechecking all types found in JSON.