Python is a dynamically typed language, meaning you do not need to specify the type of variables, parameters or return values. This is determined during program execution based on the values assigned to the variable or passed as the argument.
Python introduced type hints or static typing with version 3.5, allowing developers to declare the data type of variables, parameters, etc.
What is Type Hint?
x: int = 5
y: str = "5"
In this example, the variable x
expects an integer value while the variable y
expects a string value.
This is called type hint or static typing where you specify the expected data type for a variable, parameter or return value of a function.
Python has a different way of declaring type hints for the variables, return values, collections, etc.
Consider the following example:
def circle(radius: float) -> str:
area = 3.14 * radius ** 2
return f"Area of circle: {area}"
In this example, the function circle accepts an argument radius
which is expected to be a float value as indicated by type hint radius: float
, and the return value of this function is expected to be a string, as indicated by the -> str
hint.
Performing a Check
What if you pass the argument of a different type which is not expected? Consider the function below:
def date(dd: str, mm: str, yyyy: str) -> str:
return f"Current Date: {dd}-{mm}-{yyyy}"
curr_date = date(22, 9, 2024) # expected 'str' got 'int'
print(curr_date)
The function date()
accepts three arguments and expects all of them to be a string value, however, integer values are supplied when the function is called.
What do you think? Will this program throw an error? Well, it looks like but Python or specifically interpreter completely ignores the type hints as this is not its purpose.
Current Date: 22-9-2024
The type of arguments was decided in the runtime that is why no error is thrown, however, you may see a warning in your IDE or code editor.
Annotating Multiple Return Types
In this section, you'll learn how to annotate multiple return types for a single value of alternative types and multiple values of different types.
Alternative Types for a Return Value
def cart(item: str) -> str | None:
if item == "":
return None
return "Item added in the cart"
The function cart()
accepts an argument item and returns None
if it is not supplied, otherwise, it returns a string.
To represent multiple return types for a function, the (|
) pipe can be used. It means either str
or None
. So, when you call the function, it indicates that the return value of the function can be a str
or None
.
You can also use typing.Union
to accomplish the same task you've done using the (|
).
from typing import Union
def cart(item: str) -> Union[None, str]:
if item == "":
return None
return "Item added in the cart"
The Union[None, str]
is equivalent to None | str
. None | str
is a shorthand and it is a recommended way.
Multiple Return Values of Different Types
def cart(item: str, quantity: int) -> dict[str, int] | None:
if item == "" or quantity == 0:
return None
return {item: quantity}
The function cart()
has been updated to accept an additional argument quantity
and returns None
if any of the arguments are left empty, otherwise, it returns a dictionary containing a key (str
) and value (int
).
If you look at the type hint for the return value, this time different types (str
and int
) are expected to return in a dictionary (dict
).
You can do this in the following way.
from typing import Union, Dict
def cart(item: str, quantity: int) -> Union[Dict[str, int], None]:
if item == "" or quantity == 0:
return None
return {item: quantity}
You can use Mapping
instead of Dict
to represent the dictionary.
from typing import Union, Mapping
def cart(item: str, quantity: int) -> Union[Mapping[str, int], None]:
if item == "" or quantity == 0:
return None
return {item: quantity}
But all this seems a bit verbose so you can go for shorthand syntax.
Type Hinting Functions
from collections.abc import Callable
def apply_cart(
func: Callable[[str, int], dict[str, int]],
item: str,
quantity: int
) -> dict[str, int]:
return func(item, quantity)
def cart(item: str, quantity: int) -> dict[str, int] | None:
if item == "" or quantity == 0:
return None
return {item: quantity}
The function apply_cart()
accepts a callable object (which can be a function or any other callable object) and two arguments (item
and quantity
) and returns a dictionary containing the key and value.
The Callable
type hint provides a list of arguments ([str, int]
) that the callable object accepts. In this case, func()
expects strings and integers. Callable's second parameter is the return value (dict[str, int]
), which is a dictionary.
The second function, cart()
, is identical to the previous example and returns a dictionary if everything is correct.
cart_item = apply_cart(cart, "Mouse", 2)
print(cart_item)
--------------------
{'Mouse': 2}
The apply_cart()
function is invoked with the cart
(a function) and two arguments ('Mouse'
, 2
).
The apply_cart()
function calls the cart()
function with the given arguments and returns a result.
Here's an optimisation, if you have many arguments of different types, you can use ellipsis (...
) rather than passing different input types.
The ellipsis literal (...
) indicates that callable can accept any arbitrary list of arguments.
from collections.abc import Callable
from typing import TypeVar, Any
T = TypeVar("T")
def apply_cart(
func: Callable[..., T],
*args: Any
) -> T:
return func(*args)
The apply_cart()
function has been updated and now accepts an arbitrary list of arguments of any type ([..., T]
) as well as variadic argument (*args
) of any type (Any
).
The first parameter in the Callable
is an ellipsis (...
), suggesting that any arbitrary parameter list is permitted.
The second parameter is a type variable (T = TypeVar("T")
) that can work with any type. It indicates that the callable can accept any type and return an element of that type.
You can also use a parameter specification variable (ParamSpec
) instead of an ellipsis to make callable objects accept any number of positional or keyword arguments.
from collections.abc import Callable
from typing import TypeVar, ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
def apply_cart(
func: Callable[P, T],
*args: P.args
) -> T:
return func(*args)
In this case, the first parameter of Callable
is a parameter specification variable (P = ParamSpec("P")
) indicating arbitrary list of arguments is acceptable. The second parameter (T
) indicates that any type is acceptable.
The function apply_cart()
also accepts a variadic argument (*args
) of type P.args
that represents a tuple of any number and type of positional arguments. To annotate **kwargs
, P.kwargs
must be used that represents the mapping of keyword parameters to their values.
Instead of using the ellipsis literal, you may now utilise ParamSpec
and TypeVar
to enable callable objects to accept any number of positional or keyword arguments of any type.
The code is further updated by adding **kwargs
, which allows the function to accept keyword arguments of arbitrary length.
from collections.abc import Callable
from typing import TypeVar, ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
def apply_cart(
func: Callable[P, T],
*args: P.args,
**kwargs: P.kwargs
) -> T:
return func(*args, **kwargs)
def cart(item: str, quantity: int) -> dict[str, int] | None:
if item == "" or quantity == 0:
return None
return {item: quantity}
cart_item = apply_cart(cart, "Mouse", quantity=2)
print(cart_item)
Type Hinting Iterables
Consider the following function that takes a list of names and sorts them in ascending order.
def sort_student(names: list[str]) -> None:
sorting = sorted(names)
for name in sorting:
print(name)
n = ["Gojo", "Yuta", "Yuji", "Megumi"]
sort_student(n)
The list[str]
type is used to type hint the parameter names
to indicate that it expects a list of strings.
But what if you want names
to be a type tuple or set instead? You need to refactor your code. In this case, you need to change the type in one location, however, this might be hard in complex and big projects.
You can use the type Iterable
to make the function accept any iterable object.
from collections.abc import Iterable
def sort_student(names: Iterable[str]) -> None:
sorting = sorted(names)
for name in sorting:
print(name)
n1 = ("Gojo", "Yuta", "Yuji", "Megumi") # No error
n2 = ["Gojo", "Yuta", "Yuji", "Megumi"] # No error
n3 = {"Gojo", "Yuta", "Yuji", "Megumi"} # No error
Type Aliases for Better Readability
The complex type hints for callable objects like in the above case, arguments, and return values of the functions kind of feel cumbersome to write.
Why can't simplify the complex type by giving them an alias(name)? You can create an alias for the type in the same way you would create a variable.
type ReturnCart = dict[str, int] | None
def cart(item: str, quantity: int) -> ReturnCart:
if item == "" or quantity == 0:
return None
return {item: quantity}
cart_item = apply_cart(cart, "Mouse", quantity=2)
print(cart_item)
In the above example, an alias ReturnCart
is created and contains the type of return value (dict[str, int] | None
).
The alias is defined with the soft keywordtype
, which creates an instance of TypeAliasType
. This keyword was added in Python 3.12. If you are using an older version of Python, you can choose an alternate method.
Type aliases can also be created like you would create a variable using a simple assignment.
ReturnCart = dict[str, int] | None
To clarify, you can mark it with TypeAlias
to explicitly show this as a type alias, not a simple variable.
from typing import TypeAlias
ReturnCart: TypeAlias = dict[str, int] | None
The methods described above will assist you in simplifying complex types, and the key benefit is that you will only need to edit the type in one spot, eliminating the need for refactoring.
Another advantage of using type aliases is that they can be reused in other portions of your code, which improves code readability.
Type Checker Tools
Python entirely ignores type hints and determines the type of variables, function arguments, and return values at runtime.
Mypy, a popular third-party tool, can enforce type-checking in Python. Since it is a third-party tool, you must install it using the following command.
python -m pip install mypy
This will provide you access to the mypy
command to type-check your program.
Static Type Checking Using Mypy
The function sort_student()
takes an iterable object and sorts the student names in ascending order. Save this function to a file, such as student.py
.
# student.py
from collections.abc import Iterable
def sort_student(names: Iterable[str]) -> None:
sorting = sorted(names)
for name in sorting:
print(name)
n1 = ("Gojo", "Yuta", "Yuji", "Megumi")
sort_student(n1)
To type check your program using mypy
, enter the command mypy
followed by the file name you want to check in the terminal.
> mypy student.py
Success: no issues found in 1 source file
The type checker identified no errors. One thing you may have noticed is that the code contains the function call (sort_student(n1)
), which should have created the output, but there was none in the terminal except the message created by mypy
.
This means that mypy
does not execute your code, instead, it examines if the values match the expected type based on the type hints.
What if you made a mistake and the actual values do not match the expected type?
# student.py
def sort_student(names: list[str]) -> None:
sorting = sorted(names)
for name in sorting:
print(name)
n1 = ("Gojo", "Yuta", "Yuji", "Megumi")
sort_student(n1)
This function expects a list of strings but the tuple was passed. If you now check your code, it will generate the following error.
> mypy student.py
student.py:8: error: Argument 1 to "sort_student" has incompatible type "tuple[str, str, str, str]"; expected "list[str]" [arg-type]
Found 1 error in 1 file (checked 1 source file)
Mypy has identified the error on line number 8 saying that the argument passed to sort_student()
has an incompatible type tuple[str, str, str, str]
, it is expected to be a list[str]
.
Resources
Conclusion
Type hints are optional in Python but they can be used to make the code more readable and using type hints can avoid possible bugs in your code.
In this article, you've learned:
Implementing type hints
Alternative types for single data and different types for multiple values using pipe (
|
) andUnion
Callable
type for type hinting functionsEllipsis (
...
) andTypeVar
inCallable
type to make a callable object accept an arbitrary list of arguments of any typeParamSpec
instead of an ellipsis withTypeVar
in Callable type to make a good combination for a callable object accepting an arbitrary list of arguments of any typeIterable
type for type hinting iterable objectsType aliases to simplify complex type
Mypy for type checking in Python
There is much more you can do with type hints in Python.
πOther articles you might be interested in if you liked this one
β Best Practices: Positional and Keyword Arguments in Python
β Decorators in Python and How to Create a Custom Decorator?
β Create a WebSocket Server and Client in Python.
β Create and Interact with MySQL Database in Python
β Understanding the Different Uses of the Asterisk(*) in Python?
β Yield Keyword in Python with Examples?
That's all for now
Keep Codingββ
Top comments (2)
Thanks for sharing! This incremental approach to explaining the subject was very good.
Most Welcome