Python Type Hints: A Beginner-Friendly Guide (2026)

Python is dynamically typed — you never have to declare types. But since Python 3.5, you can add optional type hints that make your code self-documenting, catch bugs before they happen, and give your IDE superpowers. In 2026, type hints are standard practice in every production Python codebase.

1. Why Use Type Hints?

  • Better IDE support: Autocomplete, refactoring, and error detection improve dramatically with type hints
  • Catch bugs before runtime: Tools like mypy find type errors without running your code
  • Self-documenting code: Types tell you what a function expects and returns — no guessing
  • Team collaboration: New team members understand code faster
Type hints are optional. They never affect runtime behavior. Python ignores them completely. They are purely for development tooling and documentation.

2. Basic Type Hints

Primitive Types

name: str = "Alice"
age: int = 30
price: float = 19.99
is_active: bool = True

Function Signatures

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

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

def calculate_discount(price: float, discount_pct: float) -> float:
    return price * (1 - discount_pct / 100)

# Function that returns nothing
def log_message(msg: str) -> None:
    print(f"[LOG] {msg}")

Collections

from typing import List, Dict, Set, Tuple

names: List[str] = ["Alice", "Bob", "Charlie"]
scores: Dict[str, int] = {"Alice": 95, "Bob": 87}
unique_ids: Set[int] = {1, 2, 3}
coordinate: Tuple[float, float] = (40.7, -74.0)

3. Optional and Union Types

Optional (can be None)

from typing import Optional

def find_user(user_id: int) -> Optional[str]:
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)  # Returns str or None

result = find_user(3)  # result is Optional[str], could be None

if result is not None:
    print(result.upper())  # mypy knows result is str here

Union (multiple possible types)

from typing import Union

def process_input(data: Union[str, int, List[str]]) -> str:
    if isinstance(data, int):
        return str(data * 2)
    elif isinstance(data, list):
        return ", ".join(data)
    return data

In Python 3.10+, use the cleaner | syntax:

def process_input(data: str | int | list[str]) -> str:
    if isinstance(data, int):
        return str(data * 2)
    elif isinstance(data, list):
        return ", ".join(data)
    return data

4. Complex Type Hints

TypedDict (typed dictionaries)

from typing import TypedDict

class User(TypedDict):
    name: str
    age: int
    email: str

def create_user(data: User) -> str:
    return f"Created user {data['name']}, age {data['age']}"

alice: User = {"name": "Alice", "age": 30, "email": "alice@example.com"}
print(create_user(alice))

Callable (functions as arguments)

from typing import Callable

def apply(func: Callable[[int, int], int], x: int, y: int) -> int:
    return func(x, y)

def multiply(a: int, b: int) -> int:
    return a * b

result = apply(multiply, 3, 4)  # 12

5. Static Checking with mypy

ⓘ Tip: Start with mypy --strict on new projects. For existing codebases, begin with no flags and add --disallow-untyped-defs to catch new untyped functions. Adding all strict checks at once on legacy code produces hundreds of errors — not productive.

Type hints alone do nothing. mypy is the tool that checks them:

# Install
pip install mypy

# Check your code
mypy my_script.py

Example: catching a bug before running

# buggy.py
def double(n: int) -> int:
    return n * 2

print(double("hello"))  # mypy catches this BEFORE runtime
$ mypy buggy.py
buggy.py:4: error: Argument 1 to "double" has incompatible type "str"; expected "int"

6. Best Practices in 2026

  1. Start with public APIs: Add type hints to function signatures first. Internal variables can wait.
  2. Use modern syntax: list[str] not List[str], str | None not Optional[str] (Python 3.10+)
  3. Add mypy to CI: Run mypy on every pull request. Do not let untyped code accumulate.
  4. Incrementally adopt: Add --strict gradually. Start with --disallow-untyped-defs for new functions.
  5. Use type stubs for libraries: Many popular libraries ship type stubs (types-requests, types-pytz). Install them.
Type hints are a gradient, not a binary. You do not need 100% coverage to get value. Even 20% coverage on public APIs makes a massive difference.

7. Common Mistakes

7.1. Using List Instead of list in Python 3.9+

In Python 3.9+, you can write list[str] directly without importing from typing. The capitalized versions (List, Dict, Tuple) are deprecated and will be removed in a future Python version. Use lowercase built-in generics instead.

# Deprecated (Python 3.5-3.8 style, still works but not recommended)
from typing import List
names: List[str] = []

# Modern (Python 3.9+)
names: list[str] = []

7.2. Forgetting That Type Hints Are Optional at Runtime

Type hints do not enforce anything at runtime. This code runs without errors even though it violates the type annotation:

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

add("hello", "world")  # No error at runtime, mypy catches this

Always run mypy as part of your workflow. Type hints without a checker are just comments.

7.3. Over-Annotating Internal Variables

Annotating every local variable creates visual noise. Focus type annotations on function signatures, class attributes, and public APIs. The type checker infers types for local variables from their initial values.

# Over-annotated — mypy already knows x is int
def calculate() -> int:
    x: int = 5
    y: int = x * 2
    return y

# Clean — annotate the boundary, let inference handle the rest
def calculate() -> int:
    x = 5
    y = x * 2
    return y

FAQ

Do type hints make Python slower?

No. Type hints are ignored at runtime. They have zero performance impact.

Should I add type hints to existing large codebases?

Add them gradually. Start with new functions and public APIs. Use mypy in loose mode initially, tighten over time.

What is the difference between typing and type annotations?

Type annotations are the syntax (name: str). The typing module provides advanced type constructs (Optional, Union, TypedDict).