DEV Community

Cover image for Python Type Hints: A Practical Beginner's Guide
German Yamil
German Yamil

Posted on

Python Type Hints: A Practical Beginner's Guide

Python Type Hints: A Practical Beginner's Guide

Type hints don't change how Python runs your code. They change how you read it, debug it, and catch bugs before they happen.

Here's everything you need to start using them effectively.


๐ŸŽ Free: AI Publishing Checklist โ€” 7 steps in Python ยท Full pipeline: germy5.gumroad.com/l/xhxkzz (pay what you want, min $9.99)


The basics: annotating variables and functions

# Variables
name: str = "Alice"
age: int = 30
price: float = 9.99
active: bool = True

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

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

def log_event(message: str) -> None:
    print(f"[LOG] {message}")
    # returns None, so -> None
Enter fullscreen mode Exit fullscreen mode

The syntax: parameter: type for inputs, -> type for the return value.

Python does not enforce these at runtime โ€” they're hints for your IDE, linter, and teammates. greet(42) won't raise an error. But your editor will flag it, and mypy will catch it.

Built-in types

# Primitives
x: int = 1
y: float = 3.14
s: str = "hello"
b: bool = True

# Collections (Python 3.9+ โ€” lowercase, no import needed)
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
coords: tuple[float, float] = (1.0, 2.5)
unique: set[int] = {1, 2, 3}

# Nested
nested: list[dict[str, int]] = [{"a": 1}, {"b": 2}]
Enter fullscreen mode Exit fullscreen mode

For Python 3.8 and earlier, you need from typing import List, Dict, Tuple, Set and use capital letters (List[str]). Python 3.9+ supports lowercase built-ins directly.

Optional: values that can be None

from typing import Optional

# Optional[str] means str or None
def find_user(user_id: int) -> Optional[str]:
    if user_id == 1:
        return "Alice"
    return None  # explicitly allowed

# Shorthand (Python 3.10+)
def find_user(user_id: int) -> str | None:
    ...
Enter fullscreen mode Exit fullscreen mode

Optional[X] is exactly X | None. The | syntax is cleaner and available from Python 3.10.

Union: one of several types

from typing import Union

# Accepts int or str
def process(value: Union[int, str]) -> str:
    return str(value)

# Python 3.10+ shorthand
def process(value: int | str) -> str:
    return str(value)

# Useful example: ID can be int or string depending on source
def get_task(task_id: int | str) -> dict:
    ...
Enter fullscreen mode Exit fullscreen mode

Common patterns in automation code

from typing import Optional
from pathlib import Path

def load_state(path: Path) -> dict[str, str]:
    """Load pipeline state from JSON file."""
    ...

def save_result(
    task_id: str,
    output: str,
    error: Optional[str] = None,
) -> None:
    """Save task result to state file."""
    ...

def run_task(
    code: str,
    timeout: int = 30,
    env: Optional[dict[str, str]] = None,
) -> tuple[bool, str, str]:
    """
    Run code in subprocess.
    Returns (success, stdout, stderr).
    """
    ...
Enter fullscreen mode Exit fullscreen mode

When a function has 3+ parameters, type hints become essential documentation.

Callable: functions as arguments

from typing import Callable

# A function that takes a str and returns bool
def filter_items(
    items: list[str],
    predicate: Callable[[str], bool],
) -> list[str]:
    return [item for item in items if predicate(item)]

# Usage
result = filter_items(["hello", "world", ""], lambda s: len(s) > 0)
Enter fullscreen mode Exit fullscreen mode

Callable[[arg_types], return_type] โ€” input types in a list, return type after.

TypedDict: typed dictionaries

When you pass dicts around with fixed keys, TypedDict documents the structure:

from typing import TypedDict, Optional

class TaskState(TypedDict):
    id: str
    name: str
    status: str
    retries: int
    error: Optional[str]

