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}