0%

Type Hints & Static Type Checking with mypy

Add types to your Python code. Catch type errors before runtime. Make your code self-documenting and IDE-friendly.

Python is dynamically typed. You do not declare variable types. This is flexible and convenient. But as codebases grow, dynamic typing can lead to bugs. A function expects an integer, but you pass a string. The error appears at runtime, possibly in production. Type hints are optional annotations that tell programmers (and tools) what type a variable, parameter, or return value should have. They do not change how Python runs. They are ignored at runtime. But they enable powerful tools like mypy to check your code for type errors before execution. This tutorial teaches you to add type hints to your Python code and use mypy to catch type errors early. You will learn basic type hints, collection types, Optional, Union, and practical patterns for real-world code.

🕯️ Magic Note

Type hints were introduced in Python 3.5 (PEP 484). They are entirely optional. You can add them gradually to existing code. They make your code more readable and help IDEs provide better autocomplete and refactoring support.

Why Use Type Hints?
Benefits of adding type hints to your Python code.
  • **Catch bugs early:** mypy finds type errors without running the code
  • **Better IDE support:** autocomplete, refactoring, and warnings
  • **Self-documenting code:** types act as documentation
  • **Easier refactoring:** change a type and mypy shows everywhere it breaks
  • **Better team collaboration:** new developers understand expected types
Basic Type Hints Syntax
Annotate variables, function parameters, and return values.

Python

# Variable type hints (Python 3.6+)

name: str = “Feloriya”

age: int = 25

price: float = 19.99

is_active: bool = True

# Variable without initial value (Python 3.6+)

user_id: int

user_id = 42

# Function parameter and return type hints

def greet(name: str) -> str:

return f”Hello, {name}!”

def add(a: int, b: int) -> int:

return a + b

def divide(a: float, b: float) -> float:

return a / b

💡 Type hints are ignored at runtime. They do not affect performance. They are purely for developers and tools like mypy.
Type Hints for Collections
Use the typing module for lists, dictionaries, tuples, and sets.

Python

from typing import List, Dict, Tuple, Set, Optional, Union, Any

# List of strings

names: List[str] = [“Ali”, “Sara”, “Reza”]

# Dictionary with string keys and integer values

scores: Dict[str, int] = {“Ali”: 95, “Sara”: 87}

# Tuple of string and integer

person: Tuple[str, int] = (“Ali”, 25)

# Set of integers

unique_numbers: Set[int] = {1, 2, 3}

# Nested collections

matrix: List[List[int]] = [[1, 2], [3, 4]]

# Function with collection types

def process_names(names: List[str]) -> List[str]:

return [name.upper() for name in names]

🕯️ Magic Note

In Python 3.9+, you can use built-in types directly without importing from typing: list[str], dict[str, int], tuple[str, int]. This is cleaner and recommended.

Python (Python 3.9+)

# Python 3.9+ syntax (no typing import needed for basics)

names: list[str] = [“Ali”, “Sara”, “Reza”]

scores: dict[str, int] = {“Ali”: 95, “Sara”: 87}

person: tuple[str, int] = (“Ali”, 25)

unique_numbers: set[int] = {1, 2, 3}

Optional and Union Types
Handle values that can be None or multiple types.

Python

from typing import Optional, Union

# Optional means the value can be None or the specified type

def find_user(user_id: int) -> Optional[str]:

if user_id == 1:

return “Ali”

return None # Valid because return type is Optional[str]

# Optional[T] is equivalent to Union[T, None]

def get_config(key: str) -> Union[str, int, None]:

config = {“host”: “localhost”, “port”: 8080}

return config.get(key)

# Union of multiple types (Python 3.10+ has cleaner syntax)

def process(value: Union[int, str]) -> str:

return str(value)

🕯️ Magic Note

Python 3.10 introduced a cleaner syntax for unions: int | str instead of Union[int, str]. Python 3.10+ also supports int | None instead of Optional[int].

Python (Python 3.10+)

# Python 3.10+ union syntax

def process(value: int | str) -> str:

return str(value)

def find_user(user_id: int) -> str | None:

if user_id == 1:

return “Ali”

return None

Type Aliases
Give complex type hints meaningful names.

Python

from typing import List, Dict, Tuple

# Define type aliases

UserId = int

UserScore = float

UserScores = Dict[UserId, UserScore]

Coordinate = Tuple[float, float]

# Use aliases

scores: UserScores = {1: 95.5, 2: 87.0}

point: Coordinate = (10.5, 20.3)

