Runtime Type Checking with beartype
Type hints in Python are a powerful way to document your code and enable static analysis tools like mypy. But by default, Python ignores these hints at runtime—they are just metadata. This is where beartype comes in.
beartype is a runtime type checker that plugs directly into your functions via decorators. It validates inputs and outputs against your type hints in real time, catching type errors the moment they occur. Unlike heavy validation frameworks, beartype is designed to be fast and unobtrusive.
Why Runtime Type Checking?
Static type checkers like mypy catch errors before your code runs, which is great. But they cannot help you when your code receives data from external sources like APIs, user input, or files. They also cannot help when you are working with untyped third-party libraries or when your codebase uses dynamic patterns that static checkers cannot reason about.
Runtime checks fill this gap. They validate the actual values flowing through your functions at execution time, giving you confidence that your code handles data correctly regardless of its source.
Installing beartype
pip install beartype
beartype supports Python 3.8+ and has no required dependencies beyond the standard library. The package is lightweight and adds minimal overhead to your application.
Your First Type Check
The simplest way to use beartype is with the @beartype decorator:
from beartype import beartype
@beartype
def greet(name: str) -> str:
return f"Hello, {name}!"
# Works fine
print(greet("Alice")) # Hello, Alice!
# Raises TypeError
print(greet(123)) # beartype.exception ExceptionTgtUnexpectedTypeException
Every call to greet() now validates that name is a string. Pass an integer and beartype raises an exception immediately, making it easy to identify the source of type-related bugs.
Validating Complex Types
beartype handles all standard typing constructs from the typing module:
from beartype import beartype
from typing import List, Dict, Optional
@beartype
def process_data(
items: List[int],
config: Dict[str, str],
name: Optional[str] = None
) -> List[int]:
return [i * 2 for i in items]
# Valid call
process_data([1, 2, 3], {"mode": "fast"}) # [2, 4, 6]
# Invalid: list contains wrong type
process_data([1, "two", 3], {"mode": "fast"}) # TypeError
# Invalid: wrong type for config
process_data([1, 2, 3], ["mode", "fast"]) # TypeError
The decorator checks both the container types and their contents, ensuring nested type safety throughout your data structures.
Catching Custom Classes
You can use your own classes in type hints and beartype will validate them:
from beartype import beartype
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@beartype
def create_user(name: str, age: int) -> User:
return User(name, age)
user = create_user("Bob", 30) # Works
user = create_user("Bob", "thirty") # TypeError
beartype validates that the returned object is an instance of the declared type, catching errors when your functions return unexpected types.
Union Types and Literal
Python is Union, Optional, and Literal types work seamlessly with beartype:
from beartype import beartype
from typing import Union, Literal
@beartype
def set_status(status: Union[str, int], priority: Literal["low", "medium", "high"]) -> None:
print(f"Status: {status}, Priority: {priority}")
set_status("active", "high") # Works
set_status(1, "medium") # Works (Union allows int)
set_status("active", "urgent") # TypeError - not in Literal
set_status(None, "high") # TypeError - None not in Union
This makes beartype particularly useful for validating configuration options and user inputs that have a limited set of valid values.
Configuration Options
beartype is configurable. You can control its behavior with various parameters:
from beartype import beartype, BeartypeConf
# Disable return type checking
@beartype(conf=BeartypeConf(return_strategy=False))
def add(a: int, b: int) -> int:
return a + b
# Use violate strategy to raise exceptions instead of returning None
@beartype(conf=BeartypeConf(violation_strategy=None))
def multiply(x: int, y: int) -> int:
return x * y
These options give you fine-grained control over how beartype handles type violations in different contexts.
Performance Considerations
beartype is designed to be fast. It generates optimized type-checking code at decoration time rather than interpreting hints on every call. For functions with simple or no type hints, it reduces to a no-op automatically, minimizing runtime overhead.
import time
from beartype import beartype
@beartype
def fast_function(x: int) -> int:
return x * 2
# First call compiles the checker (slower)
start = time.perf_counter()
for _ in range(10000):
fast_function(5)
elapsed = time.perf_counter() - start
print(f"10k calls in {elapsed:.4f}s") # Typically < 0.01s
In practice, the overhead of beartype is negligible for most applications, especially compared to the cost of debugging type-related bugs in production.
When to Use beartype
Add runtime checks when you are receiving data from external sources such as HTTP requests, file parsing, or user input. It is also valuable when building libraries used by others who may pass incorrect types, during debugging when you want explicit type assertions, and when working with dynamic code patterns that mypy cannot understand.
Skip beartype when static analysis with mypy already covers your codebase, when performance is critical and you have already validated inputs elsewhere, or when you are using a type checker that integrates beartype natively.
See Also
- typing-module — Python standard library for type hints
- python-type-hints — Introduction to type hints in Python
- pydantic-guide — Data validation using type hints at runtime