Skip to main content

Typeguard Examples: Mandatory Runtime Type Checking in Python

· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Typeguard is a lightweight yet powerful Python library that enforces type hints at runtime. Unlike static checkers (like MyPy or Pylance), which only check code before execution, Typeguard uses function decorators to ensure that function arguments and return values strictly adhere to their type annotations during execution. If a type mismatch occurs, Typeguard raises a TypeError, effectively making your type hints mandatory contracts.

This article explores various practical use cases and examples for deploying Typeguard.

Setup and Basic Enforcement

To use Typeguard, you simply install it and apply the @typechecked decorator to any function you want to enforce.

# Installation: pip install typeguard

from typeguard import typechecked
from typing import List, Union

@typechecked
def add_and_double(a: int, b: int) -> int:
"""Requires two integers and guarantees an integer return."""
return (a + b) * 2

# Example 1: Successful Execution
print(f"Result (Valid): {add_and_double(5, 10)}")
# Output: Result (Valid): 30

# Example 2: Argument Type Mismatch (Enforcement)
try:
# Passing a string instead of an int
add_and_double("5", 10)
except TypeError as e:
print(f"Argument Error: {e}")
# Output: Argument Error: type of argument "a" must be int; got str instead

Enforcement on Return Values

Typeguard checks the type of the object returned by the function against the return annotation (-> type). This is critical for internal functions that must guarantee a specific output type for consuming functions.

@typechecked
def get_user_list(count: int) -> List[str]:
"""Guarantees the return value is a list containing only strings."""
if count == 0:
# Intentionally return an incorrect type (list of integers)
return [1, 2, 3]
return ["Alice", "Bob"]

# Example 3: Return Type Mismatch
try:
# The function runs, but Typeguard checks the result before it is returned
get_user_list(0)
except TypeError as e:
print(f"Return Error: {e}")
# Output: Return Error: type of return value must be list[str]; got list[int] instead

Handling Complex and Generic Types

Typeguard fully supports complex type hints from the typing module, including generics (List, Dict), unions (Union), and optional types (Optional).

Example 4: Enforcing List and Dictionary Contents

from typing import Dict

@typechecked
def process_data_payload(payload: Dict[str, Union[int, float]]) -> None:
"""Payload must be a dictionary where values are either int or float."""
total = sum(payload.values())
print(f"Total processed: {total}")

# Valid Payload
process_data_payload({"a": 10, "b": 20.5, "c": 5})

# Invalid Payload (contains a string)
try:
process_data_payload({"a": 10, "b": "invalid"})
except TypeError as e:
print(f"Complex Type Error: {e}")
# Output: Complex Type Error: type of payload['b'] must be int | float; got str instead

Example 5: Handling Optional and None

When a function accepts None via Optional[T] or Union[T, None], Typeguard validates that other types are still correct.

from typing import Optional

@typechecked
def lookup_value(key: Optional[str]) -> Optional[int]:
"""Key can be None, but if present, must be a string."""
if key is None:
return 0
# Logic...
return 100

# Valid Calls
lookup_value("test")
lookup_value(None)

# Invalid Call (passing an int when expecting Optional[str])
try:
lookup_value(5)
except TypeError as e:
print(f"Optional Error: {e}")
# Output: Optional Error: type of argument "key" must be str | None; got int instead

Enforcing Custom Pydantic or Custom Class Types

Typeguard can check against custom classes, including Pydantic models, ensuring that complex data objects passed between layers are the correct structural type.

Example 6: Checking Custom Class Instances

from pydantic import BaseModel

class UserProfile(BaseModel):
name: str
age: int

@typechecked
def update_profile(user_data: UserProfile) -> None:
"""Requires the input to be a validated UserProfile instance."""
print(f"Updating profile for: {user_data.name}")

# Valid Call (passing the correct class instance)
update_profile(UserProfile(name="Charlie", age=30))

# Invalid Call (passing a generic dictionary)
try:
update_profile({"name": "Dave", "age": 40})
except TypeError as e:
print(f"Class Instance Error: {e}")
# Output: Class Instance Error: type of argument "user_data" must be UserProfile; got dict instead

Use with Asynchronous Functions

Typeguard works seamlessly with asynchronous functions (async def) and methods, making it ideal for API and I/O-bound applications.

Example 7: Async Function Enforcement

import asyncio

@typechecked
async def fetch_user_data(user_id: int) -> Dict[str, Any]:
"""Enforces type check on arguments and the awaitable return value."""
await asyncio.sleep(0.01)
return {"id": user_id, "status": "fetched"}

# Run the async function with valid type
async def run_valid():
result = await fetch_user_data(123)
print(f"Async Result: {result}")

# Run the async function with an invalid type
async def run_invalid():
try:
await fetch_user_data("invalid_id")
except TypeError as e:
print(f"Async Argument Error: {e}")

# asyncio.run(run_valid())
# asyncio.run(run_invalid())

Sources and Further Reading

  1. Typeguard Documentation - Basic Usage
  2. Typeguard Documentation - Complex Types
  3. Typeguard GitHub Repository
  4. Python Documentation - The typing Module