Dynamic Strong Typing in Python
A quick and dirty use of Python function annotations to apply strong typing to functions and methods at runtime. If inputs or outputs of a function do not match the type specified in the function annotation, a type error will be thrown. This strong typing is applied to individual functions/classes through a decorator. As an example of usage:
import dynamight
@dynamight.strong
def is_able_to_pass(name : str, quest : str, favourite_colour : str) -> bool:
if name == "Lancelot" and quest == "seek the holy grail":
followup_favourite_colour = input("...")
if favourite_colour == followup_favourite_colour:
return True
return False
bridge_keepers_decision = is_able_to_pass("Lancelot", "Really likes bridges", 5)
# Will raise a TypeError when favourite_colour receives an int instead of a str
Dynamight can be installed through Pip:
pip install dynamight
This is something I whipped together fairly quickly to experiment with function annotations and didn't want to get too bogged down in; if you're looking for an actual solution for static typing in Python, you probably want MyPy at "compile" time or Enforce at runtime.
Despite this there are a few features of my approach I'm quite proud of:
-
Strong typing on the output of generator functions (functions which
yield
). In a somewhat hacky way due to not being able to modify the generator returned by a generator function, the generator returned is replaced with a similar iterable class which tries to copy the original generator (but validates the output of__next__()
). -
Flexible lists of possible types - being able to type a parameter as
[int, str]
as well as juststr
, so either a string or an integer value will be allowed.
This should sum up all the key things you need to know about supported function annotations and applying the decorator:
class SomeClass():
@staticmethod
@dynamight.strong
def happy_function(name : str):
# this ordering works correctly
pass
@dynamight.strong
@staticmethod
def sad_function(name : str):
# this ordering will cause an exception when the module is initializing
pass
An instance of a subclass will be accepted as well as an instance of the class
@dynamight.strong
def some_function(anything : object):
pass
# This wont throw a TypeError as str is a subclass of object
some_function("something")
However currently specifying a type of item within a collection is not implemented
bool, str, int, list, dict, float, object, tuple, set, None
# also custom classes will work too
@dynamight.strong
def some_function() -> None:
pass
# None will be converted to type(None) behind the scenes, so use None.
It's not going to have any effect on functions without annotations though
@dynamight.strong
class Hogwarts():
# will be wrapped but has no effect
def add_troll_to_dungeon(self, troll, dungeon):
pass
# will be wrapped and type checked when called
def move_stairs_around(self, angle : int) -> None:
pass
Trying to apply typing to them is useless but it's not going to break your functions with them in
Feedback appreciated! This was a project I worked on with an aim to see what I could get just done and released ASAP, rather than starting another programming project I don't get around to finishing.
A few other ideas I had for this but didn't have time for are:
- Using the
typing
library to check for collections of typed items as function annotations. - Since strong typing doesn't seem as necessary in Python/isn't integral to the language, another idea I had was something like a wrapper which performs a conversion on inputs which don't match the type in the annotation, to the specified type, through some sort of graph of transforms. Would be a kind of defensive programming.