Gyaan

Type Hints and Annotations

intermediate type-hints typing annotations mypy

Python is dynamically typed — we don’t have to declare types. But starting with Python 3.5, we can optionally add type hints. They don’t change how the code runs. They’re notes for humans, IDEs, and type checkers like mypy.

Basic Type Hints

We add types to function parameters and return values with : and ->.

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

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

age: int = 25
name: str = "Manish"
is_active: bool = True

If we pass wrong types, Python won’t stop us. Type hints are not enforced at runtime. But our IDE will highlight the mistake, and mypy will catch it.

Common Types

For simple types, we just use the built-in names:

  • int, float, str, bool — basic types
  • None — for functions that return nothing
  • bytes — for binary data
def process(data: str) -> None:
    print(data)  # returns None implicitly

Collections

For containers, Python 3.9+ lets us use built-in types directly. Before 3.9, we import from typing.

# Python 3.9+ (preferred)
def get_names() -> list[str]:
    return ["Alice", "Bob"]

scores: dict[str, int] = {"math": 95, "science": 88}
point: tuple[float, float] = (3.14, 2.71)

# Python 3.5-3.8 (use typing imports)
from typing import List, Dict, Tuple
def get_names() -> List[str]:
    return ["Alice", "Bob"]

Optional and Union

When a value could be one of several types, we use Union. When it could be a type or None, we use Optional.

from typing import Optional, Union

def find_user(user_id: int) -> Optional[str]:
    # Returns str or None
    if user_id == 1:
        return "Manish"
    return None

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

# Python 3.10+ — cleaner syntax with |
def find_user(user_id: int) -> str | None:
    ...

def parse(value: str | int) -> str:
    return str(value)

Optional[str] is just shorthand for Union[str, None].

TypeVar: Generic Functions

When we want a function that works with any type but preserves the type relationship:

from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

# Type checker knows: first([1, 2, 3]) returns int
# Type checker knows: first(["a", "b"]) returns str

Protocol: Structural Typing

Protocols let us define “interfaces” without inheritance. If an object has the right methods, it matches.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("Drawing circle")

def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())  # works — Circle has a draw() method

Circle doesn’t inherit from Drawable. It just happens to have the right method. This is structural typing — “if it fits, it works.”

Using mypy

mypy is the most popular static type checker. It reads our type hints and reports errors without running the code.

# example.py
def double(x: int) -> int:
    return x * 2

result: str = double(5)  # mypy will flag this
$ mypy example.py
error: Incompatible types in assignment (expression has type "int", variable has type "str")

Common Patterns

from typing import Callable, Any

# Function that takes a callback
def retry(func: Callable[..., Any], times: int = 3) -> Any:
    for _ in range(times):
        try:
            return func()
        except Exception:
            continue

# Type alias
UserID = int
Scores = dict[str, list[int]]

def get_scores(user: UserID) -> Scores:
    return {"math": [90, 85, 92]}

In simple language, type hints are like lane markings on a road. The car can still drive anywhere, but the markings help everyone stay safe. We write them for our future selves, our teammates, and our tools.