Skip to content

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 types
print(isinstance([1, 2, 3], list)) # True
# But this fails with type annotations
try:
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 data
test_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
@beartype
def 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 beartype
from typing import List, Optional
@beartype
def 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 match
try:
# 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:

  1. isinstance expects a “type object” as the second argument
  2. Type annotations like list[int] are not type objects - they’re special annotation objects
  3. 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

FeatureannotationlibbeartypeTypeGuard
Python Version3.14+ only3.7+3.8+
Type CoverageType forms onlyComprehensive annotationsType narrowing only
PerformanceFastVery fastFast (narrowing only)
Setup ComplexitySimpleSimple (decorator)Moderate
Error MessagesBasicDetailedBasic
DependenciesNoneExternalExternal
Primary Use CaseType checkingFunction validationConditional 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments