How to convert the type of function's arguments in Python at runtime?
Converting the type according to type hints in python

Introduction
Most of us have run into a situation where we have to change the type of a parameter in function; either it is simple str -> int or something far more complex. But at some point, we'll be repeating the code too much just for converting the types of the parameter instead of actual logic which drastically increases the code length and decreases readability.
Example:
# Defining a function with Type Hints (I'll explain this below)
def greater(a: int, b: int) -> int:
# Converting from str to int
a = int(a)
b = int(b)
# This is equivalent of
# if a > b:
# return a
# else:
# return b
return a if a > b else
# Taking input
a = input('Enter a number')
b = input('Enter another number')
# Passing the arguements to the function
greatest = greater(a, b)
print(greatest)
It's not that big of a problem for a single function, but assume that you're creating a python module that accepts certain data types, it'd be tiresome for users to convert the data type every time before passing to your module or you'd have to repeat your code just for converting the data types.
What are Type Hints?
Type hints in python is a way of telling python the type of data you'd want to accept and return. It is mostly used in functions, but you can use it while declaring variables as well.
Why use Type Hints?
- Using type hints will provide better autocompletion in your favorite IDE.
- You won't have to check the documentation for knowing the return type and the data type of parameters in a function.
- Type Hints make it easier to write and generate documentation.
Python being a dynamically typed language, type hints don't interfere with the core principles of python. Type hints won't affect the running of the code during runtime. Type hinting as str and passing int is perfectly okay.
NOTE: Type hint is not something that executes. Python ignores your type hints while running your code.
How to use Type Hints?
Code without Type Hints
def func_a(a, b):
return a + b
Code with Type Hints
def func_a(a: int, b: int) -> int:
return a + b
Explanation:
a: intmeans that the parameter a needs to be ainttype.-> intmeans that the functionfunc_areturns a value with typeint
The typing python module
This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable, TypeVar, and Generic.
What is typing.Any?
Any is a special type indicating an unconstrained type. Every type is compatible with Any. Any is compatible with every type. It is used when you don't know the exact type of data, and the type of data may be anything.
What is typing.Callable?
Callable is used to denote something that can be called. Most often, it is used to denote a function. Callable[[int, int], str] means that the provided function will take 2 integer arguments and return a value with the type of str. Checkout PEP 677 for more info.
What is typing.TypeVar?
TypeVar is used for creating dynamic type hints. It is mainly used by static type checkers and IDE for proper autocompletion. Check out Python Docs for more info.
For a detailed introduction to type hints, see PEP 483.
Converting the type on RunTime
Creating a function for conversion
This is a good solution, but it's only viable if there are only a few parameters in a function.
Example:
import typing as t
# Creating a TypeVar _T
_T = TypeVar('_T')
# Creating a function that converts the type
def convert_type(arg: t.Any, converter: _T) -> _T:
return converter(arg)
# Function that sqaures the given number
def square(number: t.Any) -> int:
# Converting str -> int
number = convert_type(number, int)
return number ** 2
Code Explanation:
- Created a TypeVar (
_T). You can useTypeVarfor dynamic type hints.convert_typefunction takes theconverterwith type_Tand the returns value of type_T. So if we passstrinconverter, the function would showstras the return value. - Created the function
convert_typewhich acceptsargandconverteras it's 2 parameters. - Created a function square that squares the number. But as the passed value is
str,convert_type(number, int)is used to convert the number fromstrtoint.
The code seems good to use everywhere, but once we increase the number of parameters in the function, it starts to have the same problems like repetition of code and less readability as mentioned above.
Creating a decorator for conversion
What is a decorator?
Python has an interesting feature called decorators to add functionality to an existing code. This is also called metaprogramming because a part of the program tries to modify another part of the program at runtime.
Simply put, decorators are just a sugar-coated version of using one function on top of another function.
Code Without Decorator
import typing as t
# Creating a function which takes another function
# as the parameter
def make_pretty(func: t.Callable) -> t.Callable:
# Running the passed function after printing
print("I got prettier")
func()
# Defining the actual function
def print_something() -> None:
print('something')
# Passing the actual function inside make_pretty()
make_pretty(print_something)
Code With Decorator
import typing as t
# Creating the decorator make_pretty
# It takes the function automatically as the
# paramater if you use the @decorator syntax
def make_pretty(func: t.Callable) -> t.Callable:
# This inner function takes all the arguments (args)
# and keyword arguments (kwargs) passed to the
# function we decorate. It can be None as well
def inner_function(*args, **kwargs):
# We can do anything we want here
print("I got prettier")
# Finally running the passed function
func(*args, **kwargs)
# Returning the inner function so that
# decorator can use it properly
return inner_function
# using @make_pretty decorates print_something()
# function with make_pretty() function
@make_pretty
def print_something() -> None:
print("something")
# Actually running the function
print_something()
Explanation:
- Usage of
function(*args)means if we callfunction(1, 2, 3, 4, 5), the value of the arguementargswould be(1, 2, 3, 4, 5){Tuple} and vice-versa. - Usage of
function(**kwargs)means if we callfunction(a=1, b=2, c=3), the value of the arguementkwargswould be{a: 1, b: 2, c: 3}{Dictionary} and vice-versa. - If nothing is passed, the values of
argsandkwargswill remainNone. @is used to denote the usage of adecorator.
Getting type hints of a function
For converting the argument type according to the type hints in runtime, we'll have to get the type hints of the function.
from typing import get_type_hints
def add_1(a: int) -> int:
return a + 1
print(get_type_hints(add_1))
OUTPUT:
{'a': <class 'int'>, 'return': <class 'int'>}
Creating a decorator for type conversion
import typing as t
# Creating a TypeVar for dynamic type hinting in the function
_T = t.TypeVar("_T")
# Creating a function for converting type_conversion
def type_conversion(value: t.Any, value_type: type[_T]) -> _T:
# If the value's type has a function named 'convert_types'
# than we call the function and pass the value to it
if hasattr(value_type, "convert_types"):
return value_type.convert_types(value)
# Returning the value if it's of Any type
elif value_type is t.Any:
return value
# if it doesn't we return the value by forcefully
# trying to convert
return value_type(value)
# Creating a decorator to convert types
def convert_types(func: t.Callable) -> t.Callable:
def inner_function(*args, **kwargs) -> t.Any:
# Getting the type hints of the function
annotations = t.get_type_hints(func).copy()
# Removing the 'return' from the dictioary
# as it is unnecessary right now
annotations.pop("return")
# Getting only the values of annotations for
# looping with the args of the function
args_annotations = annotations.values()
new_args = list(args)
# Looping through the annotations and args
for index, (arg, arg_type) in enumerate(zip(args, args_annotations)):
# If arg is not of the required type, we convert it
if not isinstance(arg, arg_type):
new_args[index] = type_conversion(arg)
for arg_value, (arg_name, arg_type) in zip(kwargs, annotations.items()):
# If arg is not of the required type, we convert it
if not isinstance(arg_value, arg_type):
kwargs[arg_name] = type_conversion(arg_value)
return func(*new_args, **kwargs)
return inner_function
# Creating a simple function to add 1
@convert_types
def add_1(a: int) -> int:
return a + 1
# Passing a string to the function
print(add_1("1234"))
OUTPUT:
1235
Explanation of type checker code
- We created a function
type_conversionfor converting the type of the value according to the passed required type. This function first checks if the type has a methodconvert_typesand if it does, the value is passed onto that method. Else if the value is of typetyping.Any, only the value is returned else the value is forcefully tried to convert. - The
convert_typesdecorator gets the type hints of the function, checks if the type hints are matching with the value types, and it's not, the values' type is changed. zipfunction is used to combine 2 iterables. Zipping 2 tuples;(1, 2, 3)and('a', 'b', 'c')will produce a zip object with value((1, 'a'), (2, 'b'), (3, 'c')).enumeratefunction is used to get the value and index while looping through the iterable at once.isinstanceis used to check if the value is an instance of the passed class.
Making CustomClass compatible with type checker
While it may seem like the above type checker only supports the built-in data types in python. It's not actually true. We can make our own classes compatible with the type checker by simply adding a convert_types @classmethod.
import typing as t
class MyOwnClass:
@classmethod
def convert_types(cls, value: t.Any) -> 'MyOwnClass':
return str(value)
NOTE: Yes, you can type-hint as strings as well instead of actual objects. You can use
-> 'int'instead of-> intas per your use case.
Now you can type-hint your parameters with MyOwnClass and it'd still work.
When not to use this decorator?
- If you have something complex like
t.Uniontypes; it'd be better to use pydantic. - If you're writing something that is time-sensitive, it'd be better to stay away from runtime type checking, or this specific implementation of runtime type checking, as it loops 2 times before even executing your function.
Resources for further reading
Conclusion
Python type hints were a game changer to developer experience ever since it was introduced in python 3.5. Python runtime checking is a fun topic to experiment with and might have a very wide range of use cases. But one must use it with extreme caution as it may introduce unexpected slowdowns and bugs to the code.



