Python Type Hints — Complete Guide with mypy
Type hints (also called type annotations) let you declare the expected types of variables, function parameters, and return values. Python ignores them at runtime, but static type checkers like mypy use them to catch bugs before your code runs.
What You’ll Learn
- Basic type hints:
int,str,float,bool,list,dict,tuple Optional,Union,Any,Literal— expressing flexibilityTypedDictfor structured dictionariesProtocolfor structural subtyping (duck typing)mypyconfiguration and type narrowing
Why Type Hints Matter
Durga Antivirus Pro has hundreds of functions across thousands of lines — type hints catch mismatched arguments before they cause crashes in production. DodaZIP uses typed interfaces for compression modules, making the code self-documenting. Without type hints, another developer (or you, six months later) has to read every function body to understand what it expects.
flowchart LR
A["Functions"] --> B["OOP"]
B --> C["Type Hints"]
C --> D["Testing"]
D --> E["Async"]
A:::done --> B:::done --> C:::current
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#2563eb,stroke:#2563eb,color:#fff
style C fill:#2563eb,stroke:#2563eb,color:#fff
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Type Hints
def greet(name: str, age: int) -> str:
return f"{name} is {age} years old"
def add(a: float, b: float) -> float:
return a + b
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}The syntax is variable: type for parameters and -> type for return values. Standard types (int, str, float, bool) are built-in. Container types (list, dict, tuple) need imported generics in Python 3.8-3.9, or work natively from 3.9+.
Optional, Union, Any
from typing import Optional, Union, Any
# Optional — value could be None
def find_user(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id) # Returns str or None
# Union — value could be one of several types
def process(value: Union[int, str]) -> str:
if isinstance(value, int):
return f"Number: {value}"
return f"String: {value}"
# Any — no type checking (use sparingly)
def log(message: Any) -> None:
print(message)Python 3.10+ introduced | syntax: str | None instead of Optional[str], and int | str instead of Union[int, str].
Generics: List, Dict, Tuple, Set
from typing import List, Dict, Tuple, Set # Python 3.8-3.9
# Python 3.9+: use built-in list, dict, tuple, set
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
coordinates: tuple[float, float] = (10.0, 20.0)
unique_ids: set[int] = {1, 2, 3}
def get_averages(data: list[dict[str, float]]) -> dict[str, float]:
result = {}
for entry in data:
for key, value in entry.items():
result.setdefault(key, []).append(value)
return {k: sum(v)/len(v) for k, v in result.items()}TypedDict — Structured Dictionaries
TypedDict gives dictionaries with fixed keys and specific types:
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
is_active: bool
def create_user(name: str, email: str) -> User:
return {
"id": 1,
"name": name,
"email": email,
"is_active": True,
}
user = create_user("Alice", "alice@example.com")
print(user["name"]) # Alice
# user["missing"] # mypy error: TypedDict 'User' has no key 'missing'Protocol — Structural Subtyping
Protocol defines an interface by its structure (duck typing), not inheritance:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(obj: Drawable) -> None:
print(obj.draw())
render(Circle()) # Drawing circle
render(Square()) # Drawing squareAny object with a draw() -> str method satisfies Drawable — no inheritance required.
Type Narrowing
Type checkers narrow types inside conditionals automatically:
from typing import Optional
def describe(value: Optional[int]) -> str:
if value is None:
return "No value provided"
# mypy knows value is int here
if value < 0:
return f"Negative: {value}"
return f"Positive: {value}"
from typing import Union
def process(value: int | str) -> str:
match value:
case int():
return f"Integer: {value * 2}"
case str():
return f"String: {value.upper()}"Running mypy
Install: pip install mypy
mypy my_script.pySample output:
my_script.py:10: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
my_script.py:15: error: Incompatible return value type (got "int", expected "str")
Found 2 errors in 1 file (checked 1 source file)Create a mypy.ini or pyproject.toml config:
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
warn_unused_configs = trueReal-World Example: Type-Hinted Data Processor
from typing import TypedDict, Optional
class UserData(TypedDict):
user_id: int
name: str
email: str
class ProcessedResult(TypedDict):
user_id: int
name_length: int
domain: Optional[str]
def extract_domain(email: str) -> Optional[str]:
"""Extract domain from email, return None if invalid."""
parts = email.split("@")
if len(parts) != 2:
return None
return parts[1]
def process_users(users: list[UserData]) -> list[ProcessedResult]:
results: list[ProcessedResult] = []
for user in users:
results.append({
"user_id": user["user_id"],
"name_length": len(user["name"]),
"domain": extract_domain(user["email"]),
})
return results
users: list[UserData] = [
{"user_id": 1, "name": "Alice", "email": "alice@example.com"},
{"user_id": 2, "name": "Bob", "email": "bob@example.org"},
]
print(process_users(users))Common Mistakes
1. Using List Instead of list (Python 3.9+)
# Old style (3.8-3.9)
from typing import List
items: List[str] = []
# New style (3.9+)
items: list[str] = []2. Forgetting to Annotate Return Types
Always annotate return types — they help mypy catch when you accidentally return the wrong type.
3. Overusing Any
def process(data: Any) -> Any: # Defeats the purpose!
return dataFix: Be specific. If you accept multiple types, use Union or overloads.
4. Using Optional[X] When the Value Is Never None
def get_positive(n: Optional[int]) -> int: # Wrong — n isn't optional
return abs(n)5. Ignoring mypy Errors
Commenting # type: ignore should be the exception, not the rule. Fix the type issues instead.
Practice Questions
1. What’s the difference between list[int] (3.9+) and typing.List[int]?
Same thing. The built-in syntax is preferred from Python 3.9+.
2. When should you use Protocol vs abstract base classes?
Use Protocol when you want structural subtyping (duck typing). Use ABC when you want explicit inheritance hierarchies.
3. What does Optional[str] mean?str | None — the value can be a string or None.
4. How does type narrowing work in mypy?
mypy analyzes control flow — after an if value is None check, it knows value is not None in the else branch.
Challenge: Write a function merge_dicts(a: dict[str, int], b: dict[str, int]) -> dict[str, int] with full type hints that adds values for overlapping keys.
Solution
def merge_dicts(a: dict[str, int], b: dict[str, int]) -> dict[str, int]:
result = a.copy()
for key, value in b.items():
result[key] = result.get(key, 0) + value
return resultMini Project: Type-Hinted API Client
from typing import TypedDict, Optional
import json
class Post(TypedDict):
userId: int
id: int
title: str
body: str
class APIResult(TypedDict):
success: bool
data: Optional[list[Post]]
error: Optional[str]
def fetch_posts(api_url: str) -> APIResult:
"""Simulate fetching posts from an API."""
sample_data: list[Post] = [
{"userId": 1, "id": 1, "title": "Hello", "body": "World"},
{"userId": 1, "id": 2, "title": "Python", "body": "Typing"},
]
return {"success": True, "data": sample_data, "error": None}
def count_user_posts(posts: list[Post]) -> dict[int, int]:
"""Count how many posts each user has."""
counts: dict[int, int] = {}
for post in posts:
counts[post["userId"]] = counts.get(post["userId"], 0) + 1
return counts
result = fetch_posts("https://jsonplaceholder.typicode.com/posts")
if result["success"] and result["data"] is not None:
print(count_user_posts(result["data"]))Expected output: {1: 2}
What’s Next
Type hints make your code safer and self-documenting. Next, learn context managers for resource management.
| Topic | Description | Link |
|---|---|---|
| Python Context Managers | With-statement patterns | https://tutorials.dodatech.com/programming-languages/python/py-context-managers/ |
| Python Testing | pytest and mocking | https://tutorials.dodatech.com/programming-languages/python/py-testing/ |
| Python Generators | Foundation of iteration | https://tutorials.dodatech.com/programming-languages/python/py-generators/ |
Practice tip: Add type hints to your BankAccount class and run mypy on it. Fix every error until mypy reports success.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro