How to use type hints in Python

онлайн тренажер по питону
Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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 Microsoft
    • pyre — an analyzer from Facebook
    • MonkeyType — 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.

News