Peritype

Why?

Python's typing system has been evolving for many years, introducing new features and syntaxes. Everything is available at runtime through the typing module, but navigating and manipulating these types can be a challenge.

Peritype offers a simple interface to inspect and work with Python types at runtime. You can easily inspect generic parameters, attribute type hints and method signatures of any type.

Installation

Peritype has no external dependencies and works with Python 3.12 and above.

$ pip install peritype

Install it through pip or use your favorite package manager.

Type wrapper

Peritype works by wrapping Python types into a user-friendly interface. Simply call wrap_type() with any type to get started.

from peritype import wrap_type

int_w = wrap_type(int)

Type Matching

Python's isinstance() and issubclass() do not support checking generic types. Peritype's type wrappers provide a powerful match() method to check if a type matches another type, including generics, unions, and more.

from typing import Any
from peritype import wrap_type

# Wrap simple types
assert wrap_type(int).match(int)
assert not wrap_type(int).match(str)

# Wrap union types
assert wrap_type(int | str).match(int)
assert wrap_type(int | str).match(str)
assert wrap_type(int).match(int | str)

# Wrap generic types
assert wrap_type(list[int]).match(list[int])
assert wrap_type(list[int]).match(list[int | str])

# Test with Any
assert wrap_type(list[int]).match(list[Any])
assert wrap_type(list[Any]).match(list[int])

Type Inspection

With Peritype, you can inspect the type hints of a Python class, like attributes and method signatures.

Simple types

from typing import Any, TypedDict
from peritype import wrap_type

class HttpClient:
    def get(self, url: str) -> dict[str, Any]:
        ...

class DataObject(TypedDict):
    id: int
    name: str

class Service:
    def __init__(self, http_client: HttpClient) -> None:
        self.http_client = http_client

    def fetch_object(self, url: str) -> DataObject:
        return self.http_client.get(url)

All wrapped types provide wrapped types for their attributes and methods. You can access attribute type hints via the attribute_hints property. You will get a dictionary mapping attribute names to their corresponding type wrappers.

data_w = wrap_type(DataObject)
attr_hints = data_w.attribute_hints
assert attr_hints["id"].match(int)
assert attr_hints["name"].match(str)

You can access a wrapped method using the get_method() method. This returns a wrapped method, which provides access to the method's signature hints and more, see Function wrapper for details.

service_w = wrap_type(Service)
method_w = service_w.get_method('fetch_object')
signature_hints = method_w.get_signature_hints()
assert signature_hints['url'].match(str)
return_hint = method_w.get_return_hint()
assert return_hint.match(data_w)

Nullables

If the wrapped type is a union with None, Peritype sets the nullable property on the wrapped type to True.

Generics

If you're working with generic types, especially those that inherit from other generics, Peritype can resolve the generic parameters for you when you wrap the type.

from peritype import wrap_type

class GenericParent[T]:
    def get_value(self) -> T:
        ...

class GenericChild[T, U](GenericParent[U]):
    def get_other_value(self) -> T:
        ...

wrapped_child = wrap_type(GenericChild[int, str])

value_w = wrapped_child.get_method('get_value')
value_signature = value_w.get_signature_hints()
assert value_w.get_return_hint().match(str)

other_value_w = wrapped_child.get_method('get_other_value')
other_value_signature = other_value_w.get_signature_hints()
assert other_value_w.get_return_hint().match(int)

To inspect the generic parameters of a wrapped type, you can use the generic_params property, which returns a tuple of type wrappers representing the resolved generic parameters.

generic_params = wrapped_child.generic_params
assert generic_params[0].match(int)  # T
assert generic_params[1].match(str)  # U

Union types

All of the above inspection features will not work with union types, they will raise a TypeError upon usage.

Peritype internal structure works with “nodes”. You can access them via the nodes property on any type wrapper. It return a tuple of type nodes, from which you can call the same inspection features as above in this section, even type matching.

from peritype import wrap_type

union_w = wrap_type(GenericChild[int, str] | GenericChild[str, int])
assert union_w.is_union

nodes = union_w.nodes

node1 = nodes[0]
assert node1.get_method('get_other_value').get_return_hint().match(int)
assert node1.generic_params == (wrap_type(int), wrap_type(str))

node2 = nodes[1]
assert node2.get_method('get_other_value').get_return_hint().match(str)
assert node2.generic_params == (wrap_type(str), wrap_type(int))

Note: Service | None is not considered a union type by Peritype.

