2. Type Annotations For Functions
By Bernd Klein. Last modified: 13 Jul 2023.
Type annotations supply also a specific syntax to indicate the expected types of function parameters and the return type of functions.
A simple Pyhon example of a function with type hints:
%%writefile example.py
def greeting(name: str) -> str:
return 'Hello ' + name
# We call the function with a string, which is okay:
greeting("World!")
# an integer is an illegal argument, it should a str:
greeting(3)
# A bytes string is not the right kind of a string:
greeting(b'Alice')
def bad_greeting(name: str) -> str:
return 'Hello ' * name # Unsupported operand types for * ("str" and "str")
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
example.py:9: error: Argument 1 to "greeting" has incompatible type "int"; expected "str" example.py:12: error: Argument 1 to "greeting" has incompatible type "bytes"; expected "str" example.py:15: error: Unsupported operand types for * ("str" and "str") Found 3 errors in 1 file (checked 1 source file)
The following Python function of an annotated function shows a slightly more extended function definition:
def greeting(name: str, phrase: str='hello') -> str:
return phrase + ' ' + name
We can see the annotations of a function by looking at the __annotations__
attribute:
greeting.__annotations__
OUTPUT:
{'name': str, 'phrase': str, 'return': str}
The __defaults__
attribute shows us the default values of the function:
greeting.__defaults__
OUTPUT:
('hello',)
These function annotations are available at runtime through the __annotations__
attribute. Yet, there will be no type checking at runtime. Checks have to be done via MyPy
or other type checkers.
Let's use MyPy on the previous example:
%%writefile example.py
def greeting(name: str, phrase: str='Hello') -> str:
return phrase + ' ' + name
print(greeting('Frank', 'Good evening'))
print(greeting('Olga'))
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
Now a type annotation example with a type violation:
%%writefile example.py
def greeting(name: str, phrase: str='Hello') -> str:
return phrase + ' ' + name
print(greeting('Frank', 42))
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
example.py:4: error: Argument 2 to "greeting" has incompatible type "int"; expected "str" Found 1 error in 1 file (checked 1 source file)
%%writefile example.py
from typing import List
def greet_all(names: List[str]) -> None:
for name in names:
print('Hello ' + name)
names: List[str]
names = ["Alice", "Bob", "Charlie"]
greet_all(names)
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!python example.py
OUTPUT:
Hello Alice Hello Bob Hello Charlie
%%writefile example.py
from typing import List
Vector = List[float]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
v1: Vector
v2: Vector
v1 = [3, 5, 6]
v2 = scale(3.1, v1)
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!python example.py
Example: Numerical Input
The following example addresses a common scenario encountered when reading an integer value using input
. While it works correctly for most cases, there is an issue when a user inputs a value like 5.0. Syntactically, 5.0 is a float
, but from a logical standpoint, it could be treated as an integer. When attempting to convert such input to an integer using int()
, a ValueError
exception is raised.
To accommodate users who would like these float-like inputs to be treated as integers, the example provides a solution. It incorporates a function that handles the conversion process. If the user input can be directly converted to an integer, it returns the integer value. However, if the input is a float, it rounds the number to the nearest integer using round()
and then returns it as an integer.
By employing this approach, the example not only addresses the issue of treating inputs like 5.0 as integers but also ensures that all float numbers are converted to integers by rounding them to the nearest whole number.
def get_rounded_input():
"""
Prompts the user to enter a number and returns an integer if the user
enters an integer, or the rounded value as an integer if the user
enters a float.
Returns:
int: The entered integer or the rounded value of the entered float.
"""
user_input = input("Please enter a number: ")
try:
number = int(user_input) # Try converting to int
return number
except ValueError:
try:
number = float(user_input) # Try converting to float
return round(number) # Return rounded value as int
except ValueError:
return None # Return None if input cannot be converted to a number
# Example usage:
result = get_rounded_input()
if result is not None:
print("Input:", result)
else:
print("Invalid input!")
OUTPUT:
Input: 4
In this code, the function get_rounded_input() prompts the user to enter a number. It first tries to convert the user input to an integer using int(user_input). If the conversion is successful, it returns the integer value.
If the conversion to an integer fails (raises a ValueError), the function tries to convert the user input to a float using float(user_input). If this conversion is successful, it rounds the float value using the round() function and returns the rounded value as an integer.
If both conversion attempts fail, the function returns None to indicate that the input could not be converted to a number.
You can use this function in your code and handle the return value accordingly. In the example usage provided, it checks if the result is not None before printing the input value. If the result is None, it indicates that the input was invalid.
def get_rounded_input() -> int:
"""
Prompts the user to enter a number and returns an integer if the user
enters an integer, or the rounded value as an integer if the user
enters a float.
Returns:
int: The entered integer or the rounded value of the entered float.
"""
user_input: str = input("Please enter a number: ")
try:
number: int = int(user_input) # Try converting to int
return number
except ValueError:
try:
number: float = float(user_input) # Try converting to float
return round(number) # Return rounded value as int
except ValueError:
return None # Return None if input cannot be converted to a number
# Example usage:
result: int = get_rounded_input()
if result is not None:
print("Input:", result)
else:
print("Invalid input!")
OUTPUT:
Input: 48
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Another Example
def calculate_interest(capital: float, interest_rate: float, years: int) -> float:
"""
Calculates the interest value after a specified number of years
based on the capital and interest rate.
Args:
capital (float): The initial capital amount.
interest_rate (float): The interest rate (in decimal form,
e.g., 0.05 for 5%).
years (int): The number of years.
Returns:
float: The interest value after the specified number of years.
"""
interest: float = capital * interest_rate * years
return interest
What is "wrong" with the previous code? What if somebody types calls the function with
calculate_interest(10000, 2, 10)
? Type checkers will not except it. So we use Union
to overcome this problem:
%%writefile example.py
from typing import Union
def calculate_interest(capital: Union[int, float], interest_rate: Union[int, float], years: int) -> float:
"""
Calculates the interest value after a specified number of years
based on the capital and interest rate.
Args:
capital (Union[int, float]): The initial capital amount.
interest_rate (Union[int, float]): The interest rate (in decimal
form, e.g., 0.05 for 5%).
years (int): The number of years.
Returns:
float: The interest value after the specified number of years.
"""
capital = float(capital)
interest_rate = float(interest_rate)
interest: float = capital * interest_rate * years
return interest
print(calculate_interest(10000, 2, 10))
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!python example.py
OUTPUT:
200000.0
Another Example
A function to convert degress Celsius to Fahrenheit.
%%writefile example.py
from typing import Union
def celsius_to_fahrenheit(celsius: Union[int, float]) -> Union[int, float]:
"""
Converts degrees Celsius to Fahrenheit.
Args:
celsius (Union[int, float]): The temperature in degrees Celsius.
Returns:
Union[int, float]: The temperature in degrees Fahrenheit.
"""
fahrenheit: Union[int, float] = celsius * 9/5 + 32
return fahrenheit
for c in [23.5, 19]:
print(c, celsius_to_fahrenheit(c))
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
%%writefile example.py
from typing import Union
def celsius_to_fahrenheit(celsius: Union[int, float, str]) -> Union[int, float]:
"""
Converts degrees Celsius to Fahrenheit.
Args:
celsius (Union[int, float, str]): The temperature in degrees Celsius.
Returns:
Union[int, float]: The temperature in degrees Fahrenheit.
"""
if isinstance(celsius, str):
celsius = float(celsius) # Convert string to float
fahrenheit: Union[int, float] = celsius * 9/5 + 32
return fahrenheit
for c in [23.5, 19]:
print(c, celsius_to_fahrenheit(c))
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
PEP 604 proposes overloading the | operator on types to allow writing Union[X, Y] as X | Y, and allows it to appear in isinstance and issubclass calls.
%%writefile example.py
def celsius_to_fahrenheit(celsius: int | float | str) -> int | float:
"""
Converts degrees Celsius to Fahrenheit.
Args:
celsius (Union[int, float, str]): The temperature in degrees Celsius.
Returns:
Union[int, float]: The temperature in degrees Fahrenheit.
"""
if isinstance(celsius, str):
celsius = float(celsius) # Convert string to float
fahrenheit: int | float = celsius * 9/5 + 32
return fahrenheit
for c in [23.5, 19]:
print(c, celsius_to_fahrenheit(c))
OUTPUT:
Overwriting example.py
!/home/bernd/anaconda3/envs/py3.10/bin/mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!/home/bernd/anaconda3/envs/py3.10/bin/python example.py
OUTPUT:
23.5 74.3 19 66.2
%%writefile example.py
celsius: int | float | str
OUTPUT:
Overwriting example.py
!/home/bernd/anaconda3/envs/py3.10/bin/mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!mypy --version
OUTPUT:
mypy 0.761
!/home/bernd/anaconda3/envs/py3.10/bin/mypy --version
OUTPUT:
mypy 0.981 (compiled: yes)
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Functions with Function Parameters
def mapping(func, values):
result = []
for value in values:
result.append(func(value))
return result
%%writefile example.py
from typing import Callable, Iterable, List, TypeVar, Union
def celsius_to_fahrenheit(celsius: int | float) -> int | float:
fahrenheit: int | float = celsius * 9/5 + 32
return fahrenheit
T = TypeVar('T')
U = TypeVar('U')
def mapping(func: Callable[[T], U], values: Iterable[T]) -> List[U]:
"""
Applies a given function to each element of the iterable and returns a list of the results.
Args:
func (Callable[[T], U]): The function to apply to each element.
values (Iterable[T]): The iterable containing the elements to be processed.
Returns:
List[U]: A list containing the results of applying the function to each element.
"""
result: List[U] = []
value: T
for value in values:
result.append(func(value))
return result
numbers: List[Union[int, float]] = [5, 9.4, 12, 4.8]
print(mapping(celsius_to_fahrenheit, numbers))
OUTPUT:
Overwriting example.py
!/home/bernd/anaconda3/envs/py3.10/bin/mypy example.py
OUTPUT:
Success: no issues found in 1 source file
!!/home/bernd/anaconda3/envs/py3.10/bin/python example.py
OUTPUT:
['[41.0, 48.92, 53.6, 40.64]']
Example: Compose Function
%%writefile example.py
from typing import Callable, TypeVar
T = TypeVar('T')
U = TypeVar('U')
V = TypeVar('V')
def compose_functions(f1: Callable[[T], U], f2: Callable[[U], V]) -> Callable[[T], V]:
"""
Returns a new function that applies f2 to the result of f1(x).
Args:
f1 (Callable[[T], U]): The first function.
f2 (Callable[[U], V]): The second function.
Returns:
Callable[[T], V]: The composed function.
"""
def composed_function(x: T) -> V:
return f2(f1(x))
return composed_function
def square(x: int) -> int:
return x ** 2
def add_one(x: int) -> int:
return x + 1
result_function = compose_functions(square, add_one)
result = result_function(5)
print(result) # Output: 26
OUTPUT:
Overwriting example.py
!mypy example.py
OUTPUT:
Success: no issues found in 1 source file
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Callable
Frameworks expecting callback functions of specific signatures might be type hinted using Callable[[Arg1Type, Arg2Type], ReturnType]
.
from collections.abc import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
pass
def async_query(on_success: Callable[[int], None],
on_error: Callable[[int, Exception], None]) -> None:
# Body
pass
async def on_update(value: str) -> None:
# Body
pass
callback: Callable[[str], str] = on_update
It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis for the list of arguments in the type hint: Callable[..., ReturnType]
.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.