Python Type Hints: Callable Objects, Iterables and More
Tags: python, programming
Published: 2024-04-22 11:07:38

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?

```python
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:

```python
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:

```python
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.

```bash
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

```python
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 (`|`).

```python
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

```python
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.

```python
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.

```python
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

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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 keyword](https://docs.python.org/3/reference/lexical_analysis.html#soft-keywords)[`type`](https://docs.python.org/3/reference/simple_stmts.html#type), which creates an instance of [`TypeAliasType`](https://docs.python.org/3/library/typing.html#typing.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.

```python
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.

```python
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](https://mypy.readthedocs.io/en/stable/getting_started.html#), 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
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`.

```python
# 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.

```python
> 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?

```python
# 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.

```python
> 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

* [Type cheat sheet](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html)
    

## 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 (`|`) and `Union`
    
* `Callable` type for type hinting functions
    
* Ellipsis (`...`) and `TypeVar` in `Callable` type to make a callable object accept an arbitrary list of arguments of any type
    
* `ParamSpec` instead of an ellipsis with `TypeVar` in Callable type to make a good combination for a callable object accepting an arbitrary list of arguments of any type
    
* `Iterable` type for type hinting iterable objects
    
* Type aliases to simplify complex type
    
* **Mypy** for type checking in Python
    

There is [much more](https://docs.python.org/3/library/typing.html#) 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](https://geekpython.in/positional-and-keyword-arguments-in-python)

✅[Decorators in Python and How to Create a Custom Decorator](https://geekpython.in/decorators-in-python)?

✅[Create a WebSocket Server and Client in Python](https://geekpython.in/build-websocket-server-and-client-using-python).

✅[Create and Interact with MySQL Database in Python](https://geekpython.in/mysql-database-in-python)

✅[Understanding the Different Uses of the Asterisk(\*) in Python](https://geekpython.in/asterisk-in-python)?

✅[Yield Keyword in Python with Examples](https://geekpython.in/yield-keyword-in-python)?

---

**That's all for now**

**Keep Coding✌✌**