Annotated types

Python's Annotated metadata is picked up by Peritype's type wrappers. You can access the metadata using the annotations property on any type wrapper. It will return a tuple of all annotations, in the order they were defined, without any processing.

from typing import Annotated
from peritype import wrap_type

type AnnotatedInt = Annotated[int, "Should always be", 42]

annotated_w = wrap_type(AnnotatedInt)
assert annotated_w.match(int)
annotations = annotated_w.annotations
assert annotations[0] == "Should always be"
assert annotations[1] == 42

Function wrapper

Function wrapping allows you to inspect type hints and signatures of functions. Type wrappers return function wrappers when accessing methods and type __init__() methods, but you can also wrap functions directly.

from peritype import wrap_func

def greet(name: str) -> str:
    return f"Hello, {name}!"

wrapped_greet = wrap_func(greet)

hints = wrapped_greet.get_signature_hints()
assert hints["name"].match(str)

return_hint = wrapped_greet.get_return_hint()
assert return_hint.match(str)

inspect.Signature and inspect.Parameter objects are also available via the signature and parameters properties.

signature = wrapped_greet.signature
assert isinstance(signature, inspect.Signature)

params = wrapped_greet.parameters
assert len(params) == 1
assert params[0].name == "name"

Methods accessed from wrapped types return bound function wrappers, which automatically resolve the generic parameters used in the method signature. If you manually wrap a method, the generic parameters will not be resolved.

from peritype import wrap_type

class Service[T]:
    def fetch_object(self, url: str) -> T:
        ...

service_w = wrap_type(Service[int])
method_w = service_w.get_method('fetch_object')
signature_hints = method_w.get_signature_hints()
assert signature_hints['url'].match(str)
return_hint = method_w.get_return_hint()
assert return_hint.match(int)

Cache

Peritype includes an internal caching mechanism that stores wrapped types to improve performance. When you wrap a type using wrap_type(), or a function using wrap_func(), the results are cached internally.

You can disable the caching mechanism if you want to, but keep in mind that the wrapping operations will be slower without caching.

from peritype import use_cache

use_cache(False)

Type collections

Peritype provides handy collections to manipulate and store types.

TypeBag

The TypeBag is a collection that works like a set but is specifically designed to hold type wrappers. The main feature is being able to get all types that match a given type defined with a Any in its parameters.

from typing import Any
from peritype import wrap_type
from peritype.collections import TypeBag

int_w = wrap_type(int)
str_w = wrap_type(str)

type_bag = TypeBag()
type_bag.add(int_w)
type_bag.add(str_w)

assert int_w in type_bag
assert wrap_type(float) not in type_bag

matching_types = type_bag.get_all(wrap_type(list[Any]))
assert len(matching_types) == 2
assert matching_types == {int_w, str_w}

TypeMap

The TypeMap[K, V] is a collection that works like a dictionary but is specifically designed to hold type wrappers as keys and maps to any kind of value. There is also TypeSetMap[K, V] that inherits from TypeMap[K, set[V]] that's handy to map types to multiple values.

from typing import Any
from peritype import wrap_type
from peritype.collections import TypeMap, TypeSetMap

int_w = wrap_type(int)
str_w = wrap_type(str)

type_map = TypeMap[Any, str]()
type_map[int_w] = "Integer type"
type_map[str_w] = "String type"

assert type_map[int_w] == "Integer type"
assert type_map[str_w] == "String type"
assert wrap_type(float) not in type_map

set_map = TypeSetMap[Any, str]()
set_map.push(int_w, "Number type")
set_map.push(int_w, "Whole number type")
set_map.push(str_w, "Text type")

assert set_map[int_w] == {"Number type", "Whole number type"}
assert set_map.count(str_w) == 1

TypeSuperTree

The TypeSuperTree stores all types and their bases and associates them each subtype. This allows to quickly get all subtypes stored in the tree of a given type.

tree = TypeSuperTree()

class SuperType[T]: ...

class MidType[T](SuperType[T]): ...

class SubType[T](MidType[T]): ...

super_twrap = wrap_type(SuperType[int])
mid_twrap = wrap_type(MidType[int])
twrap = wrap_type(SubType[int])
tree.add(twrap)

assert super_twrap in tree
assert tree[super_twrap] == {twrap, mid_twrap, super_twrap}
assert mid_twrap in tree
assert tree[mid_twrap] == {twrap, mid_twrap}
assert twrap in tree
assert tree[twrap] == {twrap}