def get_average(scores: UserScores) -> UserScore:

return sum(scores.values()) / len(scores)

Any Type (Opting Out)
Use Any when a value can be truly anything or when adding types is not practical.

Python

from typing import Any

# Any means “no type checking” (mypy will ignore this value)

def flexible(data: Any) -> Any:

return data

# Use Any sparingly. It defeats the purpose of type checking.

⚠️ Use Any sparingly. It tells mypy to skip checking that value. Overusing Any eliminates the benefits of type checking.
Installing and Running mypy
mypy checks your type hints and finds inconsistencies.

Bash

# Install mypy

pip install mypy

# Run mypy on a file

mypy my_script.py

# Run mypy on entire project

mypy .

# Ignore missing imports (for third-party libraries without types)

mypy –ignore-missing-imports my_script.py

Python (Example with a type error)

def greet(name: str) -> str:

return f”Hello, {name}!”

# This will cause a mypy error

greet(42) # Argument 1 to “greet” has incompatible type “int”; expected “str”

# mypy output:

# error: Argument 1 to “greet” has incompatible type “int”; expected “str”

# Found 1 error in 1 file (checked 1 source file)

🕯️ Magic Note

mypy catches type errors without running your code. It analyzes your type hints and the actual values passed. This is static type checking, like in languages such as Java or TypeScript, but optional in Python.

Practical Example: Adding Types to Existing Code
Start adding type hints gradually. Begin with function signatures.

Python

# Before (no type hints)

def calculate_total(prices, tax_rate):

subtotal = sum(prices)

tax = subtotal * tax_rate

return subtotal + tax

# After (with type hints)

from typing import List

def calculate_total(prices: List[float], tax_rate: float) -> float:

subtotal = sum(prices)

tax = subtotal * tax_rate

return subtotal + tax

# Now mypy can catch errors like:

# calculate_total([10, 20, “30”], 0.1) # error: List item has incompatible type “str”

Practical Example: Complete Class with Type Hints
Type hints make classes self-documenting.

Python

from typing import List, Optional

class Student:

def __init__(self, name: str, student_id: int) -> None:

self.name: str = name

self.student_id: int = student_id

self.grades: List[float] = []

def add_grade(self, grade: float) -> None:

if 0 <= grade <= 100:

self.grades.append(grade)

else:

raise ValueError(f”Invalid grade: {grade}”)

def get_average(self) -> Optional[float]:

if not self.grades:

return None

return sum(self.grades) / len(self.grades)

def __str__(self) -> str:

return f”Student(name={self.name}, id={self.student_id})”

mypy Configuration (mypy.ini)
Configure mypy behavior for your project.

INI (mypy.ini)

[mypy]

python_version = 3.11

warn_return_any = True

warn_unused_configs = True

disallow_untyped_defs = True

ignore_missing_imports = True

[mypy-my_package.*]

disallow_untyped_defs = True

[mypy-tests.*]

disallow_untyped_defs = False

Runtime Type Checking with isinstance()
Type hints are ignored at runtime. Use isinstance() for actual runtime checks.

Python

def safe_divide(a: float, b: float) -> float:

# Runtime check (not guaranteed by type hints)

if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):

raise TypeError(“Both arguments must be numbers”)

if b == 0:

raise ValueError(“Cannot divide by zero”)

return a / b

💡 Type hints are for developers and tools. They do not enforce types at runtime. Use isinstance() or third-party libraries like pydantic for runtime validation.
Common Mistakes with Type Hints
  • Assuming type hints enforce types at runtime (they do not)
  • Overusing Any (defeats purpose of type checking)
  • Forgetting to run mypy (type hints alone do nothing)
  • Adding type hints to code that will never be checked
  • Using Optional when Union[T, None] is intended
Check Your Understanding
  • Write a type hint for a list of integers.
  • What is the difference between Optional[int] and int?
  • How do you run mypy on a Python file?
  • Write a function signature that takes a string and returns an optional integer.
  • What is a type alias and when would you use it?

⚡ Whisper

Type hints are optional. Python will run without them. But optional does not mean useless. Type hints are documentation that never goes out of date. They are IDE superpowers. They are mypy’s eyes. They catch the mistakes you would find at 2 AM. Adding def greet(name: str) -> str: takes a second. It saves hours. Start with function signatures. Add List[str] for collections. Add Optional for None. Run mypy. Fix the errors. Then commit. Your future self will thank you. Your teammates will thank you. The code will be clearer, safer, and more professional. Type hints are not burden. They are clarity. Use them.

Related posts