Skip to content

Python Type Hints

Imagine you are sending a package to a friend. If you write “Fragile” and “This Side Up” on the package, the courier will know to handle it with care and keep it upright. Type Hints play a similar role in programming — they are a technique for adding “description labels” to code, clearly indicating what data type variables, function parameters, and return values should be.

Simply put, type hints are syntax for annotating data types in code. Their core purposes are:

  • Improve Code Readability: Let others (and your future self) understand the intent of the code at a glance
  • Facilitate Static Checking: Discover potential type errors through tools before running the code
  • Enhance IDE Support: Enable code editors to provide more accurate auto-completion and hints

A simple example:

# Without type hints
def greet(name):
    return f"Hello, {name}"

# With type hints
def greet(name: str) -> str:
    return f"Hello, {name}"

The second piece of code clearly indicates that name should be a string type (str) and the function will return a string (-> str).


Python is famous for its dynamic typing feature — you don’t need to declare the type of a variable in advance; the interpreter automatically infers it at runtime. While this is flexible, it also brings problems:

  1. Hard-to-Understand Code: When you see a function, it’s unclear what type of data should be passed in
  2. Hidden Bugs: You might accidentally pass in the wrong type, and the error only appears at runtime
  3. Low Development Efficiency: IDEs cannot provide accurate code hints and completions

Type hints solve these problems by providing optional type information, making your code more robust and maintainable.


Starting from Python 3.6, you can directly add type annotations to variables:

# Code without type annotations
name = "Alice"
age = 30
is_student = False
scores = [95, 88, 91]

# Code with type annotations
name: str = "Alice"       # Annotated as string (str)
age: int = 30             # Annotated as integer (int)
is_student: bool = False  # Annotated as boolean (bool)
scores: list = [95, 88, 91] # Annotated as list (list)

Explanation: name: str reads as “the type of the variable name is str.”

Add : type after function parameters.

# Function without type annotations
def greet(first_name, last_name):
    full_name = first_name + " " + last_name
    return "Hello, " + full_name

# Function with type annotations
def greet(first_name: str, last_name: str) -> str:
    full_name = first_name + " " + last_name
    return "Hello, " + full_name

Interpreting this function:

  • first_name: str: The parameter first_name should be a string.
  • last_name: str: The parameter last_name should be a string.
  • -> str: This function will return a string after execution.

Now, anyone calling this function can clearly know what to pass and what to get back.

Function annotations are the most common application scenario for type hints:

def add_numbers(a: int, b: int) -> int:
    """Add two integers and return the result"""
    return a + b

# Call the function
result = add_numbers(5, 3)  # Correct: two integers
# result = add_numbers("5", "3")  # Potentially problematic: though it runs, the type checker will warn

You can use type annotations and default values simultaneously:

def say_hello(name: str, times: int = 1) -> str:
    """Greet someone a specified number of times"""
    return " ".join([f"Hello, {name}!"] * times)

print(say_hello("Bob"))      # Output: Hello, Bob!
print(say_hello("Alice", 3)) # Output: Hello, Alice! Hello, Alice! Hello, Alice!

Basic str, int, list are great, but what if we want to express “a list of integers”? This is where Python’s typing module comes in, providing more powerful tools.

from typing import List, Dict, Tuple, Set

# List[int] indicates this is a list containing only integers
numbers: List[int] = [1, 2, 3, 4, 5]

# Dict[str, int] indicates this is a dictionary with string keys and integer values
student_scores: Dict[str, int] = {"Alice": 95, "Bob": 88}

# Tuple[int, str, bool] indicates this is a tuple containing an integer, string, and boolean
person_info: Tuple[int, str, bool] = (25, "Alice", True)

# Set[str] indicates this is a set containing only strings
unique_names: Set[str] = {"Alice", "Bob", "Charlie"}

Used when the value might be a certain type or None:

from typing import Optional

def find_student(name: str) -> Optional[str]:
    """Find a student by name, may be found or return None"""
    students = {"Alice": "A001", "Bob": "B002"}
    return students.get(name)  # May return a string or None

# Equivalent to Union[str, None]

Used when the value might be one of several types:

from typing import Union

def process_input(data: Union[str, int, List[int]]) -> None:
    """Process input that may be a string, integer, or list of integers"""
    if isinstance(data, str):
        print(f"String: {data}")
    elif isinstance(data, int):
        print(f"Integer: {data}")
    elif isinstance(data, list):
        print(f"List: {data}")

process_input("hello")    # Output: String: hello
process_input(42)         # Output: Integer: 42  
process_input([1, 2, 3])  # Output: List: [1, 2, 3]

Mypy is the most popular Python type checker. First install it:

pip install mypy

Suppose we have a file example.py with a potential type issue:

# example.py
def add_numbers(a: int, b: int) -> int:
    return a + b

result = add_numbers("5", "3")  # There's a problem here! Strings are passed in

Run mypy to check:

mypy example.py

You will see output similar to this:

example.py:4: error: Argument 1 to "add_numbers" has incompatible type "str"; expected "int"
example.py:4: error: Argument 2 to "add_numbers" has incompatible type "str"; expected "int"
Found 2 errors in 1 file (checked 1 source file)

Modern IDEs (such as VS Code, PyCharm) have built-in type checking support:

  1. Error Highlighting: Code with type mismatches will be marked
  2. Smart Hints: Type information of parameters and return values will be displayed when entering code
  3. Auto-Completion: More accurate code completion suggestions based on type information

  • Start using type annotations from new code
  • Gradually add annotations to important old code
  • No need to add types to all code at once
  • Maintain a consistent annotation style in the project
  • The team negotiates to decide the level of detail for annotations
# Not recommended: Types that are too obvious don't need annotations
x: int = 5  # 5 is obviously an integer, the annotation can be omitted

# Recommended: Add annotations for complex logic or public interfaces
def calculate_statistics(data: List[float]) -> Dict[str, float]:
    """Calculate various statistical indicators of the data"""
    # Complex implementation...

For third-party libraries without type annotations, you can:

  • Check if there are corresponding type stub files (usually called types-packageName)
  • Use the Any type to temporarily bypass checks
  • Or add your own type annotations for commonly used functions

No. Type hints are ignored at runtime and are only used for static analysis and development tools.

Not mandatory. Python is still a dynamically typed language, and type hints are optional. However, it is strongly recommended, especially for large projects.

What happens if the annotations are wrong?

Section titled “What happens if the annotations are wrong?”

The type checker will report an error, but the program can still run. Annotations are only “hints,” not “enforcement.”


Type hints are a powerful tool for improving code quality. Let’s consolidate what we’ve learned through a comprehensive exercise:

from typing import List, Dict, Optional, Union

def process_students(students: List[Dict[str, Union[str, int]]]) -> Optional[float]:
    """
    Process student data, calculate the average score
    
    Parameters:
        students: List of students, each student is a dictionary containing 'name' and 'score'
        
    Returns:
        Average score (float), returns None if there are no students
    """
    if not students:
        return None
    
    total = 0
    for student in students:
        total += student['score']
    
    return total / len(students)

# Test data
students_data = [
    {"name": "Alice", "score": 95},
    {"name": "Bob", "score": 88},
    {"name": "Charlie", "score": 92}
]

average = process_students(students_data)
print(f"Average score: {average}")

The output is:

Average score: 91.66666666666667