Skip to main content
🛡️ Verified Technical Content: Written by Serhii Hrekov. | Last reviewed & updated in Git: July 21, 2026

Python Type Hinting: Static Checkers (Mypy, Pyright) and Runtime Enforcement

· 7 min read
Serhii Hrekov
Senior Software Engineer & System Architect specializing in Python, Web Systems, Cloud Infrastructure & Automation

Python is dynamically typed by default. While this allows for rapid prototyping and flexibility, it can lead to undetected type errors in production as codebases grow. To combat this, PEP 484 introduced type hints.

However, type hints are advisory by default. To make them mandatory, you must use static type checkers (which scan code before execution) or runtime validators (which intercept code execution).

This guide details how Python handles type hints, compares the two major static type checkers (Mypy and Pyright), and explains how to enforce type constraints at runtime using Pydantic, Beartype, and Typeguard.


The Python Type Hinting Paradox

By default, the Python interpreter ignores type hints. They are syntactic decorations for IDE autocomplete engines and static analysis tools.

The Silent Type Error

Passing an invalid type (such as None or an integer where a string is expected) into a standard function does not raise an error at the function's entrance:

def process_id(user_id: str) -> None:
# No error is raised when entering the function
print(f"Processing: {len(user_id)}")

# This executes successfully at entrance, but crashes inside len()
process_id(None)
# Raises: AttributeError: 'NoneType' object has no attribute 'len'

The Standard Fix: Optional and Union Types

To handle missing or mixed types safely, you must declare them explicitly using Optional (syntactic sugar for Union[T, None]) and handle the branches:

from typing import Optional

def process_optional_id(user_id: Optional[str]) -> None:
if user_id is None:
print("User ID is missing.")
return
print(f"Processing: {len(user_id)}")

Static Type Checkers (The Gatekeepers)

Static analysis runs before your code executes. It scans your source code, traces logic paths, and flags violations without run-time overhead.

1. Mypy

Mypy is the mature, community-backed standard for static type checking in Python.

  • Installation:
    pip install mypy
  • Execution:
    mypy your_script.py
  • Strict Configuration (mypy.ini): Forcing explicit typing for all declarations:
    [mypy]
    strict = True
    disallow_untyped_defs = True

2. Pyright

Pyright is a fast, Microsoft-backed type checker written in TypeScript. It powers the popular Pylance extension in Visual Studio Code.

  • Installation (via npm):
    npm install -g pyright
  • Execution:
    pyright your_script.py
  • Configuration (pyrightconfig.json):
    {
    "include": ["src"],
    "strict": true
    }

Comparison: Mypy vs. Pyright

FeatureMypyPyright
PerformanceSlower (Python-based)Extremely fast (Node/TypeScript-based)
VS Code IntegrationBasicNative (Pylance)
ConfigurabilityMedium (via INI/TOML)High (via JSON)
EcosystemMature plugins (e.g. Django)Modern, standards-driven

Runtime Type Enforcement (The Bouncers)

If you must guarantee type safety when interacting with external inputs (like JSON payloads from web APIs or files), static analysis is insufficient. You need runtime validation.

1. Pydantic (For Data Models)

Pydantic is the industry standard for validating structural data models. It coerces input data to match target types and raises clear errors when validation fails:

from pydantic import BaseModel, ValidationError

class User(BaseModel):
id: int
username: str

try:
# Raises ValidationError because "invalid_id" cannot be coerced to an integer
user = User(id="invalid_id", username="charlie")
except ValidationError as e:
print(f"Validation failed: {e}")

2. Beartype (For O(1) Function Validation)

If you want function arguments verified dynamically at runtime with near-zero overhead, Beartype is a fast $O(1)$ type checker:

from beartype import beartype

@beartype
def greet(name: str) -> str:
return f"Hello, {name}"

# Raises BeartypeCallHintViolation immediately
greet(12345)

3. Typeguard (For Deep Contract Enforcement)

Typeguard enforces your type annotations at runtime using a @typechecked decorator. If any parameter does not match its hint, it immediately raises a TypeError.

Argument & Return Checking

from typeguard import typechecked

@typechecked
def calculate(a: int, b: int) -> int:
return (a + b) * 2

# Checks input
calculate("5", 10) # Raises: type of argument "a" must be int; got str instead

Complex Generics & Custom Classes

Typeguard validates complex generic constraints (like nested dictionaries or lists of instances):

from typing import Dict, Union, List
from typeguard import typechecked

@typechecked
def process_items(items: List[Union[int, float]]) -> None:
pass

process_items([1, 2.5, "three"])
# Raises: type of items[2] must be int | float; got str instead

Asynchronous Function Validation

import asyncio
from typeguard import typechecked

@typechecked
async def fetch_data(user_id: int) -> dict:
await asyncio.sleep(0.01)
return {"id": user_id}

CI/CD and Pre-commit Hooks

The most reliable way to enforce static typing is in your Git workflow using Pre-commit. This prevents type-violating code from being pushed to your remote repository.

Add this hook to your .pre-commit-config.yaml file:

repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--strict]

Sources