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
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}]
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:
...
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:
...
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).
"""
...
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)
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
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
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
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:
...
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}."""
...
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"
Common mypy flags:
mypy --strict pipeline.py # maximum strictness
mypy --ignore-missing-imports # skip untyped third-party packages
mypy --pretty # nicer error output
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
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.
Top comments (0)