Type Hints in Python — A Practical Guide
Think of Python's type hints as sticky notes attached to your code for a linter to read, but which are torn off before the code actually runs. isinstance() only sees the raw type, like list, not the detail on the "sticky note."
isinstance() or issubclass(); use static checkers or a schema validator like Pydantic instead.The Setup
You are writing an API integration gateway that processes raw JSON payloads. You use modern type hints to signal to your team that a configuration list must only contain integers, and you write a runtime assertion to verify this before committing the data.
What Does This Print?
def process_user_ids(payload: dict) -> bool:
user_ids = payload.get("user_ids")
# Attempting to validate that user_ids is a list of integers
if isinstance(user_ids, list[int]):
return True
return False
process_user_ids({"user_ids": [1, 2, 3]})
process_user_ids is executed. Does it validate the array, return False, or crash?
The Output
This execution crashes immediately at runtime. Python's type checking ecosystem is purely static and managed outside execution by checkers like mypy or pyright. At runtime, generic parameters like list[int] (subscripted generics) do not exist as concrete types. Calling isinstance() with a subscripted generic raises a TypeError because the runtime interpreter does not inspect individual elements within the collection to prove compliance.
Why Python Does This
CPython maintains dynamic type enforcement of basic types but discards detailed generic metadata during evaluation to maintain execution performance. Classes like list are defined in C, and subscripting them (e.g., list[int]) returns a types.GenericAlias object in Python 3.9+. The __instancecheck__ dunder method on GenericAlias is explicitly programmed to raise a TypeError. Checking every element of a list at runtime inside an isinstance call would degrade an $O(1)$ pointer check to an $O(N)$ iteration, violating the speed guarantees of Python's built-in type system.
The Fix
def process_user_ids(payload: dict) -> bool:
user_ids = payload.get("user_ids")
# First, validate the outer container type
if not isinstance(user_ids, list):
return False
# Explicitly check item types at runtime, or use a tool like Pydantic
return all(isinstance(uid, int) for uid in user_ids)
The fix involves using a static type checker like mypy for compile-time validation or a runtime schema validator (like Pydantic) which explicitly parses and validates data structures using the type hints. This separates static checks from runtime behavior.
How This Fails in Real Systems
A payments platform checked incoming processing batches using isinstance(batch, list[Transaction]). A quick refactor to modern type hints bypassed static analysis warning checks, causing the application to raise unhandled TypeErrors at the API boundary, rejecting over $40,000 in customer checkouts before the change was reverted.
Key Takeaway
isinstance() or issubclass(); use static checkers or a schema validator like Pydantic instead.list[int] retain their inner type information at runtime for isinstance() checks.