Understanding Python's Typing Module

Suraj RaoSuraj Rao
6 min read

Python is dynamically typed by default. A variable like x = 4 can later become x = "hello" with no complaints from the interpreter. However, as Python projects grow, it becomes crucial to improve code readability, debuggability, and collaboration. This is where the typing module (introduced in Python 3.5 via PEP 484) becomes valuable.

Why Use Type Hints?

Type hints provide a way to explicitly declare the expected data types of variables and function arguments/returns.

  • Improved Readability: Explicitly documents variable and function types.

  • Early Error Detection: Type checkers (like mypy) can catch bugs before runtime.

  • Better Developer Onboarding: New developers can understand the codebase more quickly.

  • Enhanced IDE Support: Features like autocomplete, linting, and refactoring are improved.

Type Annotations for Variables

You can annotate variables with specific types to indicate what kind of value is expected.

X: int = 5
X: int = "hello"  # No runtime error, but static checker like mypy will warn

To check types:

pip install mypy
mypy your_file.py

Function Annotations

Function annotations specify the expected input and return types of functions.

def add(a: int, b: int, c: int) -> int:
    return a + b + c  # Takes three integers and returns an integer

def do_nothing() -> None:
    pass  # Explicitly returns nothing

List Type

Use List[T] to annotate lists that contain elements all of the same type T, with a dynamic and mutable size.

from typing import List

matrix: List[List[int]] = [[1, 2], [3, 4]]  # 2D list of integers

Tuple Type

Use Tuple[X, Y, Z] to annotate fixed-length tuples where each element has a specific, predefined type.

from typing import Tuple

user_info: Tuple[int, int, str] = (42, 27, "admin")  # Fixed-length tuple with known types

Use Tuple[T, ...] to annotate variable-length tuples where all elements share the same type T.

from typing import Tuple

scores: Tuple[int, ...] = (90, 85, 100, 78)  
# Tuple of any length where every element is an int

Dict Type

Use Dict[K, V] for dictionaries with specific key and value types.

from typing import Dict

roles: Dict[str, str] = {"alice": "admin"}  # Dict with str keys and str values

Set Type

Use Set[T] to define sets containing only elements of type T.

from typing import Set

numbers: Set[int] = {1, 2, 3}  # Set of integers
letters: Set[str] = {"a", "b"}  # Set of strings

Optional Type

Optional[T] means the value can either be of type T or None.

from typing import Optional

def get_name(id: str) -> Optional[str]:
    ...  # Can return a string or None
from typing import Optional

def foo(output: Optional[bool] = False) -> None:
    # The 'output' parameter can be either a bool or None.
    # Defaults to False if not provided.

Any Type

Any disables type checking for a variable or parameter. It can be any type.

from typing import Any

def log(input: Any) -> Any:
    return input  # Accepts and returns any type

Sequence Type

Use Sequence[T] to represent ordered collections like lists, tuples, or strings.

from typing import Sequence

def print_all(items: Sequence[str]):
    for item in items:
        print(item)

# All valid
print_all(["a", "b", "c"])  # List
print_all(("x", "y", "z"))  # Tuple
print_all("hello")         # String is also a sequence

# Invalid
print_all({"a", "b", "c"}) # set is unordered and not considered a sequence, mypy will raise an error

Callable Type

Use Callable[[ArgTypes], ReturnType] to annotate function parameters that accept other functions.

from typing import Callable

def operate(f: Callable[[int, int], int]) -> None:
    print(f(2, 3))  # Accepts a function taking two ints and returning an int

def add(x: int, y: int) -> int:
    return x + y

operate(add)

Type Aliases or Custom Type

Create readable aliases for complex or reused type structures.

from typing import List, Tuple, Union, Dict

# Simplifying a complex list of tuples
Coordinates = List[Tuple[int, int]]  # Here Coordinates is the alias name
points: Coordinates = [(1, 2), (3, 4)]

# Using Union for a variable that can be an int or a string
IntOrString = Union[int, str]
data: IntOrString = 42  # Could also be a string

# For a dictionary with string keys and values that are either integers or floats
ValueDict = Dict[str, Union[int, float]]
my_dict: ValueDict = {"age": 30, "temperature": 98.6}

Union Type

A Union means a variable can be one of several specified types.

Basic Syntax (Pre–Python 3.10)

from typing import Union

# This variable can be either an int or a str
number_or_string: Union[int, str] = 5

Use Case 1: Function Parameters

Allows accepting multiple input types with appropriate handling.

from typing import Union

def process_input(value: Union[int, str]) -> Union[float, str]:
    if isinstance(value, str):
        return f"Received string: {value}"  # Returns string if input is a string
    else:
        return float(value)  # Converts int to float and returns

Use Case 2: Return Values

Used when a function might return multiple types depending on logic.

from typing import Union

def get_data(key: str) -> Union[dict, list, str]:
    """
    Returns different data types based on the provided key.
    Could return a dictionary, list, or string.
    """
    ...

Use Case 3: Variables with Multiple Possible Types

Used when the type of a variable may change over time.

from typing import Union

result: Union[int, None] = None  # Initially None
result = 10                      # Later assigned an int

Python 3.10+ Syntax Using | (PEP 604)

Python 3.10 introduced cleaner syntax using the | (pipe) symbol.

# Variable can be an int or a str
number_or_string: int | str = 5

# Function example with the new syntax
def process_input(value: int | str) -> float | str:
    if isinstance(value, str):
        return f"Received string: {value}"
    else:
        return float(value)

Generics with TypeVar

Use TypeVar to define generic functions or classes that work with multiple types.

from typing import TypeVar, List

T = TypeVar('T')  # Define a generic type variable T

def get_element(lst: List[T], index: int) -> T:
    """
    Returns the element at the given index from a list.
    The list can be of any type, but all elements must be of the same type T.
    """
    return lst[index]

# Examples of usage:
print(get_element([10, 20, 30], 1))        # Output: 20 (int)
print(get_element(["apple", "banana"], 0)) # Output: "apple" (str)
print(get_element([[1, 2], [3, 4]], 1))     # Output: [3, 4] (List[int])

Summary:

Type hints help make Python code easier to read, debug, and maintain. They allow you to specify expected types for variables and functions, which improves code clarity and catches potential errors early. As projects grow, using type hints becomes essential for better collaboration and tool support.

soDisappointing : r/ProgrammerHumor

8
Subscribe to my newsletter

Read articles from Suraj Rao directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Suraj Rao
Suraj Rao

Hi, I'm Suraj I'm an aspiring AI/ML engineer passionate about building intelligent systems that learn, see, and adapt to the real world.