Python’s type hints have revolutionized how developers write robust, maintainable code. Starting with the simple from typing import List import, this tutorial takes you from basic list typing through advanced typing concepts and culminates in using Pydantic for powerful data validation and serialization. Whether you’re building APIs, handling configurations, or just want cleaner code, these tools will transform your Python workflow.
Why Type Hints Matter in Modern Python
Type hints, introduced in Python 3.5 via PEP 484, allow you to annotate variables, function parameters, and return types without affecting runtime behavior. Static type checkers like mypy catch errors early, IDEs provide better autocomplete, and your code becomes self-documenting.
Key benefits include:
- Error detection at development time, not runtime[1][6]
- Improved readability –
def process(items: List[str]) -> stris crystal clear[2][3] - Better tooling – auto-completion and refactoring in VS Code, PyCharm, etc.[5][7]
Since Python 3.9, you can use built-in list[str] directly, but typing.List remains useful for older codebases and advanced features[5][6].
Section 1: Understanding Lists in Python
Before typing lists, grasp the fundamentals. Python lists are mutable, ordered sequences created via literals or the list() constructor[1].
Creating Lists
# List literals – most common
shapes = ['square', 'rectangle', 'triangle', 'pentagon', 'circle'][1]
# From iterables
numbers = list((1, 2, 3, 4)) # Tuple to list
letters = list("Pythonista") # String to list: ['P', 'y', ...][1]
empty_list = list() # []
Modifying Lists
Lists support indexing, slicing, and mutation:
numbers = [1, 2, 3, 4]
numbers = "one" # ['one', 2, 3, 4]
numbers[-1] = "four" # Negative indexing works[1]
numbers.append(5) # Grow the list
Pro Tip: Lists are dynamic but untyped by default –
numberscould be int or str without hints!
Section 2: Basic Type Hints with typing.List
Import List from typing to specify list element types[2][3].
Simple List Annotations
from typing import List
def process_items(items: List[str]) -> None:
for item in items:
print(item.upper())
strings: List[str] = ['apple', 'banana']
process_items(strings) # Type checker: all good!
Pre-Python 3.9: Use List[str].
Python 3.9+: list[str] works natively[3][5].
Common Collection Types
| Type | Pre-3.9 Import | Modern Syntax | Example |
|---|---|---|---|
| List | List[T] | list[T] | list[int][1][5] |
| Dict | Dict[K, V] | dict[K, V] | dict[str, int][4][5] |
| Set | Set[T] | set[T] | set[str][3][5] |
| Tuple | Tuple[T1, T2] | tuple[T1, T2] | tuple[int, str][3][6] |
from typing import Dict, Tuple, Set
inventory: Dict[str, int] = {"apples": 5, "bananas": 3}[4]
point: Tuple[float, float] = (3.0, 4.0)[5]
authors: Set[str] = {"Bob", "Eve"}[5]
Section 3: Advanced Typing Concepts
Move beyond basics with Sequence, Union, and more from typing[2][6].
Generic Protocols Like Sequence
Instead of List[str], use Sequence[str] for broader compatibility (works with lists, tuples, etc.):
from typing import Sequence
def get_fifth_element(things: Sequence[str]) -> str:
if len(things) < 5:
raise ValueError("Need at least 5 elements")
return things[4] # Works with lists OR tuples![2]
Test it:
elements_list = ["earth", "wind", "water", "fire", "multipass"]
elements_tuple = ("earth", "wind", "water", "fire", "multipass")
print(get_fifth_element(elements_list)) # "multipass"
print(get_fifth_element(elements_tuple)) # "multipass"
Unions and Literals (Python 3.10+)
from typing import List, Union
def filter_things(things: List[str] | tuple[str, ...] | set[str]) -> List[str]:
return [t for t in things if "bad" not in t] # Union type[2]
Section 4: Static Type Checking with mypy
Install mypy: pip install mypy. Run mypy your_file.py to catch errors[7].
Good code:
def safe_func(mapping: dict[int, str]) -> list[int]:
return list(mapping.keys())
mypy catches this:
def bad_func(mapping: dict[int, str]) -> list[int]:
mapping[5] = 'maybe' # Error: dict[int,str] not mutableMapping[7]
Use MutableMapping for writable dicts[7].
Section 5: Pydantic – Runtime Validation Supercharged
Pydantic (v2+) builds on type hints for data validation, parsing, and serialization. Perfect for APIs (FastAPI), configs, and JSON handling. Install: pip install pydantic[email protected].
Core Concepts
Pydantic models are classes inheriting from BaseModel. Fields use type hints and validate at runtime.
from pydantic import BaseModel
from typing import List
from datetime import date
class User(BaseModel):
id: int
name: str
age: int | None = None # Optional with default
hobbies: List[str] = [] # Typed list default
signup_date: date
# Automatic validation + parsing
user_data = {
"id": 1,
"name": "Alice",
"hobbies": ["reading", "coding"],
"signup_date": "2025-12-12"
}
user = User(**user_data) # Validates types, raises ValidationError if invalid!
print(user.name) # "Alice" – full autocomplete + intellisense
print(user.model_dump()) # JSON-ready dict
List Validation with Pydantic
class Team(BaseModel):
name: str
members: List[User] # Nested typed lists!
team_data = {
"name": "DevOps",
"members": [ {"id": 1, "name": "Bob", "signup_date": "2025-01-01"},
{"id": 2, "name": "Carol", "signup_date": "2025-02-01"}
]
}
team = Team(**team_data)
print(team.members.name) # "Bob" – deeply validated!
Error Handling:
try:
bad_user = User(name="Eve", signup_date="invalid-date") # Raises ValidationError
except ValueError as e:
print(e) # Clear error: "Input should be a valid date..."
Advanced Pydantic Features
- Validators:
@field_validator('age')for custom logic - Serialization:
user.model_dump_json()for APIs - Settings Management:
pydantic-settingsfor env vars - Generic Models:
GenericModelwithList[TypeVar]
from pydantic import field_validator
class ValidatedUser(User):
@field_validator('age')
@classmethod
def check_age(cls, v):
if v and v < 0:
raise ValueError('Age cannot be negative')
return v
Real-World Integration: FastAPI + Pydantic
Pydantic powers FastAPI for automatic API docs and validation[3].
from fastapi import FastAPI
app = FastAPI()
@app.post("/users/")
def create_user(user: User): # Auto-validates request body!
return user.model_dump()
Best Practices and Gotchas
- Use
Sequence[T]overList[T]when order matters but mutation doesn’t[2] - Prefer built-ins (
list[str]) in Python 3.9+[5] - Run mypy in CI – catches 90% of type errors early[7]
- Pydantic v2: Faster, stricter – migrate with
pydantic v1 compatif needed - Avoid
Any: Be specific for max benefits[2]
| Mistake | Fix |
|---|---|
List[int, str] | List[Union[int, str]] or list[int | str][6] |
| Mutable dict read-only | Use MutableMapping[7] |
| No defaults for optionals | age: int | None = None |
Conclusion: Level Up Your Python Code Today
From from typing import List to Pydantic’s runtime power, type hints make Python enterprise-ready. Start small: add hints to your next function. Scale up: integrate Pydantic for configs or APIs. Your future self (and team) will thank you for fewer bugs and faster development.
Next Steps:
- Install
mypyandpydantic - Type-annotate one file, run
mypy - Build a Pydantic model for your data
- Explore
TypedDict,dataclasseswith hints
Happy typing! 🚀