Skip to main content

The Python Type Hinting Paradox: Why it Doesn't Raise an Error

· 6 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

This is a fundamental and often confusing concept when developers transition to using static type checking in Python: Python's type hints are advisory, not mandatory.

The core reason why passing None to a function expecting a specific type like str or int does not raise an error at runtime (on entrance) is that the Python interpreter, by default, ignores type hints.

The Mechanism: Runtime vs. Static Checks

ConceptActionEnforcement Level
Python InterpreterRuns the code (runtime).Ignores type hints completely (unless a library like Pydantic or FastAPI intercepts the function).
Static Type CheckerReads the code without running it (static check).Enforces type hints and flags violations (e.g., MyPy, Pylance).
The None TypeThe None literal has the specific type: NoneType.Pylance/MyPy see that NoneType is not a str and will flag it statically.

Because Python is dynamically typed, the function call x(None) executes successfully because None is a perfectly valid object, and the function definition def x(atr: str): does not contain any runtime validation logic.


Example: The Silent None

In the following example, the code runs without crashing, but a static type checker would flag the call to x immediately.

# The Function Definition
def process_data(user_id: str) -> None:
"""
Expects a string, but Python allows anything.
"""
# This line might crash later if user_id is None, but the entrance is fine.
print(f"Processing ID length: {len(user_id)}")

# The Call (No Error at Entrance)
data = None
# MyPy/Pylance STATIC ERROR: Expected type 'str', got 'NoneType' instead
try:
process_data(data)
except TypeError as e:
# The crash occurs *inside* the function when len() is called on None,
# not at the entrance.
print(f"Runtime error occurred: {e}")
# Output: Runtime error occurred: object of type 'NoneType' has no len()

The Fix: Explicitly Allowing or Excluding None

Since None is a distinct type (NoneType), if you want your function to gracefully handle None without a static checker flagging it, you must explicitly declare it using typing.Optional or typing.Union.

Use Case 1: If None is Allowed (Standard Practice)

If the parameter is genuinely optional (can be present or absent), use Optional (which is syntactic sugar for Union[str, None]).

from typing import Optional

def process_optional_data(user_id: Optional[str]) -> None:
"""
Explicitly accepts str or None.
"""
if user_id is None:
print("User ID is missing. Skipping processing.")
return

# Static checkers are now happy if None is passed.
print(f"Processing ID length: {len(user_id)}")

# Call with None is now valid for static checkers:
process_optional_data(None)
# Output: User ID is missing. Skipping processing.

Use Case 2: If None Must Be Forbidden (Runtime Enforcement)

If you must ensure the parameter is not None or any other invalid type at the moment the function is called, you must add a runtime check (a conditional statement) to your function's entrance.

def process_strict_data(user_id: str) -> None:
"""
Strictly forbids None and raises an error immediately.
"""
# Runtime Enforcement Check
if user_id is None or not isinstance(user_id, str):
# Raise a meaningful exception immediately
raise TypeError(f"Attribute 'user_id' must be a str, got {type(user_id).__name__}")

print(f"Processing ID length: {len(user_id)}")

# The Call (Raises Type Error at Entrance Check)
try:
process_strict_data(None)
except TypeError as e:
print(f"Strict runtime error: {e}")
# Output: Strict runtime error: Attribute 'user_id' must be a str, got NoneType

Advanced Solutions: Runtime Type Checkers

For large applications where manual runtime checks become tedious, advanced libraries can automatically execute the type hints at the function entrance.

LibraryFunctionEnforcement
PydanticUsed for data validation (e.g., in FastAPI).Raises errors on invalid types, including None where a concrete type is expected.
TypeguardDecorator for existing functions.Wraps function calls and raises errors if input/output types do not match hints.

By default, Python prioritizes dynamic flexibility, making static type checking a layer of defense run by external tools, not by the interpreter itself.