From Beginner to Expert
- https://mypy.readthedocs.io/en/stable/index.html
- https://realpython.com/python-type-checking/
- https://dagster.io/blog/python-type-hinting
Table of Contents
- Quick Start Guide
- Introduction to Python Typing
- Basic Type Hints
- Collection Types
- Advanced Types
- Generic Types
- Protocols and Structural Typing
- Type Checking Tools
- Best Practices
- Common Pitfalls and Troubleshooting
- Real-World Examples
- Learning Roadmap
1. Quick Start Guide
๐ Your First 5 Minutes with Python Typing
Prerequisites: Python 3.6+ (Recommended: Python 3.9+)
Step 1: Setup Your Environment
# Install type checker
pip install mypy
# For VS Code users (recommended)
# Install Python extension and PylanceBashStep 2: Your First Type Hints
Create a file hello_typing.py:
def greet(name: str, age: int) -> str:
return f"Hello {name}, you are {age} years old!"
def calculate_discount(price: float, discount_percent: float) -> float:
return price * (1 - discount_percent / 100)
# Try it out
message = greet("Alice", 25)
discounted_price = calculate_discount(100.0, 10.0)
print(message)
print(f"Discounted price: ${discounted_price}")PythonStep 3: Run Type Checking
# Check for type errors
mypy hello_typing.py
# Should output: Success: no issues found in 1 source fileBashStep 4: See Type Checking in Action
Add this line to your file and run mypy again:
# This will cause a type error
wrong_greeting = greet(123, "Alice") # Arguments swapped!Pythonmypy hello_typing.py
# Output: error: Argument 1 to "greet" has incompatible type "int"; expected "str"Bash๐ฏ Quick Reference Card
| Type | Example | Usage |
|---|---|---|
str | name: str = "Alice" | Text strings |
int | age: int = 25 | Whole numbers |
float | price: float = 19.99 | Decimal numbers |
bool | is_active: bool = True | True/False values |
list[int] | numbers: list[int] = [1, 2, 3] | List of integers |
dict[str, int] | scores: dict[str, int] = {"Alice": 95} | String to int mapping |
Optional[str] | middle_name: Optional[str] = None | Can be None |
๐ก Pro Tips for Beginners
- Start Small: Add types to function signatures first
- Use Your IDE: Modern IDEs show type errors as you type
- Don’t Panic: Type errors won’t crash your program at runtime
- Gradual Adoption: You can add types to existing code incrementally
2. Introduction to Python Typing
What is Type Hinting?
Type hinting is a feature introduced in Python 3.5 that allows you to specify the expected types of variables, function parameters, and return values. It makes code more readable, helps catch bugs early, and enables better IDE support.
๐ Key Insight: Type hints are annotations, not enforcement. Python remains dynamically typed at runtime, but type hints provide static analysis benefits.
graph TD
A[๐ Python Code] --> B{๐ท๏ธ Type Hints?}
B -->|โ
Yes| C[๐ Better IDE Support]
B -->|โ
Yes| D[๐ก๏ธ Static Type Checking]
B -->|โ
Yes| E[๐ Documentation]
B -->|โ No| F[๐ Runtime Only]
C --> G[๐ก Autocomplete]
C --> H[๐จ Error Detection]
D --> I[๐ง mypy/pyright]
E --> J[๐ Self-Documenting Code]
style A fill:#e1f5fe
style C fill:#e8f5e8
style D fill:#e8f5e8
style E fill:#e8f5e8
style F fill:#ffebeeDuck Typing vs Static Typing
graph LR
subgraph "๐ฆ Duck Typing (Traditional Python)"
A1["if it walks like a duckand quacks like a duckthen it's a duck"] --> B1[Runtime Discovery]
B1 --> C1[Flexible but Error-Prone]
end
subgraph "๐ Static Typing (Modern Python)"
A2[Explicit Type Declarations] --> B2[Compile-time Checking]
B2 --> C2[Structured but Safe]
end
style A1 fill:#fff3e0
style A2 fill:#e3f2fdWhy Use Type Hints?
- ๐ Code Documentation: Types serve as inline documentation that never goes out of sync
- ๐ง IDE Support: Better autocomplete, refactoring, and error detection
- ๐ Bug Prevention: Catch type-related errors before runtime
- ๐ Refactoring Safety: Easier to refactor with confidence
- ๐ฅ Team Collaboration: Clearer contracts between team members
- ๐ Performance: Some tools can optimize typed code better
The Cost-Benefit Analysis
graph TD
subgraph "๐ฐ Costs"
A[Learning Curve]
B[Initial Setup Time]
C[More Verbose Code]
end
subgraph "๐ Benefits"
D[Fewer Runtime Errors]
E[Better IDE Experience]
F[Easier Maintenance]
G[Team Productivity]
H[Documentation]
end
A --> I{Worth It?}
B --> I
C --> I
D --> I
E --> I
F --> I
G --> I
H --> I
I -->|For most projects| J[โ
YES]
style J fill:#e8f5e8Evolution of Python Typing
timeline
title ๐ Python Typing Evolution
2014 : PEP 484 : ๐ก Type Hints Proposal
2015 : Python 3.5 : ๐ฏ typing module introduced
2016 : Python 3.6 : ๐ Variable annotations (PEP 526)
2017 : Python 3.7 : ๐ฎ Postponed evaluation of annotations
2019 : Python 3.8 : ๐ท๏ธ Literal types, TypedDict, Final
2020 : Python 3.9 : ๐๏ธ Built-in generic types (list[int])
2021 : Python 3.10 : โก Union operator (int | str)
2022 : Python 3.11 : ๐ Self type, exception groups
2023 : Python 3.12 : ๐ญ Generic type syntaxMigration Timeline Recommendation
graph LR
A[Legacy Code] --> B[Add Return Types]
B --> C[Add Parameter Types]
C --> D[Add Variable Types]
D --> E[Enable Strict Mode]
E --> F[Full Type Coverage]
style A fill:#ffebee
style F fill:#e8f5e83. Basic Type Hints
Primitive Types
# โ
Basic type annotations
def greet(name: str) -> str:
return f"Hello, {name}!"
def add_numbers(a: int, b: int) -> int:
return a + b
def calculate_average(numbers: list[float]) -> float: # More specific than just 'list'
if not numbers: # Handle edge case
raise ValueError("Cannot calculate average of empty list")
return sum(numbers) / len(numbers)
# โ
Variable annotations (Python 3.6+)
age: int = 25
is_student: bool = True
height: float = 5.9
name: str # Declaration without assignment
name = "Alice" # Assignment laterPython๐จ Common Beginner Mistakes
# โ Don't do this
def bad_function(numbers: list) -> float: # Too generic
return sum(numbers) / len(numbers)
def another_bad_function(data): # Missing type hints
return data * 2
# โ
Do this instead
def good_function(numbers: list[float]) -> float: # Specific type
if not numbers:
return 0.0
return sum(numbers) / len(numbers)
def another_good_function(data: int) -> int: # Clear type hints
return data * 2Python๐ก Type Annotation Best Practices
graph TD
A[๐ Writing Type Hints] --> B{Choose Specificity Level}
B -->|Too Generic| C["โ list, dict, Any"]
B -->|Just Right| D["โ
list[str], dict[str, int]"]
B -->|Too Specific| E["โ Literal values only"]
D --> F["๐ฏ Balanced Approach"]
style C fill:#ffebee
style D fill:#e8f5e8
style E fill:#fff3e0
style F fill:#e8f5e8Type Hierarchy and Compatibility
graph TD
A[๐ object] --> B[๐ข int]
A --> C[๐ float]
A --> D[๐ str]
A --> E[โ
bool]
A --> F[๐งฎ complex]
A --> G[๐ฆ bytes]
A --> H[๐ bytearray]
B --> E
B -.->|can be used as| C
A --> I[๐ Sequence]
I --> J[๐ list]
I --> K[๐ tuple]
I --> L[๐ str]
A --> M[๐บ๏ธ Mapping]
M --> N[๐ dict]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#fff3e0
style E fill:#e8f5e8๐ Type Compatibility Rules
# โ
These work due to type hierarchy
def process_number(value: float) -> float:
return value * 2
result1 = process_number(5) # int -> float โ
result2 = process_number(5.5) # float -> float โ
result3 = process_number(True) # bool -> int -> float โ
# โ These don't work
# result4 = process_number("5") # str -> float โPythonNone and Optional Types
from typing import Optional, Union
# โ
Optional type (can be None)
def find_user(user_id: int) -> Optional[str]:
"""
Find a user by ID. Returns username or None if not found.
Optional[str] is equivalent to Union[str, None]
"""
users = {1: "Alice", 2: "Bob", 3: "Charlie"}
return users.get(user_id) # dict.get() returns None if key not found
# โ
Union type (multiple possible types)
def process_id(user_id: Union[int, str]) -> str:
"""Process user ID that can be either int or string."""
return str(user_id)
# ๐ Python 3.10+ syntax (preferred)
def modern_process_id(user_id: int | str) -> str:
"""Modern union syntax using | operator."""
return str(user_id)
def modern_find_user(user_id: int) -> str | None:
"""Modern optional syntax."""
users = {1: "Alice", 2: "Bob", 3: "Charlie"}
return users.get(user_id)
# ๐ Practical example with error handling
def safe_divide(a: float, b: float) -> Optional[float]:
"""Safely divide two numbers, return None if division by zero."""
if b == 0:
return None
return a / b
# Usage
result = safe_divide(10, 2)
if result is not None:
print(f"Result: {result}")
else:
print("Division by zero!")Python๐ก Optional vs Union Guidelines
graph TD
A[Need to represent absence?] --> B{What kind?}
B -->|Missing value| C["Use Optional[T]"]
B -->|Multiple types| D["Use Union[T, U]"]
B -->|Error state| E[Consider exceptions]
C --> F["โ
Optional[str]"]
D --> G["โ
Union[int, str]"]
E --> H["๐ค Raise ValueError"]
style F fill:#e8f5e8
style G fill:#e8f5e8
style H fill:#fff3e0Type Aliases for Better Readability
from typing import Union, List, Dict, NewType
# โ
Simple type aliases for domain concepts
UserId = int
UserName = str
Email = str
UserData = Dict[str, Union[str, int, bool]]
UserList = List[UserData]
# ๐ Advanced: NewType for stronger typing
UserId = NewType('UserId', int) # Creates a distinct type
ProductId = NewType('ProductId', int)
def get_user(user_id: UserId) -> UserData:
"""Get user by ID. UserId provides type safety."""
# Implementation here
pass
def get_product(product_id: ProductId) -> dict:
"""Get product by ID. ProductId prevents mixing with UserId."""
# Implementation here
pass
# โ
This works
user_id = UserId(123)
user = get_user(user_id)
# โ This would cause a type error
product_id = ProductId(456)
# user = get_user(product_id) # Type error!
# ๐ Complex type aliases for data structures
Coordinates = tuple[float, float]
BoundingBox = tuple[Coordinates, Coordinates]
Color = tuple[int, int, int] # RGB values
# ๐บ๏ธ Business domain aliases
PriceData = Dict[str, Union[float, str]] # {"amount": 19.99, "currency": "USD"}
OrderItem = Dict[str, Union[str, int, float]]
ShoppingCart = List[OrderItem]
def calculate_cart_total(cart: ShoppingCart) -> float:
"""Calculate total price of items in cart."""
return sum(item.get("price", 0.0) for item in cart)
def create_user(user_id: UserId, name: UserName, email: Email) -> UserData:
"""Create a new user with type-safe parameters."""
return {
"id": user_id,
"name": name,
"email": email,
"active": True,
"created_at": "2025-09-18"
}
def get_all_users() -> UserList:
"""Get all users from the system."""
return [
{"id": UserId(1), "name": "Alice", "email": "alice@example.com", "active": True},
{"id": UserId(2), "name": "Bob", "email": "bob@example.com", "active": False}
]Python๐ฏ When to Use Type Aliases
graph TD
A[Complex Type?] --> B{How complex?}
B -->|Simple| C[Consider alias]
B -->|Complex| D[Definitely use alias]
B -->|Used often?| E[Use alias]
C --> F{Domain concept?}
F -->|Yes| G[โ
Use NewType]
F -->|No| H[โ
Use simple alias]
D --> I[โ
Use alias + documentation]
E --> J[โ
Use alias for DRY]
style G fill:#e8f5e8
style H fill:#e8f5e8
style I fill:#e8f5e8
style J fill:#e8f5e8๐ฏ Practice Exercise: Basic Types
Challenge: Create a simple calculator with proper type hints.
# TODO: Add type hints to this calculator
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
return None
return a / b
def calculate_tip(bill_amount, tip_percentage):
return bill_amount * (tip_percentage / 100)
def format_currency(amount, currency="USD"):
return f"{amount:.2f} {currency}"
# TODO: Use the functions with type checking
result1 = add(10, 5)
result2 = divide(10, 3)
result3 = calculate_tip(50.0, 15)
formatted = format_currency(result3)PythonSolution:
from typing import Optional
def add(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
def divide(a: float, b: float) -> Optional[float]:
"""Divide two numbers, return None if division by zero."""
if b == 0:
return None
return a / b
def calculate_tip(bill_amount: float, tip_percentage: float) -> float:
"""Calculate tip amount based on bill and percentage."""
return bill_amount * (tip_percentage / 100)
def format_currency(amount: float, currency: str = "USD") -> str:
"""Format amount as currency string."""
return f"{amount:.2f} {currency}"
# Usage with type checking
result1: float = add(10.0, 5.0)
result2: Optional[float] = divide(10.0, 3.0)
result3: float = calculate_tip(50.0, 15.0)
formatted: str = format_currency(result3)
print(f"Addition: {result1}")
print(f"Division: {result2}")
print(f"Tip: {formatted}")Python4. Collection Types
Lists, Tuples, and Sets
from typing import List, Tuple, Set, Dict # Legacy imports (Python < 3.9)
# โ
Homogeneous collections - all elements same type
numbers: List[int] = [1, 2, 3, 4, 5]
names: Set[str] = {"Alice", "Bob", "Charlie"}
grades: Dict[str, float] = {"Alice": 95.5, "Bob": 87.2}
# โ
Heterogeneous tuples - fixed structure
person: Tuple[str, int, bool] = ("Alice", 30, True) # name, age, is_active
coordinate_2d: Tuple[float, float] = (10.5, 20.3) # x, y
rgb_color: Tuple[int, int, int] = (255, 128, 0) # red, green, blue
# โ
Variable-length tuples - same type, variable count
coordinates: Tuple[float, ...] = (1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
test_scores: Tuple[int, ...] = (85, 92, 78, 96)
# โ Common mistakes
mixed_list: List = [1, "two", 3.0] # Too generic!
any_dict: Dict = {"key": "value"} # Too generic!
# โ
Better alternatives
mixed_data: List[Union[int, str, float]] = [1, "two", 3.0] # Explicit union
config_dict: Dict[str, str] = {"host": "localhost", "port": "8080"}Python๐ Modern Collection Syntax (Python 3.9+)
# ๐ Python 3.9+ - using built-in types directly (preferred!)
def process_numbers(numbers: list[int]) -> list[int]:
"""Double all numbers in the list."""
return [n * 2 for n in numbers]
def group_by_category(items: list[str]) -> dict[str, list[str]]:
"""Group items by their first letter."""
result: dict[str, list[str]] = {}
for item in items:
category = item[0].upper()
if category not in result:
result[category] = []
result[category].append(item)
return result
# ๐ฏ Real-world examples
def calculate_student_averages(grades: dict[str, list[float]]) -> dict[str, float]:
"""Calculate average grade for each student."""
return {
student: sum(student_grades) / len(student_grades)
for student, student_grades in grades.items()
if student_grades # Avoid division by zero
}
# ๐ Complex nested structures
UserPreferences = dict[str, dict[str, bool]] # user_id -> preference -> enabled
ProductCatalog = dict[str, list[tuple[str, float]]] # category -> [(name, price), ...]
def get_user_notifications(
preferences: UserPreferences,
user_id: str
) -> list[str]:
"""Get enabled notification types for a user."""
user_prefs = preferences.get(user_id, {})
return [notif_type for notif_type, enabled in user_prefs.items() if enabled]PythonCollection Hierarchy and When to Use Each
graph TD
A[๐ Collection] --> B[๐ Sequence]
A --> C[๐ฏ Set]
A --> D[๐บ๏ธ Mapping]
A --> E[๐ Iterator]
B --> F[๐ list - Ordered, Mutable]
B --> G[๐ tuple - Ordered, Immutable]
B --> H[๐ str - Ordered, Immutable]
B --> I[๐ฆ bytes - Ordered, Immutable]
C --> J[๐ง set - Unordered, Mutable]
C --> K[โ๏ธ frozenset - Unordered, Immutable]
D --> L["๐ dict - Key-Value, Ordered (3.7+)"]
E --> M[๐ Generator]
E --> N[๐ Iterator]
style F fill:#e8f5e8
style G fill:#fff3e0
style H fill:#e3f2fd
style J fill:#ffebee
style L fill:#f3e5f5๐ฏ Collection Selection Guide
| Use Case | Collection Type | Example |
|---|---|---|
| ๐ Ordered items, need indexing | list[T] | Shopping cart items |
| ๐ Fixed structure, immutable | tuple[T, ...] | Coordinates, RGB values |
| ๐ฏ Unique items, fast lookup | set[T] | User permissions, tags |
| ๐บ๏ธ Key-value relationships | dict[K, V] | User profiles, config |
| ๐ Large datasets, memory efficient | Iterator[T] | File processing |
๐ Advanced Collection Examples
from typing import DefaultDict, Counter, Deque, ChainMap
from collections import defaultdict, Counter, deque, ChainMap
# ๐ Specialized collections with proper typing
word_count: Counter[str] = Counter(["apple", "banana", "apple", "cherry", "apple"])
grouped_items: DefaultDict[str, list[int]] = defaultdict(list)
processing_queue: Deque[str] = deque(["task1", "task2", "task3"])
# ๐ Practical text analysis function
def analyze_text(text: str) -> dict[str, Union[int, Counter[str], set[str]]]:
"""Comprehensive text analysis with proper typing."""
words = text.lower().split()
return {
"word_count": len(words),
"unique_words": len(set(words)),
"character_count": len(text),
"frequency": Counter(words),
"unique_word_set": set(words)
}
# ๐๏ธ Advanced data structures
class DataProcessor:
def __init__(self) -> None:
self.data_cache: dict[str, list[dict[str, Any]]] = {}
self.counters: defaultdict[str, Counter[str]] = defaultdict(Counter)
self.processing_queue: deque[tuple[str, dict[str, Any]]] = deque()
def add_data(self, category: str, item: dict[str, Any]) -> None:
"""Add data item to specific category."""
if category not in self.data_cache:
self.data_cache[category] = []
self.data_cache[category].append(item)
# Update counters for this category
if "type" in item:
self.counters[category][item["type"]] += 1
# Add to processing queue
self.processing_queue.append((category, item))
def get_statistics(self) -> dict[str, dict[str, int]]:
"""Get statistics for all categories."""
return {
category: dict(counter)
for category, counter in self.counters.items()
}
# ๐ Configuration management with ChainMap
default_config: dict[str, str] = {
"host": "localhost",
"port": "8080",
"debug": "false"
}
user_config: dict[str, str] = {
"host": "production.example.com",
"debug": "true"
}
# ChainMap for configuration precedence
config_chain: ChainMap[str, str] = ChainMap(user_config, default_config)
def get_config_value(key: str) -> str:
"""Get configuration value with fallback to defaults."""
return config_chain.get(key, "")
# ๐ Performance-optimized data processing
def process_large_dataset(
data: Iterator[dict[str, Any]]
) -> Generator[dict[str, Any], None, None]:
"""Process large dataset without loading everything into memory."""
for item in data:
if item.get("is_valid", False):
# Transform the item
processed_item = {
"id": item.get("id"),
"processed_at": datetime.now().isoformat(),
"value": item.get("value", 0) * 2
}
yield processed_itemPythonโก Performance Tips for Collections
graph TD
A[Collection Performance] --> B[Memory Usage]
A --> C[Access Speed]
A --> D[Iteration Speed]
B --> E[Use generators for large data]
B --> F[Choose appropriate collection type]
C --> G["dict/set: O(1) lookup"]
C --> H["list: O(1) index, O(n) search"]
D --> I[Avoid repeated conversions]
D --> J[Use comprehensions]
style E fill:#e8f5e8
style F fill:#e8f5e8
style G fill:#e8f5e8
style H fill:#fff3e0
style I fill:#e8f5e8
style J fill:#e8f5e8๐ฏ Collection Exercise: Data Analysis
Challenge: Build a data analysis system with proper typing.
# TODO: Add proper type hints and implement the functions
def load_sales_data(filename):
# Load CSV data and return list of sales records
pass
def group_sales_by_month(sales_data):
# Group sales by month and return dictionary
pass
def calculate_monthly_totals(grouped_sales):
# Calculate total sales for each month
pass
def find_top_products(sales_data, limit=5):
# Find top-selling products
passPythonSolution:
from typing import Any
from collections import defaultdict, Counter
from datetime import datetime
import csv
SalesRecord = dict[str, Union[str, float, int]]
MonthlySales = dict[str, list[SalesRecord]]
MonthlyTotals = dict[str, float]
def load_sales_data(filename: str) -> list[SalesRecord]:
"""Load sales data from CSV file."""
sales_data: list[SalesRecord] = []
with open(filename, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
sales_data.append({
"date": row["date"],
"product": row["product"],
"amount": float(row["amount"]),
"quantity": int(row["quantity"])
})
return sales_data
def group_sales_by_month(sales_data: list[SalesRecord]) -> MonthlySales:
"""Group sales records by month."""
grouped: defaultdict[str, list[SalesRecord]] = defaultdict(list)
for sale in sales_data:
# Extract month from date (assuming YYYY-MM-DD format)
date_str = str(sale["date"])
month = date_str[:7] # YYYY-MM
grouped[month].append(sale)
return dict(grouped)
def calculate_monthly_totals(grouped_sales: MonthlySales) -> MonthlyTotals:
"""Calculate total sales amount for each month."""
return {
month: sum(float(sale["amount"]) for sale in sales)
for month, sales in grouped_sales.items()
}
def find_top_products(
sales_data: list[SalesRecord],
limit: int = 5
) -> list[tuple[str, int]]:
"""Find top-selling products by quantity."""
product_quantities: Counter[str] = Counter()
for sale in sales_data:
product = str(sale["product"])
quantity = int(sale["quantity"])
product_quantities[product] += quantity
return product_quantities.most_common(limit)Python5. Advanced Types
Callable Types
from typing import Callable, Any
# Function that takes a function as parameter
def apply_operation(numbers: List[int], operation: Callable[[int], int]) -> List[int]:
return [operation(n) for n in numbers]
# Function that returns a function
def create_multiplier(factor: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * factor
return multiplier
# Complex callable with multiple parameters
ProcessFunction = Callable[[str, int], bool]
def process_data(data: str, threshold: int, processor: ProcessFunction) -> bool:
return processor(data, threshold)PythonLiteral Types
from typing import Literal
# Restrict values to specific literals
def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None:
print(f"Setting log level to {level}")
# Literal with numbers
def set_port(port: Literal[80, 443, 8080]) -> None:
print(f"Using port {port}")
# Combined with Union
Status = Literal["pending", "approved", "rejected"]
Priority = Literal[1, 2, 3, 4, 5]
def update_task(status: Status, priority: Priority) -> None:
print(f"Task status: {status}, Priority: {priority}")Python๐๏ธ TypedDict – Structured Dictionaries
from typing import TypedDict, NotRequired, Required, Literal
from datetime import datetime
# โ
Basic TypedDict for API responses
class PersonDict(TypedDict):
name: str
age: int
email: str
# ๐ TypedDict with optional fields (Python 3.11+)
class UserProfile(TypedDict):
id: Required[int]
username: Required[str]
email: NotRequired[str]
is_active: NotRequired[bool]
last_login: NotRequired[datetime]
# ๐ฏ Real-world API examples
class APIResponse(TypedDict):
status: Literal["success", "error"]
message: str
data: NotRequired[dict[str, Any]]
errors: NotRequired[list[str]]
class DatabaseConfig(TypedDict):
host: str
port: int
database: str
username: str
password: str
ssl_enabled: NotRequired[bool]
connection_timeout: NotRequired[int]
# ๐ Inheritance with TypedDict
class BaseEntity(TypedDict):
id: int
created_at: datetime
updated_at: datetime
class User(BaseEntity):
username: str
email: str
is_active: bool
class Product(BaseEntity):
name: str
price: float
category: str
in_stock: bool
def create_person(data: PersonDict) -> str:
"""Process person data with type safety."""
return f"{data['name']} is {data['age']} years old"
def update_user(user_id: int, updates: UserProfile) -> UserProfile:
"""Update user profile with partial data."""
# In real code, this would update the database
base_profile: UserProfile = {
"id": user_id,
"username": "updated_user"
}
# Merge updates (NotRequired fields can be omitted)
return {**base_profile, **updates}
# ๐ Advanced: Nested TypedDict structures
class Address(TypedDict):
street: str
city: str
country: str
postal_code: NotRequired[str]
class CompanyInfo(TypedDict):
name: str
address: Address
employees: list[User]
founded_year: int
is_public: NotRequired[bool]
def process_company_data(company: CompanyInfo) -> dict[str, Any]:
"""Process complex nested TypedDict data."""
return {
"company_name": company["name"],
"location": f"{company['address']['city']}, {company['address']['country']}",
"employee_count": len(company["employees"]),
"years_in_business": 2025 - company["founded_year"]
}Python๐ Integration with Pydantic
from pydantic import BaseModel, ConfigDict
from typing import Optional
from datetime import datetime
# โ
Pydantic models with TypedDict compatibility
class UserModel(BaseModel):
model_config = ConfigDict(extra='forbid')
id: int
username: str
email: str
is_active: bool = True
created_at: datetime = datetime.now()
# ๐ Convert between TypedDict and Pydantic
def typeddict_to_pydantic(user_data: UserProfile) -> UserModel:
"""Convert TypedDict to Pydantic model with validation."""
return UserModel(**user_data)
def pydantic_to_typeddict(user_model: UserModel) -> UserProfile:
"""Convert Pydantic model to TypedDict."""
return user_model.model_dump()
# ๐ฏ API endpoint with both TypedDict and Pydantic
def create_user_endpoint(user_data: UserProfile) -> APIResponse:
"""API endpoint that accepts TypedDict and returns structured response."""
try:
# Validate with Pydantic
user_model = typeddict_to_pydantic(user_data)
# Process the user (save to database, etc.)
# ... database operations ...
return APIResponse(
status="success",
message="User created successfully",
data={"user_id": user_model.id}
)
except Exception as e:
return APIResponse(
status="error",
message="Failed to create user",
errors=[str(e)]
)Python๐ญ Modern Dataclasses with Advanced Features
from dataclasses import dataclass, field, InitVar
from typing import Optional, ClassVar, Self # Self requires Python 3.11+
from datetime import datetime
from enum import Enum
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
@dataclass
class Task:
title: str
description: str
priority: Priority = Priority.MEDIUM
completed: bool = False
tags: list[str] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
updated_at: Optional[datetime] = None
# Class variable (shared by all instances)
total_tasks: ClassVar[int] = 0
def __post_init__(self) -> None:
"""Called after initialization."""
Task.total_tasks += 1
def mark_completed(self) -> Self: # Python 3.11+ Self type
"""Mark task as completed and return self for chaining."""
self.completed = True
self.updated_at = datetime.now()
return self
def add_tag(self, tag: str) -> Self:
"""Add a tag and return self for chaining."""
if tag not in self.tags:
self.tags.append(tag)
self.updated_at = datetime.now()
return self
# ๐ Dataclass with computed fields
@dataclass
class Product:
name: str
base_price: float
tax_rate: float = 0.1
discount_percent: float = 0.0
@property
def discounted_price(self) -> float:
"""Calculate price after discount."""
return self.base_price * (1 - self.discount_percent / 100)
@property
def final_price(self) -> float:
"""Calculate final price with tax."""
return self.discounted_price * (1 + self.tax_rate)
# ๐ฏ Advanced: InitVar for initialization-only parameters
@dataclass
class User:
username: str
email: str
password_hash: str = field(repr=False) # Don't show in repr
is_active: bool = True
created_at: datetime = field(default_factory=datetime.now)
# InitVar: only used during initialization, not stored
raw_password: InitVar[Optional[str]] = None
def __post_init__(self, raw_password: Optional[str]) -> None:
"""Hash password if provided."""
if raw_password:
# In real code, use proper password hashing
self.password_hash = f"hashed_{raw_password}"
# ๐๏ธ Dataclass inheritance
@dataclass
class Employee(User):
employee_id: str
department: str
salary: float
manager: Optional['Employee'] = None # Forward reference
def get_team_size(self) -> int:
"""Count direct reports (simplified example)."""
# In real code, this would query a database
return 0
# ๐ง Frozen dataclasses (immutable)
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_from_origin(self) -> float:
"""Calculate distance from origin."""
return (self.x ** 2 + self.y ** 2) ** 0.5
def translate(self, dx: float, dy: float) -> Self:
"""Return new point translated by dx, dy."""
return Point(self.x + dx, self.y + dy)
# ๐ฏ Usage examples
def demo_dataclasses() -> None:
# Create task with method chaining
task = (Task("Learn Python Typing", "Study advanced typing concepts")
.add_tag("learning")
.add_tag("python")
.mark_completed())
# Create user with password
user = User("alice", "alice@example.com", raw_password="secret123")
# Create product and calculate prices
product = Product("Laptop", 1000.0, tax_rate=0.08, discount_percent=10)
print(f"Final price: ${product.final_price:.2f}")
# Immutable point operations
origin = Point(0, 0)
moved_point = origin.translate(3, 4)
distance = moved_point.distance_from_origin()
print(f"Distance: {distance}")PythonEnum Types
from enum import Enum, auto
from typing import Union
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
class Status(Enum):
PENDING = auto()
PROCESSING = auto()
COMPLETED = auto()
FAILED = auto()
def paint_object(color: Color) -> str:
return f"Painting with {color.value}"
def check_status(current_status: Status) -> bool:
return current_status in [Status.COMPLETED, Status.FAILED]Python6. Generic Types
Introduction to Generics
graph TD
A[Generic Types] --> B[Type Variables]
A --> C[Generic Classes]
A --> D[Generic Functions]
B --> E["T = TypeVar"]
C --> F["List[T]"]
C --> G["Dict[K, V]"]
D --> H[Parameterized Return Types]Basic Generics
from typing import TypeVar, Generic, List
# Type variable
T = TypeVar('T')
def first_element(items: List[T]) -> T:
return items[0]
def last_element(items: List[T]) -> T:
return items[-1]
# Usage preserves type information
numbers = [1, 2, 3, 4, 5]
first_num: int = first_element(numbers) # Type checker knows this is int
strings = ["hello", "world"]
first_str: str = first_element(strings) # Type checker knows this is strPythonGeneric Classes
from typing import TypeVar, Generic, Optional
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) -> Optional[T]:
if self._items:
return self._items.pop()
return None
def peek(self) -> Optional[T]:
if self._items:
return self._items[-1]
return None
def is_empty(self) -> bool:
return len(self._items) == 0
# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")PythonBounded Type Variables
from typing import TypeVar
from abc import ABC, abstractmethod
class Comparable(ABC):
@abstractmethod
def __lt__(self, other: 'Comparable') -> bool:
pass
# Bounded type variable
T = TypeVar('T', bound=Comparable)
def find_min(items: List[T]) -> T:
if not items:
raise ValueError("List cannot be empty")
min_item = items[0]
for item in items[1:]:
if item < min_item:
min_item = item
return min_item
# Constrained type variables
NumberType = TypeVar('NumberType', int, float)
def add_numbers(a: NumberType, b: NumberType) -> NumberType:
return a + bPythonComplex Generic Examples
from typing import TypeVar, Generic, Dict, List, Callable, Optional
K = TypeVar('K') # Key type
V = TypeVar('V') # Value type
R = TypeVar('R') # Result type
class Repository(Generic[K, V]):
def __init__(self) -> None:
self._data: Dict[K, V] = {}
def save(self, key: K, value: V) -> None:
self._data[key] = value
def find(self, key: K) -> Optional[V]:
return self._data.get(key)
def find_all(self) -> List[V]:
return list(self._data.values())
def transform(self, transformer: Callable[[V], R]) -> List[R]:
return [transformer(value) for value in self._data.values()]
# Usage
user_repo: Repository[int, str] = Repository()
user_repo.save(1, "Alice")
user_repo.save(2, "Bob")
product_repo: Repository[str, Dict[str, str]] = Repository()
product_repo.save("laptop", {"name": "Gaming Laptop", "price": "1500"})Python7. Protocols and Structural Typing
Understanding Protocols vs Inheritance
graph TD
A[๐ฏ Design Choice] --> B{Relationship Type?}
B -->|"IS-A"| C[๐๏ธ Inheritance]
B -->|"CAN-DO"| D[๐ Protocol]
C --> E[class Dog extends Animal]
D --> F[class Dog implements Walkable]
E --> G[โ
Shared behavior & data]
F --> H[โ
Shared interface only]
style C fill:#e3f2fd
style D fill:#e8f5e8๐ When to Use Protocols vs Inheritance
| Scenario | Use Inheritance | Use Protocol |
|---|---|---|
| Shared implementation | โ | โ |
| Multiple inheritance needed | โ | โ |
| Third-party class integration | โ | โ |
| Duck typing formalization | โ | โ |
| Interface segregation | โ | โ |
๐ฏ Real-World Protocol Examples
from typing import Protocol, runtime_checkable, TypeVar, Generic
from abc import abstractmethod
import asyncio
from pathlib import Path
# ๐ง File operations protocol
class Readable(Protocol):
def read(self, size: int = -1) -> str: ...
def readline(self) -> str: ...
def readlines(self) -> list[str]: ...
class Writable(Protocol):
def write(self, data: str) -> int: ...
def writelines(self, lines: list[str]) -> None: ...
def flush(self) -> None: ...
# โ
Works with files, StringIO, network streams, etc.
def copy_content(source: Readable, destination: Writable) -> None:
"""Copy content from any readable source to any writable destination."""
for line in source.readlines():
destination.write(line)
destination.flush()
# ๐ HTTP client protocol (works with requests, httpx, aiohttp, etc.)
@runtime_checkable
class HTTPClient(Protocol):
def get(self, url: str, **kwargs) -> 'Response': ...
def post(self, url: str, data=None, json=None, **kwargs) -> 'Response': ...
class Response(Protocol):
status_code: int
text: str
def json(self) -> dict: ...
def fetch_user_data(client: HTTPClient, user_id: int) -> dict:
"""Fetch user data using any HTTP client implementation."""
response = client.get(f"https://api.example.com/users/{user_id}")
if response.status_code == 200:
return response.json()
raise RuntimeError(f"Failed to fetch user: {response.status_code}")
# ๐พ Database protocol (works with any database adapter)
T = TypeVar('T')
class DatabaseConnection(Protocol[T]):
def execute(self, query: str, params: tuple = ()) -> T: ...
def fetchone(self) -> dict | None: ...
def fetchall(self) -> list[dict]: ...
def commit(self) -> None: ...
def rollback(self) -> None: ...
def get_user_by_id(conn: DatabaseConnection, user_id: int) -> dict | None:
"""Get user by ID using any database connection."""
conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return conn.fetchone()
# ๐ Data processing pipeline protocol
class DataProcessor(Protocol[T]):
def process(self, data: T) -> T: ...
def validate(self, data: T) -> bool: ...
class DataPipeline(Generic[T]):
def __init__(self) -> None:
self.processors: list[DataProcessor[T]] = []
def add_processor(self, processor: DataProcessor[T]) -> None:
self.processors.append(processor)
def process_data(self, data: T) -> T:
"""Process data through all processors."""
current_data = data
for processor in self.processors:
if processor.validate(current_data):
current_data = processor.process(current_data)
else:
raise ValueError(f"Validation failed in {processor.__class__.__name__}")
return current_data
# ๐ฎ Game entity protocol
class Drawable(Protocol):
def draw(self, screen: 'Screen') -> None: ...
class Updatable(Protocol):
def update(self, delta_time: float) -> None: ...
class Collidable(Protocol):
def get_bounds(self) -> 'Rectangle': ...
def on_collision(self, other: 'Collidable') -> None: ...
# Game entity that implements multiple protocols
class Player:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
self.health = 100
def draw(self, screen: 'Screen') -> None:
screen.draw_sprite("player", self.x, self.y)
def update(self, delta_time: float) -> None:
# Update player logic
pass
def get_bounds(self) -> 'Rectangle':
return Rectangle(self.x, self.y, 32, 32)
def on_collision(self, other: 'Collidable') -> None:
if isinstance(other, Enemy):
self.health -= 10
class Enemy:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def draw(self, screen: 'Screen') -> None:
screen.draw_sprite("enemy", self.x, self.y)
def update(self, delta_time: float) -> None:
# AI logic
pass
def get_bounds(self) -> 'Rectangle':
return Rectangle(self.x, self.y, 24, 24)
def on_collision(self, other: 'Collidable') -> None:
pass
# Game systems work with protocols
def update_entities(entities: list[Updatable], delta_time: float) -> None:
"""Update all updatable entities."""
for entity in entities:
entity.update(delta_time)
def render_entities(entities: list[Drawable], screen: 'Screen') -> None:
"""Render all drawable entities."""
for entity in entities:
entity.draw(screen)
def check_collisions(entities: list[Collidable]) -> None:
"""Check collisions between all collidable entities."""
for i, entity1 in enumerate(entities):
for entity2 in entities[i+1:]:
if rectangles_overlap(entity1.get_bounds(), entity2.get_bounds()):
entity1.on_collision(entity2)
entity2.on_collision(entity1)Python๐ Async Protocols
from typing import Protocol, AsyncIterator, Awaitable
class AsyncReadable(Protocol):
async def read(self, size: int = -1) -> str: ...
async def readline(self) -> str: ...
def __aiter__(self) -> AsyncIterator[str]: ...
class AsyncWritable(Protocol):
async def write(self, data: str) -> int: ...
async def flush(self) -> None: ...
async def async_copy_content(source: AsyncReadable, dest: AsyncWritable) -> None:
"""Async copy operation using protocols."""
async for line in source:
await dest.write(line)
await dest.flush()
# WebSocket protocol
class WebSocketConnection(Protocol):
async def send(self, message: str) -> None: ...
async def receive(self) -> str: ...
async def close(self) -> None: ...
async def chat_handler(websocket: WebSocketConnection) -> None:
"""Handle chat messages via WebSocket."""
try:
async for message in websocket:
# Echo the message back
await websocket.send(f"Echo: {message}")
finally:
await websocket.close()Pythonโก Performance Considerations
graph TD
A[Protocol Performance] --> B[โ
Zero Runtime Cost]
A --> C[โ ๏ธ Runtime Checkable Cost]
A --> D[๐ฏ Design Benefits]
B --> E[Type checking only]
C --> F["isinstance() overhead"]
D --> G[Better code organization]
D --> H[Easier testing]
style B fill:#e8f5e8
style C fill:#fff3e0
style D fill:#e3f2fd๐งช Testing with Protocols
from typing import Protocol
import pytest
from unittest.mock import Mock
class EmailSender(Protocol):
def send_email(self, to: str, subject: str, body: str) -> bool: ...
class NotificationService:
def __init__(self, email_sender: EmailSender) -> None:
self.email_sender = email_sender
def notify_user(self, user_email: str, message: str) -> bool:
return self.email_sender.send_email(
to=user_email,
subject="Notification",
body=message
)
# โ
Easy to test with mocks
def test_notification_service():
# Mock implements the protocol automatically
mock_sender = Mock(spec=EmailSender)
mock_sender.send_email.return_value = True
service = NotificationService(mock_sender)
result = service.notify_user("test@example.com", "Hello!")
assert result is True
mock_sender.send_email.assert_called_once_with(
to="test@example.com",
subject="Notification",
body="Hello!"
)
# โ
Test with fake implementation
class FakeEmailSender:
def __init__(self) -> None:
self.sent_emails: list[dict] = []
def send_email(self, to: str, subject: str, body: str) -> bool:
self.sent_emails.append({"to": to, "subject": subject, "body": body})
return True
def test_notification_service_with_fake():
fake_sender = FakeEmailSender()
service = NotificationService(fake_sender)
service.notify_user("test@example.com", "Hello!")
assert len(fake_sender.sent_emails) == 1
assert fake_sender.sent_emails[0]["to"] == "test@example.com"PythonGeneric Protocols
from typing import Protocol, TypeVar, Generic
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
class Container(Protocol[T_co]):
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[T_co]: ...
class Comparable(Protocol):
def __lt__(self: T, other: T) -> bool: ...
def sort_container(container: Container[Comparable]) -> List[Comparable]:
return sorted(container)
# Works with any container of comparable items
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sort_container(numbers) # List[int]
words = ["banana", "apple", "cherry"]
sorted_words = sort_container(words) # List[str]PythonComplex Protocol Examples
from typing import Protocol, Any, Dict
class JSONSerializable(Protocol):
def to_dict(self) -> Dict[str, Any]: ...
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'JSONSerializable': ...
class DatabaseModel(Protocol):
id: int
def save(self) -> None: ...
def delete(self) -> None: ...
@classmethod
def find_by_id(cls, model_id: int) -> Optional['DatabaseModel']: ...
class APIResource(Protocol):
def get(self, resource_id: str) -> Dict[str, Any]: ...
def post(self, data: Dict[str, Any]) -> Dict[str, Any]: ...
def put(self, resource_id: str, data: Dict[str, Any]) -> Dict[str, Any]: ...
def delete(self, resource_id: str) -> bool: ...
def process_api_resource(resource: APIResource, action: str) -> Any:
if action == "list":
return resource.get("")
elif action == "create":
return resource.post({"name": "test"})
# ... more actionsPython8. Type Checking Tools
Static Type Checkers Comparison
graph TD
A[Type Checkers] --> B[mypy]
A --> C[pyright/pylance]
A --> D[pyre]
B --> E[Gradual Typing]
B --> F[Plugin System]
C --> G[Fast Performance]
C --> H[VS Code Integration]
D --> I[Facebook/Meta]
D --> J[Advanced Features]Setting Up mypy
# mypy.ini
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True
[mypy-tests.*]
disallow_untyped_defs = FalseINIType Checking Examples
# example.py
from typing import List, Optional, Union
def process_data(data: List[Union[int, str]]) -> Optional[str]:
if not data:
return None
result = []
for item in data:
if isinstance(item, int):
result.append(str(item * 2))
else:
result.append(item.upper())
return " ".join(result)
# Type checking will catch these errors:
# process_data(42) # Error: Argument 1 has incompatible type "int"
# process_data([1, 2, 3.14]) # Error: List item 2 has incompatible type "float"PythonType Ignoring and Suppression
from typing import Any, cast
def legacy_function() -> Any:
# Legacy code that returns various types
return {"mixed": "data", "numbers": [1, 2, 3]}
def modern_function() -> Dict[str, Any]:
result = legacy_function()
# Type assertion for type checker
return cast(Dict[str, Any], result)
def problematic_code() -> None:
# Sometimes you need to ignore type checking
data = some_untyped_library_function() # type: ignore
# Or suppress specific error codes
value = unsafe_operation() # type: ignore[attr-defined]PythonAdvanced Type Checking Configuration
# Type checking with conditional imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Only imported during type checking, not at runtime
from expensive_module import ExpensiveClass
def process_expensive_data(data: 'ExpensiveClass') -> str:
# Forward reference avoids runtime import
return data.process()
# Reveal types for debugging
def debug_types() -> None:
x = [1, 2, 3]
reveal_type(x) # Revealed type is 'builtins.list[builtins.int]'
y = {1: "one", 2: "two"}
reveal_type(y) # Revealed type is 'builtins.dict[builtins.int, builtins.str]'Python9. Best Practices
Type Annotation Best Practices
flowchart TD
A[Type Annotation Best Practices] --> B[Start Simple]
A --> C[Be Specific]
A --> D[Use Type Aliases]
A --> E[Document Complex Types]
B --> F[Add types gradually]
C --> G[Prefer specific types over Any]
D --> H[Make code readable]
E --> I[Complex unions need explanation]Progressive Typing Strategy
# Phase 1: Start with function signatures
def calculate_total(items: list, tax_rate: float) -> float:
subtotal = sum(item.price for item in items)
return subtotal * (1 + tax_rate)
# Phase 2: Add more specific types
from typing import List
def calculate_total_v2(items: List[dict], tax_rate: float) -> float:
subtotal = sum(item["price"] for item in items)
return subtotal * (1 + tax_rate)
# Phase 3: Use proper data structures
from dataclasses import dataclass
from typing import List
@dataclass
class Item:
name: str
price: float
category: str
def calculate_total_v3(items: List[Item], tax_rate: float) -> float:
subtotal = sum(item.price for item in items)
return subtotal * (1 + tax_rate)PythonError Handling with Types
from typing import Union, Tuple, Optional
from dataclasses import dataclass
@dataclass
class Error:
message: str
code: int
# Result type pattern
Result = Union[Tuple[bool, str], Tuple[bool, Error]]
def validate_email(email: str) -> Result:
if "@" not in email:
return False, Error("Invalid email format", 400)
if len(email) < 5:
return False, Error("Email too short", 400)
return True, "Valid email"
# Option type pattern
def find_user_by_id(user_id: int) -> Optional[dict]:
users_db = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return users_db.get(user_id)
# Exception-based approach with types
class UserNotFoundError(Exception):
def __init__(self, user_id: int) -> None:
super().__init__(f"User with ID {user_id} not found")
self.user_id = user_id
def get_user_by_id(user_id: int) -> dict:
users_db = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
if user_id not in users_db:
raise UserNotFoundError(user_id)
return users_db[user_id]PythonPerformance Considerations
from typing import Iterator, Generator, List
import sys
# Use Iterator/Generator for memory efficiency
def read_large_file(filename: str) -> Iterator[str]:
with open(filename, 'r') as file:
for line in file:
yield line.strip()
def process_numbers() -> Generator[int, None, None]:
for i in range(1000000):
yield i * 2
# Avoid creating unnecessary intermediate lists
def sum_squares_efficient(numbers: Iterator[int]) -> int:
return sum(x * x for x in numbers)
def sum_squares_inefficient(numbers: Iterator[int]) -> int:
# Don't do this - creates intermediate list
squares: List[int] = [x * x for x in numbers]
return sum(squares)PythonType-Safe Configuration
from typing import TypedDict, Literal, get_type_hints
from dataclasses import dataclass
import os
class DatabaseConfig(TypedDict):
host: str
port: int
username: str
password: str
database: str
class RedisConfig(TypedDict):
host: str
port: int
password: str
class AppConfig(TypedDict):
debug: bool
environment: Literal["development", "staging", "production"]
database: DatabaseConfig
redis: RedisConfig
def load_config_from_env() -> AppConfig:
return {
"debug": os.getenv("DEBUG", "false").lower() == "true",
"environment": os.getenv("ENVIRONMENT", "development"),
"database": {
"host": os.getenv("DB_HOST", "localhost"),
"port": int(os.getenv("DB_PORT", "5432")),
"username": os.getenv("DB_USER", ""),
"password": os.getenv("DB_PASSWORD", ""),
"database": os.getenv("DB_NAME", ""),
},
"redis": {
"host": os.getenv("REDIS_HOST", "localhost"),
"port": int(os.getenv("REDIS_PORT", "6379")),
"password": os.getenv("REDIS_PASSWORD", ""),
}
}Python10. Real-World Examples
Web API with Type Safety
from typing import Dict, List, Optional, Union
from dataclasses import dataclass, asdict
from datetime import datetime
from enum import Enum
class UserRole(Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
@dataclass
class User:
id: int
username: str
email: str
role: UserRole
created_at: datetime
is_active: bool = True
@dataclass
class CreateUserRequest:
username: str
email: str
password: str
role: UserRole = UserRole.USER
@dataclass
class UserResponse:
id: int
username: str
email: str
role: str
created_at: str
is_active: bool
class UserService:
def __init__(self) -> None:
self._users: Dict[int, User] = {}
self._next_id = 1
def create_user(self, request: CreateUserRequest) -> UserResponse:
user = User(
id=self._next_id,
username=request.username,
email=request.email,
role=request.role,
created_at=datetime.now()
)
self._users[user.id] = user
self._next_id += 1
return UserResponse(
id=user.id,
username=user.username,
email=user.email,
role=user.role.value,
created_at=user.created_at.isoformat(),
is_active=user.is_active
)
def get_user(self, user_id: int) -> Optional[UserResponse]:
user = self._users.get(user_id)
if not user:
return None
return UserResponse(
id=user.id,
username=user.username,
email=user.email,
role=user.role.value,
created_at=user.created_at.isoformat(),
is_active=user.is_active
)
def list_users(self, role: Optional[UserRole] = None) -> List[UserResponse]:
users = self._users.values()
if role:
users = [u for u in users if u.role == role]
return [
UserResponse(
id=user.id,
username=user.username,
email=user.email,
role=user.role.value,
created_at=user.created_at.isoformat(),
is_active=user.is_active
)
for user in users
]PythonData Processing Pipeline
from typing import Protocol, TypeVar, Generic, Callable, Iterator, List
from abc import abstractmethod
T = TypeVar('T')
U = TypeVar('U')
class Processor(Protocol[T, U]):
@abstractmethod
def process(self, item: T) -> U: ...
class Filter(Protocol[T]):
@abstractmethod
def should_include(self, item: T) -> bool: ...
class Pipeline(Generic[T]):
def __init__(self, data: Iterator[T]) -> None:
self._data = data
def filter(self, predicate: Filter[T]) -> 'Pipeline[T]':
filtered_data = (item for item in self._data if predicate.should_include(item))
return Pipeline(filtered_data)
def map(self, processor: Processor[T, U]) -> 'Pipeline[U]':
mapped_data = (processor.process(item) for item in self._data)
return Pipeline(mapped_data)
def collect(self) -> List[T]:
return list(self._data)
# Concrete implementations
@dataclass
class Person:
name: str
age: int
salary: float
class AdultFilter:
def should_include(self, person: Person) -> bool:
return person.age >= 18
class SalaryProcessor:
def __init__(self, tax_rate: float) -> None:
self.tax_rate = tax_rate
def process(self, person: Person) -> float:
return person.salary * (1 - self.tax_rate)
# Usage
people = [
Person("Alice", 25, 50000),
Person("Bob", 17, 30000),
Person("Charlie", 30, 75000)
]
pipeline = Pipeline(iter(people))
net_salaries = (pipeline
.filter(AdultFilter())
.map(SalaryProcessor(0.25))
.collect())PythonTesting with Types
from typing import Any, Dict, List, Callable, TypeVar
import unittest
from unittest.mock import Mock, patch
F = TypeVar('F', bound=Callable[..., Any])
def typed_mock(**kwargs: Any) -> Mock:
"""Create a mock with better type support."""
return Mock(**kwargs)
class TypedTestCase(unittest.TestCase):
def assert_type_equal(self, obj: Any, expected_type: type) -> None:
"""Assert that an object is of the expected type."""
self.assertIsInstance(obj, expected_type)
def assert_callable_with_signature(
self,
func: Callable[..., Any],
args: List[Any],
kwargs: Dict[str, Any]
) -> None:
"""Assert that a function can be called with given arguments."""
try:
func(*args, **kwargs)
except Exception as e:
self.fail(f"Function call failed: {e}")
class TestUserService(TypedTestCase):
def setUp(self) -> None:
self.user_service = UserService()
def test_create_user_returns_correct_type(self) -> None:
request = CreateUserRequest(
username="test_user",
email="test@example.com",
password="password123"
)
response = self.user_service.create_user(request)
self.assert_type_equal(response, UserResponse)
self.assertEqual(response.username, "test_user")
self.assertEqual(response.email, "test@example.com")
@patch('datetime.datetime')
def test_create_user_with_mocked_datetime(self, mock_datetime: Mock) -> None:
mock_now = datetime(2023, 1, 1, 12, 0, 0)
mock_datetime.now.return_value = mock_now
request = CreateUserRequest(
username="test_user",
email="test@example.com",
password="password123"
)
response = self.user_service.create_user(request)
self.assertEqual(response.created_at, mock_now.isoformat())Python11. Common Pitfalls and Troubleshooting
๐จ Most Common Type Errors
1. Argument Type Mismatch
# โ Error: Argument 1 has incompatible type "int"; expected "str"
def greet(name: str) -> str:
return f"Hello, {name}!"
greet(123) # Should be greet("123")PythonSolution: Always check argument types match function signatures.
2. Optional vs Non-Optional Confusion
# โ Error: Item "None" of "Optional[str]" has no attribute "upper"
def get_user_name(user_id: int) -> Optional[str]:
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
def format_name(user_id: int) -> str:
name = get_user_name(user_id)
return name.upper() # Error! name might be NonePythonSolution: Always check for None before using Optional values.
# โ
Correct approach
def format_name(user_id: int) -> str:
name = get_user_name(user_id)
if name is None:
return "Unknown User"
return name.upper()Python3. Generic Type Parameter Missing
# โ Missing type parameters for generic type "dict"
def process_data(data: dict) -> None: # Too generic!
pass
# โ
Specify type parameters
def process_data(data: dict[str, int]) -> None:
passPython๐ง Debugging Type Issues
Using reveal_type()
def debug_function(items: list[str]) -> None:
reveal_type(items) # Reveals: list[str]
processed = [item.upper() for item in items]
reveal_type(processed) # Reveals: list[str]
filtered = [item for item in processed if len(item) > 3]
reveal_type(filtered) # Reveals: list[str]PythonCommon Type Checker Messages
graph TD
A[Type Error Categories] --> B[๐ด Incompatible Types]
A --> C[๐ก Missing Type Info]
A --> D[๐ Union Issues]
A --> E[๐ต Generic Problems]
B --> F["Expected X, got Y"]
C --> G["has no attribute"]
D --> H["Too many union members"]
E --> I["Missing type parameters"]
style B fill:#ffebee
style C fill:#fff8e1
style D fill:#fff3e0
style E fill:#e3f2fd๐ ๏ธ IDE Integration and Setup
VS Code Setup
- Install Python Extension
- Enable Pylance (Microsoft’s Python language server)
- Configure settings.json:
{
"python.analysis.typeCheckingMode": "strict",
"python.analysis.autoImportCompletions": true,
"python.analysis.diagnosticMode": "workspace"
}JSONPyCharm Setup
- Enable Type Checking: Settings โ Editor โ Inspections โ Python โ Type checker
- Configure External Tool: Add mypy as external tool
- Enable Type Hints: Settings โ Editor โ Code Style โ Python
๐ Performance and Type Checking
Type Checking Performance Tips
# โ
Use TYPE_CHECKING for expensive imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from expensive_module import ExpensiveClass
else:
ExpensiveClass = object
def process_data(data: 'ExpensiveClass') -> str:
# Forward reference prevents runtime import cost
return str(data)
# โ
Use string annotations for forward references
class Node:
def __init__(self, value: int, parent: 'Optional[Node]' = None) -> None:
self.value = value
self.parent = parent
self.children: list['Node'] = []PythonRuntime Performance Considerations
# โ
Type hints have minimal runtime overhead
def typed_function(data: list[int]) -> int:
return sum(data)
# Type hints are stored in __annotations__ and ignored at runtime
print(typed_function.__annotations__)
# Output: {'data': list[int], 'return': int}Python๐ Type Checking Configuration
mypy Configuration Examples
# mypy.ini - Strict configuration
[mypy]
python_version = 3.11
strict = True
warn_return_any = True
warn_unused_configs = True
# Per-module configuration
[mypy-tests.*]
ignore_errors = True
[mypy-external_lib.*]
ignore_missing_imports = True
# Gradual adoption
[mypy-legacy_module.*]
disallow_untyped_defs = FalseINIpyproject.toml Configuration
[tool.mypy]
python_version = "3.11"
strict = true
exclude = ["tests/", "build/"]
[[tool.mypy.overrides]]
module = "external_lib.*"
ignore_missing_imports = trueINI๐ฏ Migration Strategies
Legacy Code Migration
graph LR
A[Legacy Code] --> B[1. Add Return Types]
B --> C[2. Add Parameter Types]
C --> D[3. Add Variable Types]
D --> E[4. Enable Strict Mode]
E --> F[5. Full Coverage]
style A fill:#ffebee
style F fill:#e8f5e8Gradual Typing Example
# Step 1: Original code
def process_user_data(user_data):
return user_data["name"].upper()
# Step 2: Add return type
def process_user_data(user_data) -> str:
return user_data["name"].upper()
# Step 3: Add parameter type
def process_user_data(user_data: dict) -> str:
return user_data["name"].upper()
# Step 4: Be more specific
def process_user_data(user_data: dict[str, str]) -> str:
return user_data["name"].upper()
# Step 5: Use proper types
from typing import TypedDict
class UserData(TypedDict):
name: str
email: str
age: int
def process_user_data(user_data: UserData) -> str:
return user_data["name"].upper()Python๐ Testing Type Annotations
import pytest
from typing import get_type_hints
def test_function_type_hints():
"""Test that functions have correct type hints."""
def sample_function(x: int, y: str) -> bool:
return len(y) > x
hints = get_type_hints(sample_function)
assert hints['x'] == int
assert hints['y'] == str
assert hints['return'] == bool
def test_class_type_hints():
"""Test class attribute type hints."""
class Sample:
name: str
age: int
hints = get_type_hints(Sample)
assert hints['name'] == str
assert hints['age'] == intPython12. Learning Roadmap
๐ Structured Learning Path
graph TD
A[๐ Beginner] --> B[๐ Intermediate]
B --> C[๐ฏ Advanced]
C --> D[๐ Expert]
A --> A1[Basic types: str, int, float]
A --> A2[Optional and Union types]
A --> A3[List and dict typing]
B --> B1[Generic types and TypeVar]
B --> B2[Protocols and ABC]
B --> B3[TypedDict and dataclasses]
C --> C1[Complex generics and bounds]
C --> C2[Custom protocols]
C --> C3[Type guards and narrowing]
D --> D1[Variance and contravariance]
D --> D2[Plugin development]
D --> D3[Type system design]
style A fill:#e8f5e8
style B fill:#fff3e0
style C fill:#ffebee
style D fill:#e3f2fd๐ฏ Practice Projects by Skill Level
Beginner Projects
- Calculator with Types: Build a calculator with full type annotations
- Contact Book: Create a contact management system
- Weather App: Build a weather data processor with APIs
Intermediate Projects
- REST API: Build a typed FastAPI application
- Data Analysis Tool: Create a pandas wrapper with proper types
- Configuration Manager: Build a type-safe config system
Advanced Projects
- ORM Framework: Design a mini-ORM with generic types
- Plugin System: Create an extensible plugin architecture
- DSL Creator: Build a domain-specific language with typing
๐ Essential Resources
Official Documentation
Tools and Libraries
- Type Checkers: mypy, pyright, pyre
- Runtime Libraries: pydantic, marshmallow, cattrs
- IDE Support: VS Code + Pylance, PyCharm
Community Resources
Conclusion
Python typing has evolved from a simple annotation system to a powerful tool for building robust, maintainable applications. Here’s a summary of key takeaways:
Type Adoption Strategy
graph LR
A[Start] --> B[Basic Types]
B --> C[Collections]
C --> D[Advanced Features]
D --> E[Protocols]
E --> F[Generics]
F --> G[Expert Level]Benefits Realized
- Development Speed: Better IDE support and autocomplete
- Code Quality: Early detection of type-related bugs
- Documentation: Self-documenting code through type hints
- Refactoring Safety: Confident code changes with type checking
- Team Collaboration: Clearer interfaces and contracts
Next Steps
- Practice: Start adding types to your existing projects gradually
- Tools: Set up mypy or pyright in your development workflow
- Community: Explore typed libraries and contribute to typing discussions
- Advanced Topics: Dive deeper into variance, higher-kinded types, and dependent typing
Resources for Further Learning
Remember: Type hints are a tool to make your code better, not a burden. Start small, be consistent, and gradually adopt more advanced features as you become comfortable with the basics.
Discover more from Altgr Blog
Subscribe to get the latest posts sent to your email.
