What are Type Hints in Python?
Type Hints (or type annotations) are a system for specifying expected data types in Python code, designed to enhance the readability, reliability, and overall quality of software. While Python remains a dynamically typed language, the ability to explicitly declare types helps prevent many errors during the development phase.
Why Use Type Annotations?
Python is renowned for its flexibility, but this very flexibility often leads to hidden bugs caused by unexpected data types. Consider the following example without annotations:
def add(a, b):
return a + b
print(add(5, 10)) # 15
print(add("5", "10")) # '510' — an unexpected result
The expression add("5", "10") doesn't throw an error, but the result might surprise the developer.
Type Hints address the following needs:
- Explicitly specify the types of arguments and return values.
- Improve code readability for development teams.
- Enable static analysis to find errors before runtime.
- Provide more accurate autocompletion in IDEs.
- Document APIs without needing extra comments.
Basic Examples of Using Type Hints
Function Annotation
def add(a: int, b: int) -> int:
return a + b
def calculate_area(radius: float) -> float:
return 3.14159 * radius ** 2
def format_greeting(name: str) -> str:
return f"Hello, {name}!"
Variable Annotation
name: str = "Alice"
age: int = 30
pi: float = 3.14159
is_active: bool = True
Working with Collections
from typing import List, Dict, Tuple, Set
def process_scores(scores: List[int]) -> float:
return sum(scores) / len(scores)
def get_user_data() -> Dict[str, str]:
return {"name": "John", "email": "john@example.com"}
def get_coordinates() -> Tuple[float, float]:
return (10.5, 20.3)
def unique_values(items: List[str]) -> Set[str]:
return set(items)
Advanced Type Hints Capabilities
Optional and Union
from typing import Optional, Union
def greet(name: Optional[str] = None) -> str:
if name:
return f"Hello, {name}!"
return "Hello, Guest!"
def process_value(value: Union[int, float, str]) -> str:
return str(value)
# In Python 3.10+, you can use the | operator
def process_new_syntax(value: int | float | str) -> str:
return str(value)
Callable Types
from typing import Callable
def apply_function(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
def multiply(x: int, y: int) -> int:
return x * y
result = apply_function(multiply, 5, 3) # 15
Generic Types
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items: List[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
if not self.items:
raise IndexError("Stack is empty")
return self.items.pop()
def is_empty(self) -> bool:
return len(self.items) == 0
# Usage
int_stack = Stack[int]()
int_stack.push(42)
Annotations in Classes
from typing import List, Optional
from dataclasses import dataclass
class Person:
def __init__(self, name: str, age: int, email: Optional[str] = None) -> None:
self.name = name
self.age = age
self.email = email
def get_info(self) -> Dict[str, Union[str, int]]:
return {
"name": self.name,
"age": self.age,
"email": self.email or "Not specified"
}
# Using dataclass for automatic method generation
@dataclass
class Employee:
name: str
position: str
salary: float
is_active: bool = True
Modern Features (Python 3.9+)
Built-in Collection Types
# Starting with Python 3.9
def process_data(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
def merge_lists(list1: list[int], list2: list[int]) -> list[int]:
return list1 + list2
# Instead of typing.List, typing.Dict
Literal Types
from typing import Literal
def set_mode(mode: Literal["development", "production", "testing"]) -> None:
print(f"Setting mode to: {mode}")
# Only these values will be accepted
set_mode("development") # OK
set_mode("debug") # Error in static analysis
Final Variables
from typing import Final
API_URL: Final[str] = "https://api.example.com"
MAX_RETRIES: Final[int] = 3
# These variables should not be changed
Static Analysis with mypy
Installation and Usage
pip install mypy
mypy your_script.py
Example Check
def divide(a: int, b: int) -> float:
return a / b
result = divide(10, "2") # mypy will report an error
mypy Configuration
Create a mypy.ini file:
[mypy]
python_version = 3.9
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
Special Types
NewType
from typing import NewType
UserId = NewType('UserId', int)
ProductId = NewType('ProductId', int)
def get_user(user_id: UserId) -> Dict[str, str]:
return {"name": "John", "id": str(user_id)}
def get_product(product_id: ProductId) -> Dict[str, str]:
return {"title": "Laptop", "id": str(product_id)}
# Usage
user_id = UserId(123)
product_id = ProductId(456)
Protocol for Structural Typing
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render_shape(shape: Drawable) -> None:
shape.draw()
Handling Errors with Type Hints
from typing import Union, Optional
class DatabaseError(Exception):
pass
def fetch_user(user_id: int) -> Optional[Dict[str, str]]:
try:
# Logic to fetch user
return {"name": "John", "email": "john@example.com"}
except DatabaseError:
return None
def safe_divide(a: float, b: float) -> Union[float, str]:
if b == 0:
return "Division by zero error"
return a / b
Tools for Working with Type Hints
- PyCharm:
- Built-in Type Hints support
- Type-based autocompletion
- Highlighting of typing errors
- VS Code with Pylance:
- Fast type checking
- Automatic imports
- Type-aware refactoring
- Other tools:
pyright— a fast type checker from Microsoftpyre— an analyzer from FacebookMonkeyType— automatic generation of annotations
Best Practices
Gradual Adoption
# Start with public APIs
def public_function(data: List[str]) -> Dict[str, int]:
return _process_data(data)
# Gradually add types to internal functions
def _process_data(data): # TODO: add types
return {item: len(item) for item in data}
Using Type Aliases
from typing import Dict, List, Union
# Create aliases for complex types
UserData = Dict[str, Union[str, int, bool]]
ProcessingResult = List[Dict[str, Union[str, float]]]
def process_users(users: List[UserData]) -> ProcessingResult:
return [{"name": user["name"], "score": 95.5} for user in users]
Compatibility with Different Python Versions
# For Python < 3.9
from typing import List, Dict
def old_style(items: List[str]) -> Dict[str, int]:
return {item: len(item) for item in items}
# For Python >= 3.9
def new_style(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
Common Mistakes and Solutions
Circular Imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .user import User
class Order:
def __init__(self, user: 'User') -> None: # Use string annotation
self.user = user
Mutable Default Values
from typing import List, Optional
# Incorrect
def bad_function(items: List[str] = []) -> List[str]:
return items
# Correct
def good_function(items: Optional[List[str]] = None) -> List[str]:
if items is None:
items = []
return items
Integration with Modern Frameworks
FastAPI
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Item(BaseModel):
name: str
price: float
description: Optional[str] = None
@app.post("/items/")
async def create_item(item: Item) -> dict[str, str]:
return {"message": f"Item {item.name} created"}
@app.get("/items/")
async def get_items() -> List[Item]:
return [Item(name="Laptop", price=999.99)]
Django with Types
from django.db import models
from typing import Optional
class User(models.Model):
username: str = models.CharField(max_length=150)
email: str = models.EmailField()
is_active: bool = models.BooleanField(default=True)
def get_full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
@classmethod
def get_by_email(cls, email: str) -> Optional['User']:
try:
return cls.objects.get(email=email)
except cls.DoesNotExist:
return None
Performance and Type Hints
Type Hints do not affect program execution performance because:
- Annotations are stored in the
__annotations__attribute. - The Python interpreter ignores them during execution.
- Type checking only occurs in static analyzers.
import time
from typing import List
def without_types(items):
return [x * 2 for x in items]
def with_types(items: List[int]) -> List[int]:
return [x * 2 for x in items]
# Performance is the same
Conclusion
Type Hints in Python are a powerful tool for creating more reliable and maintainable code. They help to:
- Identify errors during development
- Improve code readability and documentation
- Provide better support in IDEs
- Simplify refactoring and team development
Start with simple annotations in new projects and gradually introduce them into existing code. Using static analyzers like mypy will significantly improve the quality of your Python code and reduce the number of errors in production.
The Future of AI in Mathematics and Everyday Life: How Intelligent Agents Are Already Changing the Game
Experts warned about the risks of fake charity with AI
In Russia, universal AI-agent for robots and industrial processes was developed