Python Typeguard: Runtime Type Checking
python typeguard: Learn how to use Python Typeguard to enforce type hints at runtime, catch type errors early, and integrate it with pytest for robust testing.
Python's type hints are a powerful documentation and static analysis tool, but they are not enforced at runtime. A function annotated to accept an int will happily receive a string, and the error may only surface deep in the call stack. Python typeguard solves this by adding runtime type checking to your functions and classes, turning type hints into executable contracts.
Why Runtime Type Checking Matters
Static type checkers like mypy and pyright catch many type errors before code runs, but they operate on the source code alone. They cannot see values that come from external sources such as user input, database records, or network responses. A function that expects a list[int] might receive a tuple or a list of strings at runtime, and the failure might not occur until much later, making the root cause hard to trace.
Typeguard fills this gap by validating arguments and return values against their type annotations at runtime. When a mismatch occurs, it raises a TypeCheckError with a clear message that identifies the offending value and the expected type. This is especially valuable in development and testing, where catching type errors early saves debugging time and prevents subtle bugs from reaching production.
Installing Typeguard
Typeguard is available on PyPI and can be installed with pip:
pip install typeguard
It has no mandatory dependencies beyond Python 3.8 or later, making it easy to add to any project. Once installed, you can import the typechecked decorator and start validating functions immediately.
Enforcing Types with @typechecked
The core of typeguard is the @typechecked decorator. Applying it to a function enables runtime checks for both arguments and the return value. Consider this simple example:
from typeguard import typechecked @typechecked def add(a: int, b: int) -> int: return a + b
When you call add(1, 2), it works as expected. But calling add("1", 2) raises a TypeCheckError because the first argument is a string, not an integer. The error message clearly states the problem:
TypeCheckError: argument "a" (str) is not an instance of int
This immediate feedback allows you to fix the caller or the function signature before the wrong value propagates through your system.
The decorator also checks the return value. If the function returns a value that does not match the declared return type, typeguard raises an error at the point of return. This catches mistakes in the function body itself, not just the inputs.
Checking Classes and Dataclasses
Typeguard can also validate methods on classes. Applying @typechecked to a class decorates all its methods, including __init__, properties, and regular methods. This is useful for enforcing invariants on object state.
from typeguard import typechecked @typechecked class BankAccount: def __init__(self, owner: str, balance: float) -> None: self.owner = owner self.balance = balance def deposit(self, amount: float) -> None: self.balance += amount
With the class decorator, passing a non-numeric balance to the constructor raises a TypeCheckError immediately. The same applies to the amount parameter in deposit. This prevents invalid objects from being created in the first place.
Dataclasses work equally well. You can combine @dataclass with @typechecked to get runtime validation on every field assignment:
from dataclasses import dataclass from typeguard import typechecked @typechecked @dataclass class User: id: int name: str email: str
Now constructing User("123", "Alice", "alice@example.com") fails because id is a string, not an integer. This is particularly helpful when dataclasses are used to parse external data, such as JSON payloads.
Global Configuration with typeguard.config
Typeguard provides a global configuration object that lets you adjust its behavior without changing every decorator. The typeguard.config object has attributes like check_return_types, check_argument_types, and forward_ref_policy. For example, you can disable return type checks globally if they are too strict for your use case:
import typeguard typeguard.config.check_return_types = False
This setting affects all decorated functions in the current process. You can also control how forward references are resolved, which is useful when you use from __future__ import annotations. The configuration object is a singleton, so changes apply process-wide. This is handy for toggling type checking based on environment variables or a debug flag.
Typeguard as a Pytest Plugin
One of the most practical uses of typeguard is integrating it with pytest. Typeguard ships with a pytest plugin that can automatically instrument all test functions and the functions they call. To enable it, run pytest with the --typeguard-packages option, specifying the packages you want to check:
pytest --typeguard-packages=myapp
This instruments every function in the myapp package, so any type mismatch during test execution raises a TypeCheckError and fails the test. This is an excellent way to enforce type correctness across a codebase without adding decorators to every function. You can also combine it with coverage tools to ensure that type checks are exercised during testing.
The plugin is particularly effective in continuous integration, where it catches type errors that static checkers might miss because they only appear with real runtime values.
Performance and Overhead
Runtime type checking adds overhead to every decorated function call. Typeguard must inspect each argument and the return value, which involves isinstance checks and, for complex generic types, type inference. In hot paths, this can slow down execution noticeably. However, the overhead is usually acceptable in development and test environments, where correctness is more important than raw speed.
If you need to disable type checking in production, you have a few options. You can remove the decorators manually, but that is error-prone. A better approach is to use an environment variable to control the configuration or to conditionally apply the decorator only in debug mode. For example:
import os from typeguard import typechecked if os.getenv("ENABLE_TYPECHECK"): def checked(func): return typechecked(func) else: def checked(func): return func
This way, you can run with type checking during development and disable it in production without changing the code. The overhead is zero when the decorator is not applied, because the function is left untouched.
Limitations and Edge Cases
Typeguard is powerful, but it has limitations. It cannot enforce types that are not representable at runtime, such as TypeVar with a bound that is only a protocol. In such cases, typeguard may fall back to a looser check or skip it entirely. Also, typeguard does not perform deep equality checks on arbitrary objects; it relies on isinstance and structural checks for generic types like list[int]. If you need to validate that a list contains only integers, typeguard will check each element, but if the list is extremely large, this can be slow.
Another edge case is that typeguard does not check the types of local variables or attributes unless they are explicitly annotated and part of a function signature or class field. It only validates what is declared in the function signature, return type, and class annotations. This is by design, as checking every local variable would be too invasive.
Finally, typeguard's behavior can vary with Python versions and typing constructs. For example, support for typing.Literal or typing.TypedDict may require specific Python versions. Always test typeguard in your target environment to ensure it behaves as expected. Despite these limitations, typeguard is a valuable tool for adding a runtime safety net to your Python code, especially when combined with static type checking and a robust test suite.