Of course! In Python, "annotation" most commonly refers to type hints. This is a powerful feature that allows you to add type information to your code, which can then be used by various tools for static analysis, IDEs for autocompletion and error-checking, and even at runtime.

Let's break down everything you need to know about Python annotations.
What are Type Hints?
Type hints are a way of declaring the expected data type of a variable, function parameter, or return value. They are not enforced by the Python interpreter at runtime (by default), but they serve as a form of documentation and a contract for your code.
Before Type Hints (Python < 3.5):
def add(a, b):
return a + b
result = add(10, 20) # Works fine
result = add("hello", "world") # Also works, but is this intended?
This code works, but it's not clear what types a and b should be. It could accept numbers, strings, lists, etc.

With Type Hints (Python 3.5+):
def add(a: int, b: int) -> int:
return a + b
result = add(10, 20) # Works fine
result = add("hello", "world") # Also works, but a type checker will flag this!
The a: int part means "parameter a is expected to be an integer." The -> int part means "this function is expected to return an integer."
The Basics of Syntax
Here are the fundamental ways to use type hints.
A. Variable Annotations
You can annotate variables directly.

name: str = "Alice" age: int = 30 height: float = 5.9 is_student: bool = True
B. Function Parameter Annotations
This is the most common use case. You annotate the parameters of a function.
def greet(name: str) -> str:
return f"Hello, {name}!"
C. Return Value Annotations
Use the -> symbol followed by the type to annotate the return value.
def get_age() -> int:
return 30
Advanced Type Hinting
Python's type system is quite rich and can handle complex scenarios.
A. typing Module
For more complex types, you need to import the typing module.
List, Dict, Tuple, Set
from typing import List, Dict, Tuple, Set
# A list of integers
scores: List[int] = [95, 88, 76]
# A dictionary mapping strings to integers
student_grades: Dict[str, int] = {"Alice": 95, "Bob": 88}
# A tuple with specific types for each element
person: Tuple[str, int, float] = ("Charlie", 40, 6.1)
# A set of strings
unique_tags: Set[str] = {"python", "coding", "tutorial"}
Optional and Union
Optional[T] is a shortcut for Union[T, None]. It means the value can be of type T or None.
from typing import Optional, Union
# A function that might return a string or None
def find_user(user_id: int) -> Optional[str]:
# ... logic to find user ...
if user_found:
return "user123"
else:
return None
# Union means it can be one of several types
def process_data(data: Union[int, str]) -> str:
if isinstance(data, int):
return f"Processed number: {data}"
else:
return f"Processed string: {data}"
Any
Any signifies that a value can be of any type. It's useful when you don't know the type or want to bypass type checking for a specific part of your code.
from typing import Any
def flexible_function(value: Any) -> Any:
# This function can accept and return anything
return value * 2
B. Literal Type
Use Literal to restrict a value to one of a specific set of values.
from typing import Literal
def set_status(status: Literal["active", "inactive", "pending"]) -> None:
print(f"Status set to: {status}")
set_status("active") # OK
set_status("approved") # Error! Type checker will flag this.
C. Callable Type
Use Callable to represent a function or other callable object.
from typing import Callable
# A function that takes two numbers and returns a number
# The Callable is defined as [list of argument types, return type]
def apply_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
return operation(x, y)
result = apply_operation(10, 5, lambda a, b: a + b) # result will be 15
D. Custom Classes as Types
You can use your own classes as type hints.
class User:
def __init__(self, name: str):
self.name = name
def create_user(name: str) -> User:
return User(name)
current_user: User = create_user("David")
Why Use Type Hints? (Benefits)
-
Static Analysis and Error Catching: This is the biggest benefit. Tools like Mypy can analyze your code without running it to find potential bugs, like passing a string to a function that expects an integer.
# mypy your_script.py # Example error: error: Argument 1 to "add" has incompatible type "str"; expected "int"
-
Improved IDE Support: Modern IDEs like VS Code, PyCharm, and PyDev use type hints to provide:
- Autocompletion.
- In-line error highlighting.
- Better "Go to Definition" and "Find Usages" features.
-
Self-Documenting Code: Type hints act as built-in documentation. Anyone reading your function signature immediately understands what it expects and what it returns, making the code easier to understand and maintain.
-
Refactoring Confidence: When you rename a class or change a function's return type, a type checker will immediately tell you all the places in your code that need to be updated, preventing bugs.
How to Use Type Hints in Practice
A. Running a Type Checker (Mypy)
Mypy is the de-facto standard for static type checking in Python.
-
Install Mypy:
pip install mypy
-
Write a Python file (
example.py):from typing import List def get_average(scores: List[int]) -> float: if not scores: return 0.0 return sum(scores) / len(scores) # --- Correct usage --- my_scores = [85, 90, 78] average = get_average(my_scores) print(f"The average is: {average}") # --- Incorrect usage (will be caught by mypy) --- # This line will cause a type error at static analysis time wrong_scores = [85, "90", 78] average2 = get_average(wrong_scores) -
Run Mypy on the file:
mypy example.py
Output:
example.py:17: error: Argument 1 to "get_average" has incompatible type "List[Union[int, str]]"; expected "List[int]" Found 1 error in 1 file (checked 1 source file)Mypy caught the error where you tried to pass a list containing a string into a function that expects a list of integers.
B. Enforcing Types at Runtime
If you want Python to enforce types at runtime, you can use a library like Pydantic. This is extremely popular for data validation, especially in web applications (e.g., with FastAPI).
Example with Pydantic:
-
Install Pydantic:
pip install pydantic
-
Write a script (
pydantic_example.py):from pydantic import BaseModel, ValidationError class User(BaseModel): id: int name: str is_active: bool = True # Optional with a default value # --- Correct usage --- user_data = {"id": 1, "name": "Eve"} user = User(**user_data) print(user) #> id=1 name='Eve' is_active=True # --- Incorrect usage (will raise an error at runtime) --- try: invalid_data = {"id": "two", "name": "Frank"} # id should be an int invalid_user = User(**invalid_data) except ValidationError as e: print(f"\nValidation Error: {e}")Output:
id=1 name='Eve' is_active=True Validation Error: 1 validation error for User id Input should be a valid integer [type=int_type, input_value='two', input_type=str]
Summary
| Feature | Description | Example |
|---|---|---|
| Basic Annotation | Declare types for variables and functions. | def greet(name: str) -> str: |
typing Module |
For complex types like List, Dict, Optional. |
from typing import List, Optionalscores: List[int]def find() -> Optional[str]: |
| Static Checkers | Tools like Mypy find type errors without running code. | mypy my_script.py |
| Runtime Enforcement | Libraries like Pydantic validate data at runtime. | class User(BaseModel): id: int |
| Benefits | Improves code quality, readability, and maintainability. | Catches bugs early, enables better IDE support. |
