Runtime Type Checking in Python: beartype vs TypeGuard vs annotationlib
Problem
When I use Python type annotations in my code, I need to check if values conform to those types at runtime. But I hit a wall with traditional isinstance and issubclass methods:
# This works fine with built-in typesprint(isinstance([1, 2, 3], list)) # True
# But this fails with type annotationstry: print(isinstance([1, 2, 3], list[int])) # TypeError!except TypeError as e: print(f"Error: {e}")The error message shows: “isinstance() arg 2 must be a type or tuple of types
I need runtime type checking that works with modern Python type annotations, not just built-in types.
Environment
- Python 3.9+ (with typing_extensions for older versions)
- Windows 11
- beartype 0.18.0
- TypeGuard 3.0.0
- annotationlib 3.14.0 (if using Python 3.14+)
What happened?
I was working on a data processing library where I needed to validate data against type annotations at runtime. I use type annotations extensively for documentation and static type checking with mypy.
When I tried to check if data matched my annotated types at runtime, I discovered that isinstance and issubclass don’t work with generic types or other type annotations.
Here’s my setup:
from typing import List, Optional, Union
def process_data(data: List[Optional[int]]) -> List[int]: # I need to validate that 'data' is actually List[Optional[int]] # But isinstance(data, List[Optional[int]]) throws TypeError return [x for x in data if x is not None]
# Test datatest_data = [1, 2, None, 3, None, 4]result = process_data(test_data)I can explain the key parts:
data: List[Optional[int]]- I’m using type annotations to specify expected input[x for x in data if x is not None]- I’m filtering None values- But I have no runtime validation that the input is actually correct
When I tried to add runtime validation using isinstance, I got this error:
def process_data(data: List[Optional[int]]) -> List[int]: if not isinstance(data, List[Optional[int]]): # TypeError here! raise TypeError(f"Expected List[Optional[int]], got {type(data)}") return [x for x in data if x is not None]The TypeError is: “isinstance() arg 2 must be a type or tuple of types
How to solve it?
I tried using annotationlib first:
import annotationlib
def process_data(data: List[Optional[int]]) -> List[int]: if not annotationlib.isinstance(data, List[Optional[int]]): raise TypeError(f"Expected List[Optional[int]], got {type(data)}") return [x for x in data if x is not None]This works for Python 3.14+. The isinstance replacement can handle type annotations.
But when I tried the same on Python 3.9, it failed because annotationlib doesn’t exist. So I tried beartype instead.
from beartype import beartype
@beartypedef process_data(data: List[Optional[int]]) -> List[int]: if not isinstance(data, list): raise TypeError(f"Expected list, got {type(data)}") return [x for x in data if x is not None]I can use beartype as a decorator to validate function arguments and return values. It automatically checks that the input matches the type annotation.
Then I tried TypeGuard for conditional logic:
from typing import TypeGuard, Optional
def is_non_empty_string_list(value: list[Optional[str]]) -> TypeGuard[list[str]]: """Check if list contains only non-empty strings"" if not isinstance(value, list): return False return all(isinstance(item, str) and item.strip() for item in value)
if is_non_empty_string_list(my_list): # Type checker knows my_list is list[str] here print(my_list)TypeGuard gives me runtime type narrowing similar to what static type checkers can do.
Now test again with beartype:
from beartype import beartypefrom typing import List, Optional
@beartypedef process_data(data: List[Optional[int]]) -> List[int]: """Function with runtime type checking enabled"" return [x for x in data if x is not None]
# This will raise beartype violation if types don't matchtry: # Correct type result = process_data([1, 2, 3, None, 4]) print(f"Success: {result}")
# Wrong type - will be caught wrong_result = process_data("not a list")except Exception as e: print(f"Error: {e}")You can see that beartype catches type mismatches at runtime and provides helpful error messages.
The reason
I think the key reason for the problem is that isinstance and issubclass were designed for traditional Python types, not the type annotation system that evolved with PEP 484 and later PEPs.
The core issue is:
- isinstance expects a “type object” as the second argument
- Type annotations like
list[int]are not type objects - they’re special annotation objects - Traditional type checking tools can’t evaluate these annotations at runtime
Each solution addresses this differently:
- annotationlib provides the official way to evaluate annotations at runtime
- beartype translates annotations into runtime validation checks
- TypeGuard focuses on narrowing types in conditional contexts
Runtime Type Checking Tools Comparison
| Feature | annotationlib | beartype | TypeGuard |
|---|---|---|---|
| Python Version | 3.14+ only | 3.7+ | 3.8+ |
| Type Coverage | Type forms only | Comprehensive annotations | Type narrowing only |
| Performance | Fast | Very fast | Fast (narrowing only) |
| Setup Complexity | Simple | Simple (decorator) | Moderate |
| Error Messages | Basic | Detailed | Basic |
| Dependencies | None | External | External |
| Primary Use Case | Type checking | Function validation | Conditional narrowing |
When to Use Each Tool
Use annotationlib when:
- You’re targeting Python 3.14+
- You need only basic isinstance/issubclass functionality
- You want stdlib-only solutions
- You’re working with type forms specifically
Use beartype when:
- You need comprehensive type checking
- Performance is critical
- You want detailed error messages
- You’re supporting multiple Python versions
- You need validation for complex nested types
Use TypeGuard when:
- You’re doing conditional type narrowing
- You want runtime type checking similar to static type checkers
- You’re working with isinstance-like checks in if statements
- You need type assertions for branching logic
Summary
In this post, I showed how runtime type checking works with modern Python type annotations. The key point is that each tool serves different needs: annotationlib for stdlib compatibility, beartype for comprehensive function validation, and TypeGuard for conditional type narrowing.
I found that beartype gives me the most comprehensive solution for runtime validation, while TypeGuard is perfect for narrowing types in conditional contexts. annotationlib is good but limited to Python 3.14+.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 beartype GitHub
- 👨💻 TypeGuard GitHub
- 👨💻 annotationlib Docs
- 👨💻 Reddit Discussion
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments