Back to Posts
Close-up of vintage typewriter keys, symbolizing the importance of precision in coding, analogous to 'How to improve Python code with type hinting' for better software development.

Streamline Python: Advanced Type Hint Techniques

Type hinting, while occasionally feeling bolted onto the language, is a powerful feature that supports you, the developer, your users, and the tools you use. And while there is no harm in not adding type hinting, I’ll demonstrate how this approach can enhance the readability of your code and provide better support from your IDE.

Why is it important?

Type hinting not only provides context to the users of your code but also provides linting information to your IDE, which can help inform you of what arguments and return a function has or what type a variable might be. This feature can help us eliminate bugs and potential crashes by informing us when there is an unexpected type or when we are passing the wrong arguments to a function, etc.

It’s particularly useful when building libraries intended for end users or when working collaboratively on a project, as it can help reduce the amount of time spent reading documentation by providing an at-a-glance view of what a function expects, what it returns, or what an attribute might be.

Another benefit comes in the form of better linting and code completions in your IDE. This can aid you in your development and speed up the process of debugging type-related issues.

Note

I won’t be covering the basics of type hinting; instead, I’ll be covering more advanced hinting that can provide better context for your code. If you do wish to know more about type hints and why I think you should use them, check out my video here.

The typing module

The typing module in Python enhances our code with advanced type hints, providing a clearer understanding of the data types our code is handling. It comes with a variety of type hints that help us cover a wide range of concepts that would otherwise be impossible with the basic type hints.

typing.Union & typing.Optional

typing.Union: It’s a versatile type hint that allows a variable to have multiple types. This is useful in functions that operate on different data types or when the type of variable is not fixed. Essentially, Union indicates that the variable can be either type A or type B.

typing.Optional: It represents a distinct variation of Union in which the type can be either the explicitly defined type or None. It’s commonly utilized for optional arguments or functions that may or may not yield a value.

These are particularly valuable in scenarios where we may be searching for data that could potentially be absent or where we may be accommodating various types of data.

from typing import Union, Optional


def get(id: Union[int, str]) -> Optional[User]:
  """
  This function accepts an ID, which can be either an integer or a string. It returns either a User object or None.
  """

typing.Literal

Sometimes, we want to show that a variable or argument accepts some specific values, usually in the case of configuration. The literal hint lets us show that we only accept certain values and not certain types.

from typing import Literal, List


def get_users_by_status(status: Literal['active', 'inactive']) -> List[User]:
    """
      This function accepts a status, which can be either 'active' or 'inactive'. It returns a list of User objects.
  """

typing.Callable

This type hint is utilized to express that a variable represents a function. Moreover, it enables us to define the type of arguments that the function accepts and the return type. This is highly advantageous for clarity and ensuring accurate usage of functions, particularly when functions are passed as arguments.

from typing import Callable, List


def filter_users(filter_func: Callable[[User], bool], users:List[user]) -> List[User]:
    """
    This function accepts a function that takes a User object and returns a boolean. It returns a list of User objects.
    """

typing.TypeVar & typing.Generic

These are used for creating generic classes or functions, where the exact types are not known initially but are specified when the class or function is used. This is useful when creating generic data structures, such as the repository pattern.

from typing import TypeVar, Generic

RepoObject = TypeVar('RepoObject')


class Repository(Generic[RepoObject]):
    """
    A generic Repository class. 'RepoObject' can be any type, and the specific type will be determined when an instance of the class is created.
    """

    def get(self, id: int) -> RepoObject:
        """
        A function that returns an object of type 'RepoObject'. The exact type of this object will be known when the class is instantiated.
        """


class User:
    """
    A User class, which can be used as a type for 'RepoObject'.
    """


def get_user_repo() -> Repository[User]:
    """
    A function that returns a Repository of type User.
    """

typing.Protocol

There are instances where it’s necessary to represent the structure of the object instead of its type. This is most frequently employed when dealing with multiple types that all conform to the same structure and can be interchanged with one another. This concept is referred to as Duck Typing, and it’s a prevalent pattern in Python. The usage of the Protocolhint enables us to incorporate this concept effectively into our code, particularly when functions are being passed as arguments.

from typing import Protocol


class IOTDevice(Protocol):
    """
    A protocol that defines the structure of an IOT device.
    """

    def turn_on(self) -> None:
        """
        A function that turns the device on.
        """

    def turn_off(self) -> None:
        """
        A function that turns the device off.
        """


class LightBulb:
    """
    A class that represents a light bulb.
    """

    def turn_on(self) -> None:
        """
        A function that turns the light bulb on.
        """

    def turn_off(self) -> None:
        """
        A function that turns the light bulb off.
        """


class SmartPlug:
    """
    A class that represents a smart plug.
    """

    def turn_on(self) -> None:
        """
        A function that turns the smart plug on.
        """

    def turn_off(self) -> None:
        """
        A function that turns the smart plug off.
        """


def turn_on_device(device: IOTDevice) -> None:
    """
    A function that turns on an IOT device.
    """

    device.turn_on()


def turn_off_device(device: IOTDevice) -> None:
    """
    A function that turns off an IOT device.
    """

    device.turn_off()

While these two classes are of distinct types, they both adhere to the same structure, and as such, both would be compatible with the IOTDevice protocol.

typing.TypedDict

There are times when we want to show that a dictionary has a specific structure and that it contains specific keys. This is where TypedDict comes in. It allows us to define the structure of a dictionary and the types of its keys and values.

from typing import TypedDict

class User(TypedDict):
    """
    A TypedDict that represents a user.
    """

    id: int
    name: str
    email: str
    age: int

typing.Final

The Final hint indicates a value should not be overwritten. This is useful for constants and can help prevent accidental overwrites.

from typing import Final
import os
class Database:
    DATABASE_PATH: Final[str] = os.getenv('DATABASE_PATH')

class UserDatabase(Database):
    DATABASE_PATH = 'users.db' #  this would give a warning, as we are altering a value flasgged as Final

By applying the Final type hint, we’ve indicated that the value should not be overwritten, and the IDE will inform us if we attempt to do so.

Final thoughts

As you can see, by leveraging the power of type hints, you can write clearer code, get better feedback from your IDE, and write less documentation. By using type hints you can get a better idea of what the data of your project looks like, and it can help discourage bad habits. I hope this post has helped you understand the power of type hints, and how they can help you write better code.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts