Python is a dynamically typed language, which means variables don’t need explicit type declarations. However, as projects grow, this flexibility can lead to bugs, poor readability, and confusion.
To solve this, Python introduced type hints (PEP 484), which let you add type information to your code. While Python won’t enforce these at runtime, tools like MyPy can perform static type checking to catch issues early.
In this article, you’ll learn everything from basic type hints to advanced features like TypedDict, dataclasses, and generics, along with practical examples.
Why Use Type Hints?
- Improved readability – Code becomes self-documenting.
- Better IDE support – Autocompletion and linting work more effectively.
- Fewer bugs – Catch errors before running the code.
- Scalability – Large projects become easier to maintain.
1. Basic Variable Type Hints
name: str = "Alice"
age: int = 30
With type hints, your IDE or MyPy will warn if you try to reassign with a wrong type:
age = "thirty" # MyPy will raise an error
2. Function Type Hints
You can specify parameter types and return types:
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old."
Output:
Hello Alice, you are 30 years old.
3. Optional and Union Types
Sometimes parameters can accept multiple types.
from typing import Optional
def create_user(name: str, age: Optional[int] = None) -> dict[str, str | int | None]:
return {"name": name, "age": age}
Here, age can be an int or None.
4. Type Aliases
Type aliases improve readability when working with complex types.
User = dict[str, str | int | None]
def create_user(name: str, age: int | None) -> User:
return {"name": name, "age": age}
5. NewType for Safer Distinctions
If two values share the same underlying type but mean different things, use NewType.
from typing import NewType
UserId = NewType("UserId", int)
ProductId = NewType("ProductId", int)
def get_user(user_id: UserId) -> str:
return f"Fetching user {user_id}"
Now, passing a ProductId by mistake will raise a type error.
6. TypedDict for Structured Dictionaries
TypedDict ensures each key has a specific type.
from typing import TypedDict
class User(TypedDict):
name: str
age: int
email: str
user: User = {"name": "Alice", "age": 30, "email": "alice@example.com"}
If you miss a key or use the wrong type, MyPy will flag it.
7. Dataclasses with Type Hints
Dataclasses are great for defining structured objects with minimal boilerplate.
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
name: str
age: Optional[int] = None
email: Optional[str] = None
user = User("Alice", 30, "alice@example.com")
print(user)
Output:
User(name='Alice', age=30, email='alice@example.com')
8. Generics for Reusable Functions
Generics let you define flexible yet type-safe functions.
from typing import TypeVar
T = TypeVar("T")
def get_first_item(items: list[T]) -> T:
return items[0]
print(get_first_item([1, 2, 3])) # 1 (int)
print(get_first_item(["a", "b", "c"])) # 'a' (str)
The function adapts to any type of list.
9. Using Type Hints with Third-Party Libraries
Not all libraries include type hints. You can install stub packages, e.g., for requests:
pip install types-requests
Now MyPy can check your usage of requests with proper type information.
10. Best Practices
- Add type hints gradually—don’t rewrite everything at once.
- Inputs should be generic, outputs should be specific.
- Use Optional for parameters with None defaults.
- Prefer dataclasses over dictionaries for structured objects.
- Use NewType to differentiate logically distinct values.