Skip to main content

No project description provided

Project description

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  # type: object
    if (point := trycast(Point2D, request_json)) is not None:
        draw_point(point)  # type is narrowed to Point2D
        return HTTPResponse(status=200)
    else:
        return HTTPResponse(status=400)  # Bad Request

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 not None:
        draw_shape(shape)  # type is narrowed to Shape
        return HTTPResponse(status=200)  # OK
    else:
        return HTTPResponse(status=400)  # Bad Request

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 not 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 TypedDicts 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 isassignable(request_json, Shape):
        draw_shape(request_json)  # type is narrowed to Shape
        return HTTPResponse(status=200)  # OK
    else:
        return HTTPResponse(status=400)  # Bad Request

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 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.

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 typed dictionaries to represent data structures instead of classes, named tuples, or other formats?

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.

Other alternatives for representing data structures in Python include dataclasses, named tuples, attrs, and plain classes.

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.

More Information

A presentation about trycast was given at the 2021 PyCon Typing Summit:

2021 PyCon Typing Summit Presentation

License

MIT

API Reference

trycast API

def trycast(
    tp: TypeForm[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

Note that unlike isinstance(), this method does NOT consider bool values to be valid int values, as consistent with Python typecheckers:

trycast(int, False) -> None 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:

trycast(float, 1) -> 1 isinstance(1, float) -> 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.
  • 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.
  • 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.

isassignable API

def isassignable(
    value: object,
    tp: TypeForm[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 mypy_extensions.TypedDict or a Python 3.8 typing.TypedDict 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

v0.7.3

  • Support X|Y syntax for Union types from PEP 604.
  • Documentation improvements:
    • Improve introduction.
    • Add API reference.

v0.7.2

  • Add logo.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

trycast-0.7.3.tar.gz (17.5 kB view details)

Uploaded Source

Built Distribution

trycast-0.7.3-py3-none-any.whl (12.3 kB view details)

Uploaded Python 3

File details

Details for the file trycast-0.7.3.tar.gz.

File metadata

  • Download URL: trycast-0.7.3.tar.gz
  • Upload date:
  • Size: 17.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.6 CPython/3.10.1 Darwin/18.7.0

File hashes

Hashes for trycast-0.7.3.tar.gz
Algorithm Hash digest
SHA256 2ce9a7dcd9fc40356ee23107a2898f5aac166cfec67f082303e36e32c6d4b9de
MD5 e1bfd6f4db2322a943616eb3d5826a6a
BLAKE2b-256 20289479c53084848f800e421857f19495ec8d501cc47bda2520429f4a71e801

See more details on using hashes here.

File details

Details for the file trycast-0.7.3-py3-none-any.whl.

File metadata

  • Download URL: trycast-0.7.3-py3-none-any.whl
  • Upload date:
  • Size: 12.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.6 CPython/3.10.1 Darwin/18.7.0

File hashes

Hashes for trycast-0.7.3-py3-none-any.whl
Algorithm Hash digest
SHA256 68a55f4644560b6a20dfc3bd970fd668011f0d37bf98f620b1d5d7b8feaaeb44
MD5 de0e9a54d70fdb41f2a08f42dcefdfb5
BLAKE2b-256 2baa1d2b8c612a552687f0fc05c5da60a2105649876855eadd1c394cd4b36c25

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page