type_enforced
Type Enforced
A pure python (no special compiler required) type enforcer for type annotations. Enforce types in python functions and methods.
Setup
Make sure you have Python 3.11.x (or higher) installed on your system. You can download it here.
- Unsupported python versions can be used, however newer features will not be available.
- For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported)
- For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported)
- For 3.9: use type_enforced<=1.9.0 (
staticmethod
, union with|
andfrom __future__ import annotations
typechecking are not supported) - For 3.10: use type_enforced<=1.10.2 (
from __future__ import annotations
may cause errors (EG: when using staticmethods and classmethods))
Installation
pip install type_enforced
Basic Usage
import type_enforced
@type_enforced.Enforcer(enabled=True, strict=True)
def my_fn(a: int , b: int | str =2, c: int =3) -> None:
pass
- Note:
enabled=True
by default if not specified. You can setenabled=False
to disable type checking for a specific function, method, or class. This is useful for a production vs debugging environment or for undecorating a single method in a larger wrapped class. - Note:
strict=True
by default if not specified. You can setstrict=False
to disable exceptions being raised when type checking fails. Instead, a warning will be printed to the console.
Getting Started
type_enforcer
contains a basic Enforcer
wrapper that can be used to enforce many basic python typing hints. Technical Docs Here.
Enforcer
can be used as a decorator for functions, methods, and classes. It will enforce the type hints on the function or method inputs and outputs. It takes in the following optional arguments:
enabled
(True): A boolean to enable or disable type checking. IfTrue
, type checking will be enforced. IfFalse
, type checking will be disabled.strict
(True): A boolean to enable or disable type mismatch exceptions. IfTrue
exceptions will be raised when type checking fails. IfFalse
, exceptions will not be raised but instead a warning will be printed to the console.
type_enforcer
currently supports many single and multi level python types. This includes class instances and classes themselves. For example, you can force an input to be an int
, a number int | float
, an instance of the self defined MyClass
, or a even a vector with list[int]
. Items like typing.List
, typing.Dict
, typing.Union
and typing.Optional
are supported.
You can pass union types to validate one of multiple types. For example, you could validate an input was an int or a float with int | float
or typing.Union[int, float]
.
Nesting is allowed as long as the nested items are iterables (e.g. typing.List
, dict
, ...). For example, you could validate that a list is a vector with list[int]
or possibly typing.List[int]
.
Variables without an annotation for type are not enforced.
Why use Type Enforced?
type_enforced
is a pure python type enforcer that does not require any special compiler or preprocessor to work.type_enforced
uses the standard python typing hints and enforces them at runtime.- This means that you can use it in any python environment (3.11+) without any special setup.
type_enforced
is designed to be lightweight and easy to use, making it a great choice for both small and large projects.type_enforced
supports complex (nested) typing hints, union types, and many of the standard python typing functions.- Other Options:
Pydantic
:- A great all around option.
- Its ~40% slower at scale than type enforced for larger object verifications with complex (nested) types.
- It is ~30% faster for verifications of smaller objects.
- It is designed for data classes
- We have found it to be very consistent with type checking including complex (nested) type hints.
Beartype
:- The fastest option we tested by far (close to O(n) time complexity).
- Is great for simple type checking and we highly recommend it for that use case.
- Is inconsistent at catching errors with complex (nested) type hints (at least in beartype 0.21.0)
- EG: Running the same data over a loop randomly fails to catch type errors.
Typeguard
:- A useable option.
- ~100% slower than type enforced for simple type checking.
- Does not support complex (nested) type hints.
Enforce
:- Has not been updated since 2017.
- We were unable to get working with python 3.13.
- Note: See the benchmarks for more information on the performance of each type checker.
Supported Type Checking Features:
- Function/Method Input Typing
- Function/Method Return Typing
- Dataclass Typing
- All standard python types (
str
,list
,int
,dict
, ...) - Union types
- typing.Union
|
separated items (e.g.int | float
)
- Nested types (e.g.
dict[str, int]
orlist[int|float]
)- Note: Each parent level must be an iterable
- Specifically a variant of
list
,set
,tuple
ordict
- Specifically a variant of
- Note:
dict
requires two types to be specified (unions count as a single type)- The first type is the key type and the second type is the value type
- e.g.
dict[str, int|float]
ordict[int, float]
- Note:
list
andset
require a single type to be specified (unions count as a single type)- e.g.
list[int]
,set[str]
,list[float|str]
- e.g.
- Note:
tuple
Allows forN
types to be specified- Each item refers to the positional type of each item in the tuple
- Support for ellipsis (
...
) is supported if you only specify two types and the second is the ellipsis type- e.g.
tuple[int, ...]
ortuple[int|str, ...]
- e.g.
- Note: Unions between two tuples are not supported
- e.g.
tuple[int, str] | tuple[str, int]
will not work
- e.g.
- Deeply nested types are supported too:
dict[dict[int]]
list[set[str]]
- Note: Each parent level must be an iterable
- Many of the
typing
(package) functions and methods including:- Standard typing functions:
List
Set
Dict
Tuple
Union
Optional
Any
Sized
- Essentially creates a union of:
list
,tuple
,dict
,set
,str
,bytes
,bytearray
,memoryview
,range
- Note: Can not have a nested type
- Because this does not always meet the criteria for
Nested types
above
- Because this does not always meet the criteria for
- Essentially creates a union of:
Literal
- Only allow certain values to be passed. Operates slightly differently than other checks.
- e.g.
Literal['a', 'b']
will require any passed values that are equal (==
) to'a'
or'b'
.- This compares the value of the passed input and not the type of the passed input.
- Note: Multiple types can be passed in the same
Literal
as acceptable values.- e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (
==
) to'a'
,'b'
,1
or2
.
- e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (
- Note: If type is a
str | Literal['a', 'b']
- The check will validate that the type is a string or the value is equal to
'a'
or'b'
. - This means that an input of
'c'
will pass the check since it matches the string type, but an input of1
will fail.
- The check will validate that the type is a string or the value is equal to
- Note: If type is a
int | Literal['a', 'b']
- The check will validate that the type is an int or the value is equal to
'a'
or'b'
. - This means that an input of
'c'
will fail the check, but an input of1
will pass.
- The check will validate that the type is an int or the value is equal to
- Note: Literals stack when used with unions.
- e.g.
Literal['a', 'b'] | Literal[1, 2]
will require any passed values that are equal (==
) to'a'
,'b'
,1
or2
.
- e.g.
Callable
- Essentially creates a union of:
staticmethod
,classmethod
,types.FunctionType
,types.BuiltinFunctionType
,types.MethodType
,types.BuiltinMethodType
,types.GeneratorType
- Essentially creates a union of:
- Note: Other functions might have support, but there are not currently tests to validate them
- Feel free to create an issue (or better yet a PR) if you want to add tests/support
- Standard typing functions:
Constraint
validation.- This is a special type of validation that allows passed input to be validated.
- Standard and custom constraints are supported.
- This is useful for validating that a passed input is within a certain range or meets a certain criteria.
- Note: Constraints stack when used with unions.
- e.g.
int | Constraint(ge=0) | Constraint(le=5)
will require any passed values to be integers that are greater than or equal to0
and less than or equal to5
.
- e.g.
- Note: The constraint is checked after type checking occurs and operates independently of the type checking.
- This operates differently than other checks (like
Literal
) and is evaluated post type checking. - For example, if you have an annotation of
str | Constraint(ge=0)
, this will always raise an exception since if you pass a string, it will raise on the constraint check and if you pass an integer, it will raise on the type check.
- This operates differently than other checks (like
- Note: See the example below or technical constraint and generic constraint docs for more information. ```
- This is a special type of validation that allows passed input to be validated.
Interactive Example
>>> import type_enforced
>>> @type_enforced.Enforcer
... def my_fn(a: int , b: int|str =2, c: int =3) -> None:
... pass
...
>>> my_fn(a=1, b=2, c=3)
>>> my_fn(a=1, b='2', c=3)
>>> my_fn(a='a', b=2, c=3)
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
my_fn(a='a', b=2, c=3)
~~~~~^^^^^^^^^^^^^^^^^
File "/app/type_enforced/enforcer.py", line 233, in __call__
self.__check_type__(assigned_vars.get(key), value, key)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/type_enforced/enforcer.py", line 266, in __check_type__
self.__exception__(
~~~~~~~~~~~~~~~~~~^
f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/app/type_enforced/enforcer.py", line 188, in __exception__
raise TypeError(f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}")
TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` with value `a` instead.
Nested Examples
import type_enforced
import typing
@type_enforced.Enforcer
def my_fn(
a: dict[str,dict[str, int|float]], # Note: For dicts, the key is the first type and the value is the second type
b: list[typing.Set[str]] # Could also just use set
) -> None:
return None
my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success
my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error =>
# TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a['i']['j']`. Expected one of the following `[<class 'int'>, <class 'float'>]` but got `<class 'str'>` with value `k` instead.
Class and Method Use
Type enforcer can be applied to methods individually:
import type_enforced
class my_class:
@type_enforced.Enforcer
def my_fn(self, b:int):
pass
You can also enforce all typing for all methods in a class by decorating the class itself.
import type_enforced
@type_enforced.Enforcer
class my_class:
def my_fn(self, b:int):
pass
def my_other_fn(self, a: int, b: int | str):
pass
You can also enforce types on staticmethod
s and classmethod
s if you are using python >= 3.10
. If you are using a python version less than this, classmethod
s and staticmethod
s methods will not have their types enforced.
import type_enforced
@type_enforced.Enforcer
class my_class:
@classmethod
def my_fn(self, b:int):
pass
@staticmethod
def my_other_fn(a: int, b: int | str):
pass
Dataclasses are suported too.
import type_enforced
from dataclasses import dataclass
@type_enforced.Enforcer
@dataclass
class my_class:
foo: int
bar: str
You can skip enforcement if you add the argument enabled=False
in the Enforcer
call.
- This is useful for a production vs debugging environment.
- This is also useful for undecorating a single method in a larger wrapped class.
- Note: You can set
enabled=False
for an entire class or simply disable a specific method in a larger wrapped class. - Note: Method level wrapper
enabled
values take precedence over class level wrappers.
import type_enforced
@type_enforced.Enforcer
class my_class:
def my_fn(self, a: int) -> None:
pass
@type_enforced.Enforcer(enabled=False)
def my_other_fn(self, a: int) -> None:
pass
Validate with Constraints
Type enforcer can enforce constraints for passed variables. These constraints are validated after any type checks are made.
To enforce basic input values are integers greater than or equal to zero, you can use the Constraint class like so:
import type_enforced
from type_enforced.utils import Constraint
@type_enforced.Enforcer()
def positive_int_test(value: int |Constraint(ge=0)) -> bool:
return True
positive_int_test(1) # Passes
positive_int_test(-1) # Fails
positive_int_test(1.0) # Fails
To enforce a GenericConstraint:
import type_enforced
from type_enforced.utils import GenericConstraint
CustomConstraint = GenericConstraint(
{
'in_rgb': lambda x: x in ['red', 'green', 'blue'],
}
)
@type_enforced.Enforcer()
def rgb_test(value: str | CustomConstraint) -> bool:
return True
rgb_test('red') # Passes
rgb_test('yellow') # Fails
Validate class instances and classes
Type enforcer can enforce class instances and classes. There are a few caveats between the two.
To enforce a class instance, simply pass the class itself as a type hint:
import type_enforced
class Foo():
def __init__(self) -> None:
pass
@type_enforced.Enforcer
class my_class():
def __init__(self, object: Foo) -> None:
self.object = object
x=my_class(Foo()) # Works great!
y=my_class(Foo) # Fails!
Notice how an initialized class instance Foo()
must be passed for the enforcer to not raise an exception.
To enforce an uninitialized class object use typing.Type[classHere]
on the class to enforce inputs to be an uninitialized class:
import type_enforced
import typing
class Foo():
def __init__(self) -> None:
pass
@type_enforced.Enforcer
class my_class():
def __init__(self, object_class: typing.Type[Foo]) -> None:
self.object = object_class()
y=my_class(Foo) # Works great!
x=my_class(Foo()) # Fails
By default, type_enforced will check for subclasses of a class when validating types. This means that if you pass a subclass of the expected class, it will pass the type check.
Note: Uninitialized class objects that are passed are not checked for subclasses.
import type_enforced
class Foo:
pass
class Bar(Foo):
pass
class Baz:
pass
@type_enforced.Enforcer
def my_fn(custom_class: Foo):
pass
my_fn(Foo()) # Passes as expected
my_fn(Bar()) # Passes as expected
my_fn(Baz()) # Raises TypeError as expected
What changed in 2.0.0?
The main changes in version 2.0.0 revolve around migrating towards the standard python typing hint process and away from the original type_enfoced type hints (as type enforced was originally created before the |
operator was added to python).
- Support for python3.10 has been dropped.
- List based union types are no longer supported.
- For example
[int, float]
is no longer a supported type hint. - Use
int|float
ortyping.Union[int, float]
instead.
- For example
- Dict types now require two types to be specified.
- The first type is the key type and the second type is the value type.
- For example,
dict[str, int|float]
ordict[int, float]
are valid types.
- Tuple types now allow for
N
types to be specified.- Each item refers to the positional type of each item in the tuple.
- Support for ellipsis (
...
) is supported if you only specify two types and the second is the ellipsis type.- For example,
tuple[int, ...]
ortuple[int|str, ...]
are valid types.
- For example,
- Note: Unions between two tuples are not supported.
- For example,
tuple[int, str] | tuple[str, int]
will not work.
- For example,
- Constraints and Literals can now be stacked with unions.
- For example,
int | Constraint(ge=0) | Constraint(le=5)
will require any passed values to be integers that are greater than or equal to0
and less than or equal to5
. - For example,
Literal['a', 'b'] | Literal[1, 2]
will require any passed values that are equal (==
) to'a'
,'b'
,1
or2
.
- For example,
- Literals now evaluate during the same time as type checking and operate as OR checks.
- For example,
int | Literal['a', 'b']
will validate that the type is an int or the value is equal to'a'
or'b'
.
- For example,
- Constraints are still are evaluated after type checking and operate independently of the type checking.
Development
Running Tests, Prettifying Code, and Updating Docs
Make sure Docker is installed and running.
- Create a docker container and drop into a shell
./run.sh
- Run all tests (see ./utils/test.sh)
./run.sh test
- Prettify the code (see ./utils/prettify.sh)
./run.sh prettify
Update the docs (see ./utils/docs.sh)
./run.sh docs
Note: You can and should modify the
Dockerfile
to test different python versions.
1""" 2# Type Enforced 3[](https://badge.fury.io/py/type_enforced) 4[](https://opensource.org/licenses/MIT) 5[](https://pypi.org/project/type_enforced/) 6 7A pure python (no special compiler required) type enforcer for type annotations. Enforce types in python functions and methods. 8 9# Setup 10 11Make sure you have Python 3.11.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/). 12 13- Unsupported python versions can be used, however newer features will not be available. 14 - For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported) 15 - For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported) 16 - For 3.9: use type_enforced<=1.9.0 (`staticmethod`, union with `|` and `from __future__ import annotations` typechecking are not supported) 17 - For 3.10: use type_enforced<=1.10.2 (`from __future__ import annotations` may cause errors (EG: when using staticmethods and classmethods)) 18 19### Installation 20 21``` 22pip install type_enforced 23``` 24 25## Basic Usage 26```py 27import type_enforced 28 29@type_enforced.Enforcer(enabled=True, strict=True) 30def my_fn(a: int , b: int | str =2, c: int =3) -> None: 31 pass 32``` 33- Note: `enabled=True` by default if not specified. You can set `enabled=False` to disable type checking for a specific function, method, or class. This is useful for a production vs debugging environment or for undecorating a single method in a larger wrapped class. 34- Note: `strict=True` by default if not specified. You can set `strict=False` to disable exceptions being raised when type checking fails. Instead, a warning will be printed to the console. 35 36## Getting Started 37 38`type_enforcer` contains a basic `Enforcer` wrapper that can be used to enforce many basic python typing hints. [Technical Docs Here](https://connor-makowski.github.io/type_enforced/type_enforced/enforcer.html). 39 40`Enforcer` can be used as a decorator for functions, methods, and classes. It will enforce the type hints on the function or method inputs and outputs. It takes in the following optional arguments: 41 42- `enabled` (True): A boolean to enable or disable type checking. If `True`, type checking will be enforced. If `False`, type checking will be disabled. 43- `strict` (True): A boolean to enable or disable type mismatch exceptions. If `True` exceptions will be raised when type checking fails. If `False`, exceptions will not be raised but instead a warning will be printed to the console. 44 45`type_enforcer` currently supports many single and multi level python types. This includes class instances and classes themselves. For example, you can force an input to be an `int`, a number `int | float`, an instance of the self defined `MyClass`, or a even a vector with `list[int]`. Items like `typing.List`, `typing.Dict`, `typing.Union` and `typing.Optional` are supported. 46 47You can pass union types to validate one of multiple types. For example, you could validate an input was an int or a float with `int | float` or `typing.Union[int, float]`. 48 49Nesting is allowed as long as the nested items are iterables (e.g. `typing.List`, `dict`, ...). For example, you could validate that a list is a vector with `list[int]` or possibly `typing.List[int]`. 50 51Variables without an annotation for type are not enforced. 52 53## Why use Type Enforced? 54 55- `type_enforced` is a pure python type enforcer that does not require any special compiler or preprocessor to work. 56- `type_enforced` uses the standard python typing hints and enforces them at runtime. 57 - This means that you can use it in any python environment (3.11+) without any special setup. 58- `type_enforced` is designed to be lightweight and easy to use, making it a great choice for both small and large projects. 59- `type_enforced` supports complex (nested) typing hints, union types, and many of the standard python typing functions. 60- Other Options: 61 - `Pydantic`: 62 - A great all around option. 63 - Its ~40% slower at scale than type enforced for larger object verifications with complex (nested) types. 64 - It is ~30% faster for verifications of smaller objects. 65 - It is designed for data classes 66 - We have found it to be very consistent with type checking including complex (nested) type hints. 67 - `Beartype`: 68 - The fastest option we tested by far (close to O(n) time complexity). 69 - Is great for simple type checking and we highly recommend it for that use case. 70 - Is inconsistent at catching errors with complex (nested) type hints (at least in beartype 0.21.0) 71 - EG: Running the same data over a loop randomly fails to catch type errors. 72 - `Typeguard`: 73 - A useable option. 74 - ~100% slower than type enforced for simple type checking. 75 - Does not support complex (nested) type hints. 76 - `Enforce`: 77 - Has not been updated since 2017. 78 - We were unable to get working with python 3.13. 79- Note: See the [benchmarks](https://github.com/connor-makowski/type_enforced/blob/main/benchmark.md) for more information on the performance of each type checker. 80 81## Supported Type Checking Features: 82 83- Function/Method Input Typing 84- Function/Method Return Typing 85- Dataclass Typing 86- All standard python types (`str`, `list`, `int`, `dict`, ...) 87- Union types 88 - typing.Union 89 - `|` separated items (e.g. `int | float`) 90- Nested types (e.g. `dict[str, int]` or `list[int|float]`) 91 - Note: Each parent level must be an iterable 92 - Specifically a variant of `list`, `set`, `tuple` or `dict` 93 - Note: `dict` requires two types to be specified (unions count as a single type) 94 - The first type is the key type and the second type is the value type 95 - e.g. `dict[str, int|float]` or `dict[int, float]` 96 - Note: `list` and `set` require a single type to be specified (unions count as a single type) 97 - e.g. `list[int]`, `set[str]`, `list[float|str]` 98 - Note: `tuple` Allows for `N` types to be specified 99 - Each item refers to the positional type of each item in the tuple 100 - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type 101 - e.g. `tuple[int, ...]` or `tuple[int|str, ...]` 102 - Note: Unions between two tuples are not supported 103 - e.g. `tuple[int, str] | tuple[str, int]` will not work 104 - Deeply nested types are supported too: 105 - `dict[dict[int]]` 106 - `list[set[str]]` 107- Many of the `typing` (package) functions and methods including: 108 - Standard typing functions: 109 - `List` 110 - `Set` 111 - `Dict` 112 - `Tuple` 113 - `Union` 114 - `Optional` 115 - `Any` 116 - `Sized` 117 - Essentially creates a union of: 118 - `list`, `tuple`, `dict`, `set`, `str`, `bytes`, `bytearray`, `memoryview`, `range` 119 - Note: Can not have a nested type 120 - Because this does not always meet the criteria for `Nested types` above 121 - `Literal` 122 - Only allow certain values to be passed. Operates slightly differently than other checks. 123 - e.g. `Literal['a', 'b']` will require any passed values that are equal (`==`) to `'a'` or `'b'`. 124 - This compares the value of the passed input and not the type of the passed input. 125 - Note: Multiple types can be passed in the same `Literal` as acceptable values. 126 - e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 127 - Note: If type is a `str | Literal['a', 'b']` 128 - The check will validate that the type is a string or the value is equal to `'a'` or `'b'`. 129 - This means that an input of `'c'` will pass the check since it matches the string type, but an input of `1` will fail. 130 - Note: If type is a `int | Literal['a', 'b']` 131 - The check will validate that the type is an int or the value is equal to `'a'` or `'b'`. 132 - This means that an input of `'c'` will fail the check, but an input of `1` will pass. 133 - Note: Literals stack when used with unions. 134 - e.g. `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 135 - `Callable` 136 - Essentially creates a union of: 137 - `staticmethod`, `classmethod`, `types.FunctionType`, `types.BuiltinFunctionType`, `types.MethodType`, `types.BuiltinMethodType`, `types.GeneratorType` 138 - Note: Other functions might have support, but there are not currently tests to validate them 139 - Feel free to create an issue (or better yet a PR) if you want to add tests/support 140- `Constraint` validation. 141 - This is a special type of validation that allows passed input to be validated. 142 - Standard and custom constraints are supported. 143 - This is useful for validating that a passed input is within a certain range or meets a certain criteria. 144 - Note: Constraints stack when used with unions. 145 - e.g. `int | Constraint(ge=0) | Constraint(le=5)` will require any passed values to be integers that are greater than or equal to `0` and less than or equal to `5`. 146 - Note: The constraint is checked after type checking occurs and operates independently of the type checking. 147 - This operates differently than other checks (like `Literal`) and is evaluated post type checking. 148 - For example, if you have an annotation of `str | Constraint(ge=0)`, this will always raise an exception since if you pass a string, it will raise on the constraint check and if you pass an integer, it will raise on the type check. 149 - Note: See the example below or technical [constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#Constraint) and [generic constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint) docs for more information. 150 ``` 151 152## Interactive Example 153 154```py 155>>> import type_enforced 156>>> @type_enforced.Enforcer 157... def my_fn(a: int , b: int|str =2, c: int =3) -> None: 158... pass 159... 160>>> my_fn(a=1, b=2, c=3) 161>>> my_fn(a=1, b='2', c=3) 162>>> my_fn(a='a', b=2, c=3) 163Traceback (most recent call last): 164 File "<python-input-2>", line 1, in <module> 165 my_fn(a='a', b=2, c=3) 166 ~~~~~^^^^^^^^^^^^^^^^^ 167 File "/app/type_enforced/enforcer.py", line 233, in __call__ 168 self.__check_type__(assigned_vars.get(key), value, key) 169 ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 170 File "/app/type_enforced/enforcer.py", line 266, in __check_type__ 171 self.__exception__( 172 ~~~~~~~~~~~~~~~~~~^ 173 f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead." 174 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 175 ) 176 ^ 177 File "/app/type_enforced/enforcer.py", line 188, in __exception__ 178 raise TypeError(f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}") 179TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` with value `a` instead. 180``` 181 182## Nested Examples 183```py 184import type_enforced 185import typing 186 187@type_enforced.Enforcer 188def my_fn( 189 a: dict[str,dict[str, int|float]], # Note: For dicts, the key is the first type and the value is the second type 190 b: list[typing.Set[str]] # Could also just use set 191) -> None: 192 return None 193 194my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success 195 196my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error => 197# TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a['i']['j']`. Expected one of the following `[<class 'int'>, <class 'float'>]` but got `<class 'str'>` with value `k` instead. 198``` 199 200## Class and Method Use 201 202Type enforcer can be applied to methods individually: 203 204```py 205import type_enforced 206 207class my_class: 208 @type_enforced.Enforcer 209 def my_fn(self, b:int): 210 pass 211``` 212 213You can also enforce all typing for all methods in a class by decorating the class itself. 214 215```py 216import type_enforced 217 218@type_enforced.Enforcer 219class my_class: 220 def my_fn(self, b:int): 221 pass 222 223 def my_other_fn(self, a: int, b: int | str): 224 pass 225``` 226 227You can also enforce types on `staticmethod`s and `classmethod`s if you are using `python >= 3.10`. If you are using a python version less than this, `classmethod`s and `staticmethod`s methods will not have their types enforced. 228 229```py 230import type_enforced 231 232@type_enforced.Enforcer 233class my_class: 234 @classmethod 235 def my_fn(self, b:int): 236 pass 237 238 @staticmethod 239 def my_other_fn(a: int, b: int | str): 240 pass 241``` 242 243Dataclasses are suported too. 244 245```py 246import type_enforced 247from dataclasses import dataclass 248 249@type_enforced.Enforcer 250@dataclass 251class my_class: 252 foo: int 253 bar: str 254``` 255 256You can skip enforcement if you add the argument `enabled=False` in the `Enforcer` call. 257- This is useful for a production vs debugging environment. 258- This is also useful for undecorating a single method in a larger wrapped class. 259- Note: You can set `enabled=False` for an entire class or simply disable a specific method in a larger wrapped class. 260- Note: Method level wrapper `enabled` values take precedence over class level wrappers. 261```py 262import type_enforced 263@type_enforced.Enforcer 264class my_class: 265 def my_fn(self, a: int) -> None: 266 pass 267 268 @type_enforced.Enforcer(enabled=False) 269 def my_other_fn(self, a: int) -> None: 270 pass 271``` 272 273## Validate with Constraints 274Type enforcer can enforce constraints for passed variables. These constraints are validated after any type checks are made. 275 276To enforce basic input values are integers greater than or equal to zero, you can use the [Constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#Constraint) class like so: 277```py 278import type_enforced 279from type_enforced.utils import Constraint 280 281@type_enforced.Enforcer() 282def positive_int_test(value: int |Constraint(ge=0)) -> bool: 283 return True 284 285positive_int_test(1) # Passes 286positive_int_test(-1) # Fails 287positive_int_test(1.0) # Fails 288``` 289 290To enforce a [GenericConstraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint): 291```py 292import type_enforced 293from type_enforced.utils import GenericConstraint 294 295CustomConstraint = GenericConstraint( 296 { 297 'in_rgb': lambda x: x in ['red', 'green', 'blue'], 298 } 299) 300 301@type_enforced.Enforcer() 302def rgb_test(value: str | CustomConstraint) -> bool: 303 return True 304 305rgb_test('red') # Passes 306rgb_test('yellow') # Fails 307``` 308 309 310 311## Validate class instances and classes 312 313Type enforcer can enforce class instances and classes. There are a few caveats between the two. 314 315To enforce a class instance, simply pass the class itself as a type hint: 316```py 317import type_enforced 318 319class Foo(): 320 def __init__(self) -> None: 321 pass 322 323@type_enforced.Enforcer 324class my_class(): 325 def __init__(self, object: Foo) -> None: 326 self.object = object 327 328x=my_class(Foo()) # Works great! 329y=my_class(Foo) # Fails! 330``` 331 332Notice how an initialized class instance `Foo()` must be passed for the enforcer to not raise an exception. 333 334To enforce an uninitialized class object use `typing.Type[classHere]` on the class to enforce inputs to be an uninitialized class: 335```py 336import type_enforced 337import typing 338 339class Foo(): 340 def __init__(self) -> None: 341 pass 342 343@type_enforced.Enforcer 344class my_class(): 345 def __init__(self, object_class: typing.Type[Foo]) -> None: 346 self.object = object_class() 347 348y=my_class(Foo) # Works great! 349x=my_class(Foo()) # Fails 350``` 351 352By default, type_enforced will check for subclasses of a class when validating types. This means that if you pass a subclass of the expected class, it will pass the type check. 353 354Note: Uninitialized class objects that are passed are not checked for subclasses. 355 356```py 357import type_enforced 358 359class Foo: 360 pass 361 362class Bar(Foo): 363 pass 364 365class Baz: 366 pass 367 368@type_enforced.Enforcer 369def my_fn(custom_class: Foo): 370 pass 371 372my_fn(Foo()) # Passes as expected 373my_fn(Bar()) # Passes as expected 374my_fn(Baz()) # Raises TypeError as expected 375``` 376 377## What changed in 2.0.0? 378The main changes in version 2.0.0 revolve around migrating towards the standard python typing hint process and away from the original type_enfoced type hints (as type enforced was originally created before the `|` operator was added to python). 379- Support for python3.10 has been dropped. 380- List based union types are no longer supported. 381 - For example `[int, float]` is no longer a supported type hint. 382 - Use `int|float` or `typing.Union[int, float]` instead. 383- Dict types now require two types to be specified. 384 - The first type is the key type and the second type is the value type. 385 - For example, `dict[str, int|float]` or `dict[int, float]` are valid types. 386- Tuple types now allow for `N` types to be specified. 387 - Each item refers to the positional type of each item in the tuple. 388 - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type. 389 - For example, `tuple[int, ...]` or `tuple[int|str, ...]` are valid types. 390 - Note: Unions between two tuples are not supported. 391 - For example, `tuple[int, str] | tuple[str, int]` will not work. 392- Constraints and Literals can now be stacked with unions. 393 - For example, `int | Constraint(ge=0) | Constraint(le=5)` will require any passed values to be integers that are greater than or equal to `0` and less than or equal to `5`. 394 - For example, `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 395- Literals now evaluate during the same time as type checking and operate as OR checks. 396 - For example, `int | Literal['a', 'b']` will validate that the type is an int or the value is equal to `'a'` or `'b'`. 397- Constraints are still are evaluated after type checking and operate independently of the type checking. 398 399# Development 400## Running Tests, Prettifying Code, and Updating Docs 401 402Make sure Docker is installed and running. 403 404- Create a docker container and drop into a shell 405 - `./run.sh` 406- Run all tests (see ./utils/test.sh) 407 - `./run.sh test` 408- Prettify the code (see ./utils/prettify.sh) 409 - `./run.sh prettify` 410- Update the docs (see ./utils/docs.sh) 411 - `./run.sh docs` 412 413- Note: You can and should modify the `Dockerfile` to test different python versions.""" 414from .enforcer import Enforcer, FunctionMethodEnforcer