About

This documentation describes the type system provided by {typed} and how to use it.

  1. Overview

  2. Types

  3. Defining Types

  4. Metatypes

  5. Factories

  6. Defining Factories

  7. Annotations, Models and Dependent Types

  8. Typed Functions

  9. Typed Variables

  10. Other Docs

Overview

In {typed} type system we have three kinds of entities:

  1. types: are the basic entities

  2. factories: are functions used to build types

  3. typed functions: are functions with type hints checked at runtime.

Types

A {typed} is just a class named in CamelCase. Types can be organized into primitive types and derived types. The first ones are those that come already defined in {typed}. The other ones are those constructed by the user by making use of factories, as we will see in the sequence.

There are a lot of primitive types. Some of them are Python builtins.

type

python builtin

Int

int

Float

float

Str

str

Bool

bool

Nill

type(None)

table 1

Other examples of primitive types in {typed} which are not Python builtins, include:

type

description

Any

the type of anything

Json

the type of a json entity

Path

the type of paths

table 2

For the full list of primitive and types, see here.

Defining Types

Although any CamelCase class is an acceptable type, in {typed} the types are typically subject to the following conditions:

  1. they are constructed as the concretization of a metaclass

  2. they are a subclass of some already defined {typed} type

  3. they have a explicit __instancecheck__ method

  4. they may contains a explicit __subclasscheck__ method

So, a typical definition of type is as follows:

 1# the metaclass
 2class _SomeType(type(existing_type)):
 3    ...
 4    def __instancecheck__(cls, instance):
 5        ...
 6    def __subclasscheck__(cls, subclass):
 7        ...
 8
 9# the new type
10SomeType = _SomeType('SomeType', (existing_type,) {})

Metatypes

A very special kind of types are the so-called metatypes. They are “types of types”, being named in UPPERCASE notation instead of in CamelCase notation.

  • The main example of metatype is TYPE which is the type of all types.

In more precise terms, a metatype is any object t such that:

  1. issubclass(t, TYPE) is True;

  2. if isinstance(x, t) is True for some x, then isinstance(x, TYPE) is True as well.

The metatypes form themselves a metatype META.

Factories

In {typed}, a factory is a CamelCase named function that returns a type. It can receive types (i.e, instances of TYPE) as arguments or anything else. In the case where it receive only types as arguments, a factory is viewed as a “type operation”.

Another way of thinking about a factory is as a dependent type, as will be discussed later.

There a lot of predefined factories in {typed}. Some examples of factories that can be viewed as “type operations” are the following:

factory

description

Union

returns the union type of types

List

returns the type of list elements of given types

Tuple

returns the type of tuple elements of given types

Set

returns the type of set elements of given types

Dict

returns the type of dict elements of given types

table 3

Other examples, which are not “type operations”, are:

factory

description

Regex

returns type of strings that matches a regex

Len

returns the subtype of objects of a given lenght

Not

returns any object which is an instance of given types

Enum

returns the type formed by fixed values of a given type

Single

returns the type with a single given object

table 4

Similarly, a metafactory is a factory which returns a metatype. As happens with metatypes, metafactories are denoted with UPPERCASE notation. Some examples are the following:

metafactory

description

SUB

returns the metatype of all subtypes of given types

NOT

returns the metatype of all types which are not the given types

table 5

For the list and definition of all predefined factories, see here.

Defining Factories

Besides the predefined factories, you can create your own. In this case, it is recommended to use the decorator @factory in order to ensure type safety in the factory definition. The basic structure of a factory is as follows:

 1from typed import factory, Tuple, TYPE
 2
 3@factory
 4def SomeFactory(*args: Tuple(...)) -> TYPE:
 5    ...
 6    # 1. do something
 7    ...
 8    # 2. constructs a metaclass involving the '*args'
 9    class _SomeType(type(...)):
10        ...
11        def __instancecheck__(cls, instance):
12            ...
13        def __subclasscheck__(cls, subclass):
14            ...
15    # 3. returns a concrete class
16    ...
17    return _SomeType('SomeType', (...,), {...})

Annotations, Models and Dependent Types

As you can note from table above, there is a factory associated to each annotation in the library typing. In this sense, you can also think of a factory as a way to implement the type annotations from typing:

{typed} can be viewed as a library in which type hints really work.

Note that the factories of {typed} that corresponds to type annotations in typing are always “type operations”. On other and, we commented that there are other kinds of factories. Therefore, actually

{typed} does much more than just implement type annotations.

A special flavor of factories are the so-called models. They receive a bunch of arguments and produces types that have such arguments as attributes. They work similar to dataclasses or to classes that extends BaseModel in pydantic, being specially used for data validation (this will be discussed in the sequence - see here). So:

{typed} provides an effective mechanism for data validation.

A generic factory is a function factory: X -> TYPE that assigns a type to a collection of arguments, hence can be viewed as an “indexed family of types”, hence as an implementation of dependent types in Python:

typed implements dependent types in Python.

Typed Functions

In {typed}, there is a distinguished class of functions: the typed functions. They have the following characteristics:

  1. they are defined using the @typed decorator;

  2. each of its arguments is decorated with an annotation given by a {typed} type;

  3. it also has a return annotation given by a {typed} type.

So, in essence, the structure of a typed function is as follows:

1from typed import typed, SomeType, OtherType, ...
2
3@typed
4def my_function(some_var: SomeType, ...) -> OtherType:
5    ...

When you define a function as a typed function, its type annotations are checked at runtime, ensuring type safety. This means that, looking at the examples above, if at runtime some_var is not an instance of SomeType, or if the return value of my_function is not an instance of OtherType, then a TypeError will be raised.

The type errors from {typed} are as intuitive as possible. For more about them, see errors.

Typed Variables

As a function, the @typed decorator can be applied not only to functions, but also to types, allowing us to define typed variables, as below.

1from typed import typed, SomeType
2
3some_var = typed(SomeType)
4
5some_var = typed(SomeType, some_value)

In both cases above, at runtime it will be checked if some_var has the defined type SomeType, returning a TypeError otherwise.

Other Docs

  1. overview

  2. models

  3. errors

  4. examples

  5. api

  6. glossary

  7. changelog