Type[T]
Types that make coding in Python quick and safe.
Type[T] works best with Python 3.6 or later. Prior to 3.6, object types must use comment type hint syntax.
Installation
Install it using pip:
pip install typet
Features
- An Object base class that eliminates boilerplate code and verifies and coerces types when possible.
- Validation types that, when instantiated, create an instance of a specific type and verify that they are within the user defined boundaries for the type.
Quick Start: Creating a Person
Import the Type[T] types that you will use.
from typet import Bounded, Object, String
- Object, for composing complex objects
- Bound to describe a type that validates its value is of the correct type and within bounds upon instantiation
- String, which will validate that it is instantiated with a string with a length within the defined bounds.
Create Type Aliases That Describe the Intent of the Type
Age = Bounded[int, 0:150]
Name = String[1:50]
Hobby = String[1:300]
In this example, a Person has an Age, which is an integer between 0 and 150, inclusive; a Name which must be a non-empty string with no more than 50 characters; and finally, a Hobby, which is a non-empty string with no more than 300 characters.
Compose a Person object Using Type Aliases
class Person(Object):
name: Name
surname: Name
age: Age
hobby: Hobby = None
Assigning a class attribute sets that value as the default value for instances of the Object. In this instance, hobby is assigned a default value of None; by convention, this tells Python that the type is Optional[Hobby], and Type[T] will allow None in addition to strings of the correct length.
Put It All Together
from typet import Bounded, Object, String
Age = Bounded[int, 0:150]
Name = String[1:50]
Hobby = String[1:300]
class Person(Object):
name: Name
surname: Name
age: Age
hobby: Hobby = None
Person is now a clearly defined and typed object with an intuitive constructor, hash method, comparison operators and bounds checking.
Positional arguments will be in the order of the definition of class attributes, and keyword arguments are also acceptable.
jim = Person('Jim', 'Coder', 23, 'Python')
bob = Person('Robert', 'Coder', hobby='C++', age=51)
Python 2.7 to 3.5
Type[T] supports PEP 484 class comment type hints for defining an Object.
from typing import Optional
from typet import Bounded, Object, String
Age = Bounded[int, 0:150]
Name = String[1:50]
Hobby = String[1:300]
class Person(Object):
name = None # type: Name
surname = None # type: Name
age = None # type: Age
hobby = None # type: Optional[Hobby]
Note that, because Python prior to 3.6 cannot annotate an attribute without defining it, by convention, assigning the attribute to None will not imply that it is optional; it must be specified explicitly in the type hint comment.
Object Types
Object
One of the cooler features of Type[T] is the ability to create complex objects with very little code. The following code creates an object that generates properties from the annotated class attributes that will ensure that only values of int or that can be coerced into int can be set. It also generates a full suite of common comparison methods.
from typet import Object
class Point(Object):
x: int
y: int
Point objects can be used intuitively because they generate a standard __init__ method that will allow positional and keyword arguments.
p1 = Point(0, 0) # Point(x=0, y=0)
p2 = Point('2', 2.5) # Point(x=2, y=2)
p3 = Point(y=5, x=2) # Point(x=2, y=5)
assert p1 < p2 # True
assert p2 < p1 # AssertionError
A close equivalent traditional class would be much larger, would have to be updated for any new attributes, and wouldn't support more advanced casting, such as to types annotated using the typing module:
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return 'Point(x={x}, y={y})'.format(x=self.x, y=self.y)
def __setattr__(self, name, value):
if name in ('x', 'y'):
value = int(value)
super(Point, self).__setattr__(name, value)
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __ne__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) != (other.x, other.y)
def __lt__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) < (other.x, other.y)
def __le__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) <= (other.x, other.y)
def __gt__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) > (other.x, other.y)
def __ge__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.x, self.y) >= (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
Attributes can be declared optional either manually, by using typing.Optional or by using the PEP 484 implicit optional of a default value of None.
from typing import Optional
from typet import Object
class Point(Object):
x: Optional[int]
y: int = None
p1 = Point() # Point(x=None, y=None)
p2 = Point(5) # Point(x=5, y=None)
StrictObject
By default, Object will use cast from typingplus to attempt to coerce any values supplied to attributes to the annotated type. In some cases, it may be preferred to disallow casting and only allow types that are already of the correct type. StrictObject has all of the features of Object, but will not coerce values into the annotated type.
from typet import StrictObject
class Point(StrictObject):
x: int
y: int
Point(0, 0) # Okay
Point('2', 2.5) # Raises TypeError
StrictObject uses is_instance from typingplus to check types, so it's possible to use types from the typing library for stricter checking.
from typing import List
from typet import StrictObject
class IntegerContainer(StrictObject):
integers: List[int]
IntegerContainer([0, 1, 2, 3]) # Okay
IntegerContainer(['a', 'b', 'c', 'd']) # Raises TypeError
Validation Types
Type[T] contains a suite of sliceable classes that will create bounded, or validated, versions of those types that always assert their values are within bounds; however, when an instance of a bounded type is instantiated, the instance will be of the original type.
Bounded
Bounded can be sliced with either two arguments or three. The first argument is the type being bound. The second is a slice containing the upper and lower bounds used for comparison during instantiation.
from typet import Bounded
BoundedInt = Bounded[int, 10:20]
BoundedInt(15) # Okay
type(x) # <class 'int'>
BoundedInt(5) # Raises ValueError
Optionally, a third argument, a function, may be supplied that will be run on the value before the comparison.
from typet import Bounded
LengthBoundedString = Bounded[str, 1:3, len]
LengthBoundedString('ab') # Okay
LengthBoundedString('') # Raises ValueError
LengthBoundedString('abcd') # Raises ValueError
Length
Because len is a common comparison method, there is a shortcut type, Length that takes two arguments and uses len as the comparison method.
from typing import List
from typet import Length
LengthBoundedList = Length[List[int], 1:3]
LengthBoundedList([1, 2]) # Okay
LengthBoundedList([]) # Raises ValueError
LengthBoundedList([1, 2, 3, 4]) # Raises ValueError
String
str and len are commonly used together so a special type, String, has been added to simplify binding strings to specific lengths.
from typet import String
ShortString = String[1:3]
ShortString('ab') # Okay
ShortString('') # Raises ValueError
ShortString('abcd') # Raises ValueError
Note that, on Python 2, String instantiates unicode objects and not str.
Metaclasses and Utilities
Singleton
Singleton will cause a class to allow only one instance.
from typet import Singleton
class Config(metaclass=Singleton):
pass
c1 = Config()
c2 = Config()
assert c1 is c2 # Okay
Singleton supports an optional __singleton__ method on the class that will allow the instance to update if given new parameters.
from typet import Singleton
class Config(metaclass=Singleton):
def __init__(self, x):
self.x = x
def __singleton__(self, x=None):
if x:
self.x = x
c1 = Config(1)
c1.x # 1
c2 = Config() # Okay because __init__ is not called.
c2.x # 1
c3 = Config(3) # Calls __singleton__ if it exists.
c1.x # 3
c2.x # 3
c3.x # 3
assert c1 is c2 is c3 # Okay
@singleton
Additionally, there is a decorator, @singleton that can be used make a class a singleton, even if it already uses another metaclass. This is convenient for creating singleton Objects.
from typet import Object, singleton
@singleton
class Config(Object):
x: int
c1 = Config(1)
c2 = Config() # Okay because __init__ is not called.
assert c1 is c2 # Okay
@metaclass
Type[T] contains a class decorator, @metaclass, that will create a derivative metaclass from the given metaclasses and the metaclass used by the decorated class and recreate the class with the derived metaclass.
Most metaclasses are not designed to be used in such a way, so careful testing must be performed when this decorator is to be used. It is primarily intended to ease use of additional metaclasses with Objects.
from typet import metaclass, Object, Singleton
@metaclass(Singleton)
class Config(Object):
x: int
c1 = Config(1)
c2 = Config() # Okay because __init__ is not called.
assert c1 is c2 # Okay