The Basics#

Typical is built around a powerful, high-level functional API whose purpose is to make working with annotations at runtime a breeze for any developer.

Installation#

Installation is as simple as pip install -U typical.

For an easy speedup at runtime, you can add ujson to the installation with typical[json]. This results in a 30-50% improvement when serializing your data with .tojson().

@typic.al()#

The simplest way to get going with Typical is the @typic.al decorator. This is the core entrypoint for all the magic 🔮.

Wrapping Callables#

import enum

import typic


class Decision(enum.IntEnum):
    YES = 1
    NO = 0
    MAYBE = -1


class Explanation(str, enum.Enum):
    YES = "Of course!"
    NO = "That's just the way it is."
    MAYBE = "¯\_(ツ)_/¯"


DECISION_MAP = dict(zip(Decision, Explanation))


@typic.al
def explain(decision: Decision) -> str:
    return DECISION_MAP[decision]


print(repr(explain(1.0)))
#> <Explanation.YES: 'Of course!'>

In the above example, Typical has taken care of your runtime type-validation automatically. It also follows the classical Python logic of duck-typing: float -> int -> Decision. But it will handle more cases than that:

print(repr(explain(b"-1")))
#> <Explanation.MAYBE: '¯\\_(ツ)_/¯'>

Typical knows to look for common cases such as json and string/byte literals and handles them gracefully.

That means you don’t have to remember to handle every single edge case yourself. Just write your pure function and let Typical handle the rest. This is incredibly useful for code which lives on the edges of your application - such as a handler for an external caller or an ingestor from a data-source.

Errors#

But what about errors? Typical is built to provide transparency in the event that a value cannot be transmuted into the expected annotation. Taking the above example, if we pass a value not defined by our Decsion enum:

explain(2)
#> ValueError: 2 is not a valid Decision

So instead of a random, non-descriptive KeyError, you get a clear, predictable ValueError which can be easily passed on to the external caller for handling.

Wrapping Classes#

Typical works with classes too -

import typic


@typic.al
class Foo:
    bar: str

    def __init__(self, bar: str):
        self.bar = bar

print(repr(Foo(b"bar").bar))
#> 'bar'

But if you’re wrapping your class, you may as well go all the way with that sexy dataclass-style…

import typic


@typic.klass
class Foo:
    bar: str

This is a dataclass under the hood, so anything you do with dataclasses you can do with your @typic.klass, and much, much more.