def process_task(task: TaskState) -> None:
    print(f"Processing: {task['name']}")
    # IDE now knows task has 'id', 'name', 'status', 'retries', 'error'
    # and will flag task['typo'] as an error
Enter fullscreen mode Exit fullscreen mode

vs. dict with no type info:

def process_task(task: dict) -> None:
    # IDE has no idea what's in here
    print(f"Processing: {task['name']}")  # could be anything
Enter fullscreen mode Exit fullscreen mode

Literal: restrict to specific values

from typing import Literal

Status = Literal["pending", "running", "done", "failed"]

def update_status(task_id: str, status: Status) -> None:
    ...

update_status("t01", "done")     # โœ…
update_status("t01", "unknown")  # โŒ mypy catches this
Enter fullscreen mode Exit fullscreen mode

Type aliases: name complex types

from typing import TypeAlias

# Instead of repeating dict[str, list[str]] everywhere
TagMap: TypeAlias = dict[str, list[str]]
ArticleId: TypeAlias = int
FilePath: TypeAlias = str

def get_article_tags(article_id: ArticleId) -> TagMap:
    ...
Enter fullscreen mode Exit fullscreen mode

Real-world typed functions from the pipeline

from pathlib import Path
from typing import Optional
from dataclasses import dataclass

@dataclass
class ValidationResult:
    passed: bool
    exit_code: int
    stdout: str
    stderr: str
    timed_out: bool = False

    def __bool__(self) -> bool:
        return self.passed


def validate_syntax(code: str) -> tuple[bool, str]:
    """Check syntax with ast.parse. Returns (valid, error_msg)."""
    ...

def run_isolated(
    code: str,
    timeout: int = 30,
    extra_env: Optional[dict[str, str]] = None,
) -> ValidationResult:
    """Run code in a temp subprocess. Returns structured result."""
    ...

def publish_article(
    filepath: Path,
    api_token: str,
    series: Optional[str] = None,
) -> dict[str, str | int]:
    """Publish to Dev.to. Returns {"id": int, "url": str}."""
    ...
Enter fullscreen mode Exit fullscreen mode

With these hints, any teammate (or your future self) can understand the contract of each function without reading the body.

Checking hints with mypy

Type hints are optional but a linter enforces them:

pip install mypy

# Check a single file
mypy pipeline.py

# Check entire project
mypy .

# Common output
# pipeline.py:42: error: Argument 1 to "run_isolated" has incompatible type "int"; expected "str"
Enter fullscreen mode Exit fullscreen mode

Common mypy flags:

mypy --strict pipeline.py      # maximum strictness
mypy --ignore-missing-imports  # skip untyped third-party packages
mypy --pretty                  # nicer error output
Enter fullscreen mode Exit fullscreen mode

You don't need to fix everything at once. Add # type: ignore on a line to skip it temporarily.

When NOT to annotate

Type hints add value when:

  • Functions have 3+ parameters
  • The return type isn't obvious
  • A variable could be multiple types

Skip them for:

  • Simple one-liners: total = sum(prices) โ€” obvious
  • Local variables inside a function when the type is clear from assignment
  • Throwaway scripts you'll delete tomorrow

Quick reference

# Basic
x: int
x: float
x: str
x: bool

# Collections (3.9+)
x: list[str]
x: dict[str, int]
x: tuple[str, int, bool]
x: set[str]

# Nullable
x: str | None         # 3.10+
x: Optional[str]      # all versions

# Multiple types
x: str | int          # 3.10+
x: Union[str, int]    # all versions

# Function types
f: Callable[[str, int], bool]

# Return nothing
def fn() -> None: ...

# Any type (escape hatch)
from typing import Any
x: Any
Enter fullscreen mode Exit fullscreen mode

The pipeline uses type hints on every public function โ€” the IDE autocomplete during development alone saves an hour per ebook: germy5.gumroad.com/l/xhxkzz โ€” pay what you want, min $9.99.


Further Reading

Top comments (0)