In Parts 1-3, you mastered Python’s type() function, learned type hints and annotations, and discovered when to use duck typing versus static typing. Now it’s time to level up with advanced type patterns that separate junior developers from senior ones.
Previous parts in this series
- Part – 1 – Python type() Function Explained: Everything You Need to Know
- Part – 2 – Python Type Hints and Annotations: From Beginner to Pro
- Part – 3 – Python’s Duck Typing vs Static Typing: When to Use Each
After building type-safe systems for Fortune 500 companies and open-source projects with millions of users, I’ll show you the professional-grade typing techniques that make Python codebases truly maintainable. You’ll learn generics, protocols, advanced type variables, and performance optimization strategies that most Python developers never master.
Page Contents
Generic Types: Writing Reusable, Type-Safe Code
Generic types let you write functions and classes that work with multiple types while maintaining type safety. Think of them as templates that get filled in with specific types later.
Basic Generic Functions
from typing import TypeVar, List, Optional, Generic, Dict
T = TypeVar('T')
K = TypeVar('K') # Key type
V = TypeVar('V') # Value type
def first_or_none(items: List[T]) -> Optional[T]:
"""Get first item or None if list is empty."""
return items[0] if items else None
def last_or_none(items: List[T]) -> Optional[T]:
"""Get last item or None if list is empty."""
return items[-1] if items else None
# Usage with type inference
numbers = [1, 2, 3, 4, 5]
first_number = first_or_none(numbers) # Type: Optional[int]
words = ["hello", "world", "python"]
first_word = first_or_none(words) # Type: Optional[str]
# Type checker knows the return types!
if first_number is not None:
print(first_number + 10) # Safe: knows it's int
if first_word is not None:
print(first_word.upper()) # Safe: knows it's str
Advanced Generic Functions with Multiple Type Variables
from typing import Callable, Tuple
def map_pair(func: Callable[[T], U], pair: Tuple[T, T]) -> Tuple[U, U]:
"""Apply function to both elements of a pair."""
return (func(pair[0]), func(pair[1]))
def zip_dict(keys: List[K], values: List[V]) -> Dict[K, V]:
"""Create dictionary from key and value lists."""
return dict(zip(keys, values))
# Examples
number_pair = (10, 20)
string_pair = map_pair(str, number_pair) # Type: Tuple[str, str]
print(string_pair) # ('10', '20')
keys = ["a", "b", "c"]
values = [1, 2, 3]
result = zip_dict(keys, values) # Type: Dict[str, int]
print(result) # {'a': 1, 'b': 2, 'c': 3}
Generic Classes: Building Type-Safe Data Structures
from typing import Generic, Iterator, Optional
from dataclasses import dataclass
@dataclass
class Node(Generic[T]):
"""Generic node for linked structures."""
data: T
next: Optional['Node[T]'] = None
class Stack(Generic[T]):
"""Generic stack implementation."""
def __init__(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
"""Push item onto stack."""
self._items.append(item)
def pop(self) -> Optional[T]:
"""Pop item from stack."""
return self._items.pop() if self._items else None
def peek(self) -> Optional[T]:
"""Look at top item without removing."""
return self._items[-1] if self._items else None
def is_empty(self) -> bool:
"""Check if stack is empty."""
return len(self._items) == 0
def __len__(self) -> int:
"""Get stack size."""
return len(self._items)
# Type-safe usage
int_stack = Stack[int]()
int_stack.push(1)
int_stack.push(2)
int_stack.push(3)
top = int_stack.pop() # Type: Optional[int]
if top is not None:
print(f"Popped: {top * 2}") # Type checker knows it's int
string_stack = Stack[str]()
string_stack.push("hello")
string_stack.push("world")
word = string_stack.pop() # Type: Optional[str]
if word is not None:
print(f"Popped: {word.upper()}") # Type checker knows it's str
Bounded Type Variables
from typing import Protocol
from numbers import Number
class Comparable(Protocol):
"""Protocol for objects that can be compared."""
def __lt__(self, other: 'Comparable') -> bool: ...
def __gt__(self, other: 'Comparable') -> bool: ...
# Bounded type variable
ComparableType = TypeVar('ComparableType', bound=Comparable)
NumberType = TypeVar('NumberType', bound=Number)
def find_max(items: List[ComparableType]) -> Optional[ComparableType]:
"""Find maximum item that supports comparison."""
return max(items) if items else None
def safe_divide(a: NumberType, b: NumberType) -> Optional[NumberType]:
"""Safely divide numbers, return None if division by zero."""
if b == 0:
return None
return a / b
# Usage
numbers = [1, 5, 3, 9, 2]
max_num = find_max(numbers) # Type: Optional[int]
strings = ["apple", "banana", "cherry"]
max_str = find_max(strings) # Type: Optional[str]
# Type checker prevents errors
result1 = safe_divide(10.0, 3.0) # Type: Optional[float]
result2 = safe_divide(20, 4) # Type: Optional[int]
Protocols: Duck Typing with Type Safety
Protocols define interfaces without requiring inheritance, combining the flexibility of duck typing with the safety of static typing.
Creating Custom Protocols
from typing import Protocol, runtime_checkable
from abc import abstractmethod
@runtime_checkable
class Drawable(Protocol):
"""Protocol for objects that can be drawn."""
def draw(self) -> str:
"""Draw the object and return string representation."""
...
def get_area(self) -> float:
"""Calculate and return the area."""
...
@runtime_checkable
class Serializable(Protocol):
"""Protocol for objects that can be serialized."""
def serialize(self) -> dict:
"""Convert object to dictionary."""
...
@classmethod
def deserialize(cls, data: dict) -> 'Serializable':
"""Create object from dictionary."""
...
# Implementations don't need to inherit from protocols
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def draw(self) -> str:
return f"Circle with radius {self.radius}"
def get_area(self) -> float:
return 3.14159 * self.radius ** 2
def serialize(self) -> dict:
return {"type": "circle", "radius": self.radius}
@classmethod
def deserialize(cls, data: dict) -> 'Circle':
return cls(data["radius"])
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def draw(self) -> str:
return f"Rectangle {self.width}x{self.height}"
def get_area(self) -> float:
return self.width * self.height
def serialize(self) -> dict:
return {"type": "rectangle", "width": self.width, "height": self.height}
@classmethod
def deserialize(cls, data: dict) -> 'Rectangle':
return cls(data["width"], data["height"])
# Functions that work with protocols
def render_drawable(obj: Drawable) -> str:
"""Render any drawable object."""
area = obj.get_area()
drawing = obj.draw()
return f"{drawing} (Area: {area:.2f})"
def save_serializable(obj: Serializable, filename: str) -> None:
"""Save any serializable object to file."""
import json
data = obj.serialize()
with open(filename, 'w') as f:
json.dump(data, f)
# Usage - type checker verifies protocol compliance
shapes: List[Drawable] = [
Circle(5.0),
Rectangle(4.0, 6.0)
]
for shape in shapes:
print(render_drawable(shape))
# Runtime protocol checking
circle = Circle(3.0)
print(isinstance(circle, Drawable)) # True
print(isinstance(circle, Serializable)) # True
Protocol Composition and Inheritance
class Movable(Protocol):
"""Protocol for objects that can be moved."""
def move(self, dx: float, dy: float) -> None: ...
class Rotatable(Protocol):
"""Protocol for objects that can be rotated."""
def rotate(self, angle: float) -> None: ...
class Transform(Protocol):
"""Protocol that combines multiple transformation abilities."""
def move(self, dx: float, dy: float) -> None: ...
def rotate(self, angle: float) -> None: ...
def scale(self, factor: float) -> None: ...
class GameSprite:
"""Game sprite that implements multiple protocols."""
def __init__(self, x: float, y: float, angle: float = 0.0) -> None:
self.x = x
self.y = y
self.angle = angle
self.scale_factor = 1.0
def move(self, dx: float, dy: float) -> None:
self.x += dx
self.y += dy
def rotate(self, angle: float) -> None:
self.angle += angle
def scale(self, factor: float) -> None:
self.scale_factor *= factor
def draw(self) -> str:
return f"Sprite at ({self.x}, {self.y}), angle={self.angle}°, scale={self.scale_factor}"
def animate_movable(obj: Movable, path: List[Tuple[float, float]]) -> None:
"""Animate any movable object along a path."""
for dx, dy in path:
obj.move(dx, dy)
def apply_transform(obj: Transform, dx: float, dy: float, angle: float, scale: float) -> None:
"""Apply complete transformation to object."""
obj.move(dx, dy)
obj.rotate(angle)
obj.scale(scale)
# Usage
sprite = GameSprite(0, 0)
path = [(1, 0), (0, 1), (-1, 0), (0, -1)]
animate_movable(sprite, path) # Works because GameSprite implements Movable
apply_transform(sprite, 10, 10, 45, 1.5) # Works because GameSprite implements Transform
print(sprite.draw())
Advanced Type Variables and Constraints
Covariant and Contravariant Type Variables
from typing import TypeVar, List, Callable, Generic
# Covariant type variable (preserves subtype relationship)
T_co = TypeVar('T_co', covariant=True)
# Contravariant type variable (reverses subtype relationship)
T_contra = TypeVar('T_contra', contravariant=True)
class Producer(Generic[T_co]):
"""Producer that only outputs values (covariant)."""
def __init__(self, value: T_co) -> None:
self._value = value
def get(self) -> T_co:
return self._value
class Consumer(Generic[T_contra]):
"""Consumer that only accepts values (contravariant)."""
def consume(self, value: T_contra) -> None:
print(f"Consuming: {value}")
# Covariance example
class Animal:
def speak(self) -> str:
return "Some sound"
class Dog(Animal):
def speak(self) -> str:
return "Woof!"
# Producer[Dog] is a subtype of Producer[Animal] (covariant)
dog_producer: Producer[Dog] = Producer(Dog())
animal_producer: Producer[Animal] = dog_producer # This works!
# Contravariance example
animal_consumer: Consumer[Animal] = Consumer()
dog_consumer: Consumer[Dog] = animal_consumer # This works!
# Consumer[Animal] can consume Dog instances
dog_consumer.consume(Dog())
Complex Type Constraints
from typing import Union, overload, Literal
# Constrained type variables
AnyStr = TypeVar('AnyStr', str, bytes)
NumericType = TypeVar('NumericType', int, float, complex)
def concat(a: AnyStr, b: AnyStr) -> AnyStr:
"""Concatenate two items of the same string-like type."""
return a + b
def multiply_numeric(a: NumericType, b: NumericType) -> NumericType:
"""Multiply two numbers of the same numeric type."""
return a * b
# Usage
str_result = concat("hello", " world") # Type: str
bytes_result = concat(b"hello", b" world") # Type: bytes
int_result = multiply_numeric(5, 3) # Type: int
float_result = multiply_numeric(2.5, 4.0) # Type: float
# Using Literal types for precise control
def get_config(env: Literal["dev", "test", "prod"]) -> dict:
"""Get configuration for specific environment."""
configs = {
"dev": {"debug": True, "db": "dev.db"},
"test": {"debug": False, "db": "test.db"},
"prod": {"debug": False, "db": "prod.db"}
}
return configs[env]
# Type checker ensures only valid environments
dev_config = get_config("dev") # ✓ Valid
prod_config = get_config("prod") # ✓ Valid
# invalid_config = get_config("staging") # ✗ Type error
Type Performance Analysis and Optimization
Measuring Type Checking Performance
import time
import timeit
from typing import List, Dict, Any, Union, Optional
# Benchmark different typing approaches
def benchmark_typing_performance():
"""Compare performance of different type checking strategies."""
# Setup data
large_list = list(range(100000))
mixed_data = [1, "hello", 3.14, [1, 2, 3], {"key": "value"}] * 20000
# No type checking
def process_no_types(data):
return [str(item) for item in data]
# Runtime type checking
def process_with_runtime_checks(data: List[Any]) -> List[str]:
result = []
for item in data:
if isinstance(item, (int, float, str)):
result.append(str(item))
else:
result.append(repr(item))
return result
# Static type hints only (no runtime overhead)
def process_with_static_types(data: List[Union[int, str, float]]) -> List[str]:
return [str(item) for item in data]
# Benchmarks
times = {}
# No types
times['no_types'] = timeit.timeit(
lambda: process_no_types(large_list),
number=10
)
# Runtime checking
times['runtime_checks'] = timeit.timeit(
lambda: process_with_runtime_checks(mixed_data),
number=10
)
# Static types (no runtime cost)
times['static_types'] = timeit.timeit(
lambda: process_with_static_types(large_list),
number=10
)
return times
# Memory usage analysis
def analyze_type_memory_usage():
"""Analyze memory overhead of different typing approaches."""
import sys
# Function without types
def plain_function(x, y, z):
return x + y + z
# Function with simple types
def typed_function(x: int, y: int, z: int) -> int:
return x + y + z
# Function with complex types
def complex_typed_function(
data: Dict[str, List[Union[int, str, float]]],
config: Optional[Dict[str, Any]] = None
) -> Dict[str, Union[int, float]]:
# Implementation here
return {}
print("Memory usage comparison:")
print(f"Plain function: {sys.getsizeof(plain_function)} bytes")
print(f"Typed function: {sys.getsizeof(typed_function)} bytes")
print(f"Complex typed function: {sys.getsizeof(complex_typed_function)} bytes")
print("\nAnnotations storage:")
print(f"Plain: {getattr(plain_function, '__annotations__', 'None')}")
print(f"Typed: {typed_function.__annotations__}")
print(f"Complex: {complex_typed_function.__annotations__}")
# Run benchmarks
if __name__ == "__main__":
performance_results = benchmark_typing_performance()
for approach, time_taken in performance_results.items():
print(f"{approach}: {time_taken:.4f} seconds")
print("\n" + "="*50)
analyze_type_memory_usage()
Optimizing Type-Heavy Code
from typing import Final, ClassVar, NewType
from dataclasses import dataclass
from functools import lru_cache
# Use Final for constants to help type checkers optimize
MAX_ITEMS: Final[int] = 1000
DEFAULT_TIMEOUT: Final[float] = 30.0
API_VERSION: Final[str] = "v1"
# Use NewType for domain-specific types
UserId = NewType('UserId', int)
Email = NewType('Email', str)
Timestamp = NewType('Timestamp', float)
@dataclass(frozen=True) # Immutable for better performance
class User:
"""Optimized user data structure."""
id: UserId
email: Email
created_at: Timestamp
is_active: bool = True
# Class variables for shared data
_cache: ClassVar[Dict[UserId, 'User']] = {}
@classmethod
@lru_cache(maxsize=128)
def get_by_id(cls, user_id: UserId) -> Optional['User']:
"""Cached user lookup with type safety."""
return cls._cache.get(user_id)
def __post_init__(self) -> None:
"""Add to cache after creation."""
User._cache[self.id] = self
# Efficient generic container with minimal overhead
class FastGenericContainer(Generic[T]):
"""Memory-efficient generic container."""
__slots__ = ['_data', '_size']
def __init__(self) -> None:
self._data: List[T] = []
self._size = 0
def add(self, item: T) -> None:
"""Add item with O(1) amortized complexity."""
self._data.append(item)
self._size += 1
def get(self, index: int) -> T:
"""Get item by index."""
if 0 <= index < self._size:
return self._data[index]
raise IndexError("Index out of range")
def __len__(self) -> int:
return self._size
# Performance-optimized type checking
def fast_type_dispatch(value: Union[int, str, float, list]) -> str:
"""Fast type-based dispatch using isinstance tuple."""
# Single isinstance call with tuple is faster than multiple calls
if isinstance(value, (int, float)):
return f"Number: {value}"
elif isinstance(value, str):
return f"String: {value}"
elif isinstance(value, list):
return f"List with {len(value)} items"
else:
return "Unknown type"
# Example usage
user = User(
id=UserId(123),
email=Email("[email protected]"),
created_at=Timestamp(time.time())
)
container = FastGenericContainer[int]()
for i in range(1000):
container.add(i)
print(f"Container size: {len(container)}")
print(f"First item: {container.get(0)}")
print(fast_type_dispatch(42))
print(fast_type_dispatch("hello"))
Building Type-Safe Frameworks and Libraries
Plugin Architecture with Type Safety
from typing import Type, TypedDict, get_type_hints
from abc import ABC, abstractmethod
import inspect
class PluginConfig(TypedDict):
"""Typed configuration for plugins."""
name: str
version: str
enabled: bool
class BasePlugin(ABC, Generic[T]):
"""Base class for all plugins with generic data type."""
config: PluginConfig
def __init__(self, config: PluginConfig) -> None:
self.config = config
@abstractmethod
def process(self, data: T) -> T:
"""Process data of type T."""
pass
@abstractmethod
def validate_input(self, data: Any) -> bool:
"""Validate if data can be processed by this plugin."""
pass
class PluginRegistry:
"""Type-safe plugin registry."""
def __init__(self) -> None:
self._plugins: Dict[str, Type[BasePlugin]] = {}
def register(self, plugin_class: Type[BasePlugin]) -> None:
"""Register a plugin class with type validation."""
# Validate plugin implements required interface
if not issubclass(plugin_class, BasePlugin):
raise TypeError(f"{plugin_class} must inherit from BasePlugin")
# Extract type information
type_hints = get_type_hints(plugin_class.process)
if 'data' not in type_hints:
raise TypeError(f"{plugin_class} must have typed 'data' parameter")
plugin_name = getattr(plugin_class, 'PLUGIN_NAME', plugin_class.__name__)
self._plugins[plugin_name] = plugin_class
def create_plugin(self, name: str, config: PluginConfig) -> BasePlugin:
"""Create plugin instance with type safety."""
if name not in self._plugins:
raise KeyError(f"Plugin '{name}' not registered")
plugin_class = self._plugins[name]
return plugin_class(config)
def list_plugins(self) -> List[str]:
"""List all registered plugin names."""
return list(self._plugins.keys())
# Example plugin implementations
class TextProcessorPlugin(BasePlugin[str]):
"""Plugin for processing text data."""
PLUGIN_NAME = "text_processor"
def process(self, data: str) -> str:
"""Convert text to uppercase."""
return data.upper()
def validate_input(self, data: Any) -> bool:
"""Check if data is a string."""
return isinstance(data, str)
class NumberProcessorPlugin(BasePlugin[Union[int, float]]):
"""Plugin for processing numeric data."""
PLUGIN_NAME = "number_processor"
def process(self, data: Union[int, float]) -> Union[int, float]:
"""Square the number."""
return data ** 2
def validate_input(self, data: Any) -> bool:
"""Check if data is numeric."""
return isinstance(data, (int, float))
# Usage example
registry = PluginRegistry()
registry.register(TextProcessorPlugin)
registry.register(NumberProcessorPlugin)
# Type-safe plugin creation and usage
text_plugin = registry.create_plugin("text_processor", {
"name": "Text Processor",
"version": "1.0.0",
"enabled": True
})
result = text_plugin.process("hello world") # Type: str
print(f"Processed text: {result}")
number_plugin = registry.create_plugin("number_processor", {
"name": "Number Processor",
"version": "1.0.0",
"enabled": True
})
result = number_plugin.process(5.0) # Type: Union[int, float]
print(f"Processed number: {result}")
Type-Safe Configuration System
from typing import TypedDict, Union, get_origin, get_args
from dataclasses import dataclass, field
import json
class DatabaseConfig(TypedDict):
host: str
port: int
username: str
password: str
database: str
class RedisConfig(TypedDict):
host: str
port: int
password: Optional[str]
class AppConfig(TypedDict):
debug: bool
secret_key: str
database: DatabaseConfig
redis: RedisConfig
allowed_hosts: List[str]
@dataclass
class ConfigValidator:
"""Type-safe configuration validator."""
def validate_config(self, config_data: dict, expected_type: Type) -> bool:
"""Validate configuration against TypedDict."""
if not hasattr(expected_type, '__annotations__'):
return False
required_fields = expected_type.__annotations__
for field_name, field_type in required_fields.items():
if field_name not in config_data:
print(f"Missing required field: {field_name}")
return False
value = config_data[field_name]
if not self._check_type(value, field_type):
print(f"Invalid type for {field_name}: expected {field_type}, got {type(value)}")
return False
return True
def _check_type(self, value: Any, expected_type: Type) -> bool:
"""Check if value matches expected type."""
# Handle Optional types
if get_origin(expected_type) is Union:
args = get_args(expected_type)
return any(isinstance(value, arg) for arg in args if arg is not type(None))
# Handle List types
if get_origin(expected_type) is list:
if not isinstance(value, list):
return False
item_type = get_args(expected_type)[0]
return all(isinstance(item, item_type) for item in value)
# Handle TypedDict
if hasattr(expected_type, '__annotations__'):
return isinstance(value, dict) and self.validate_config(value, expected_type)
# Basic type checking
return isinstance(value, expected_type)
class ConfigManager:
"""Type-safe configuration manager."""
def __init__(self) -> None:
self.validator = ConfigValidator()
self._config: Optional[AppConfig] = None
def load_config(self, config_path: str) -> AppConfig:
"""Load and validate configuration from file."""
with open(config_path, 'r') as f:
raw_config = json.load(f)
if not self.validator.validate_config(raw_config, AppConfig):
raise ValueError("Invalid configuration")
self._config = raw_config
return self._config
def get_database_config(self) -> DatabaseConfig:
"""Get database configuration."""
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config['database']
def get_redis_config(self) -> RedisConfig:
"""Get Redis configuration."""
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config['redis']
# Example usage
sample_config = {
"debug": True,
"secret_key": "your-secret-key",
"database": {
"host": "localhost",
"port": 5432,
"username": "admin",
"password": "password",
"database": "myapp"
},
"redis": {
"host": "localhost",
"port": 6379,
"password": None
},
"allowed_hosts": ["localhost", "127.0.0.1"]
}
# Save sample config to file for testing
with open("config.json", "w") as f:
json.dump(sample_config, f, indent=2)
# Load and validate configuration
config_manager = ConfigManager()
config = config_manager.load_config("config.json")
# Type-safe access to configuration
db_config = config_manager.get_database_config()
print(f"Database host: {db_config['host']}:{db_config['port']}")
redis_config = config_manager.get_redis_config()
print(f"Redis host: {redis_config['host']}:{redis_config['port']}")
Integration with Static Analysis Tools
Advanced mypy Configuration
# mypy.ini - Production-ready configuration
[mypy]
python_version = 3.9
# Import discovery
namespace_packages = True
explicit_package_bases = True
# Type checking strictness
strict = True
warn_return_any = True
warn_unused_configs = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
# Error reporting
show_error_codes = True
show_column_numbers = True
pretty = True
color_output = True
# Per-module settings
[mypy-tests.*]
ignore_errors = True
[mypy-external_lib.*]
ignore_missing_imports = True
# Custom plugin configuration
[mypy.plugins.dataclasses]
init = True
eq = True
order = True
Custom Type Checker Plugins
# custom_plugin.py - Custom mypy plugin
from typing import Type as TypingType, Callable, Optional
from mypy.plugin import Plugin, AttributeContext, FunctionContext
from mypy.nodes import ARG_POS, ARG_STAR, Argument, Var, Decorator
from mypy.types import Type
class CustomValidationPlugin(Plugin):
"""Custom plugin for enhanced type validation."""
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], Type]]:
"""Hook for function call type checking."""
if fullname == 'myapp.validators.validate_email':
return self._validate_email_hook
return None
def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeContext], Type]]:
"""Hook for attribute access type checking."""
if fullname.endswith('.safe_get'):
return self._safe_get_hook
return None
def _validate_email_hook(self, context: FunctionContext) -> Type:
"""Custom validation for email validation function."""
# Add custom type checking logic here
return context.default_return_type
def _safe_get_hook(self, context: AttributeContext) -> Type:
"""Custom validation for safe attribute access."""
# Add custom type checking logic here
return context.default_attr_type
def plugin(version: str) -> type[Plugin]:
return CustomValidationPlugin
# Usage in code with plugin
from typing import TypeGuard
def is_email(value: str) -> TypeGuard[str]:
"""Type guard for email validation."""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, value))
def validate_email(email: str) -> str:
"""Validate email format."""
if is_email(email):
return email # Type checker knows this is a valid email
raise ValueError("Invalid email format")
# Advanced type guards
def is_list_of_strings(value: Any) -> TypeGuard[List[str]]:
"""Type guard for list of strings."""
return (isinstance(value, list) and
all(isinstance(item, str) for item in value))
def process_strings(data: Any) -> List[str]:
"""Process data if it's a list of strings."""
if is_list_of_strings(data):
# Type checker knows data is List[str] here
return [s.upper() for s in data]
raise TypeError("Expected list of strings")
Production-Ready Type Patterns
Error Handling with Result Types
from typing import Union, Generic, TypeVar, Optional
from dataclasses import dataclass
T = TypeVar('T')
E = TypeVar('E')
@dataclass(frozen=True)
class Ok(Generic[T]):
"""Success result with value."""
value: T
@dataclass(frozen=True)
class Err(Generic[E]):
"""Error result with error value."""
error: E
Result = Union[Ok[T], Err[E]]
class ResultType:
"""Utility methods for Result type."""
@staticmethod
def ok(value: T) -> Ok[T]:
"""Create success result."""
return Ok(value)
@staticmethod
def err(error: E) -> Err[E]:
"""Create error result."""
return Err(error)
@staticmethod
def is_ok(result: Result[T, E]) -> bool:
"""Check if result is Ok."""
return isinstance(result, Ok)
@staticmethod
def is_err(result: Result[T, E]) -> bool:
"""Check if result is Err."""
return isinstance(result, Err)
@staticmethod
def unwrap(result: Result[T, E]) -> T:
"""Get value from Ok result, raise if Err."""
if isinstance(result, Ok):
return result.value
raise RuntimeError(f"Called unwrap on Err: {result.error}")
@staticmethod
def unwrap_or(result: Result[T, E], default: T) -> T:
"""Get value from Ok result, return default if Err."""
if isinstance(result, Ok):
return result.value
return default
# Example usage with type-safe error handling
def divide_safe(a: float, b: float) -> Result[float, str]:
"""Safe division with Result type."""
if b == 0:
return ResultType.err("Division by zero")
return ResultType.ok(a / b)
def parse_int_safe(value: str) -> Result[int, str]:
"""Safe integer parsing."""
try:
return ResultType.ok(int(value))
except ValueError as e:
return ResultType.err(f"Invalid integer: {e}")
# Chain operations safely
def calculate_average(numbers_str: List[str]) -> Result[float, str]:
"""Calculate average of string numbers."""
total = 0
count = 0
for num_str in numbers_str:
result = parse_int_safe(num_str)
if ResultType.is_err(result):
return result # Propagate error
total += ResultType.unwrap(result)
count += 1
if count == 0:
return ResultType.err("Empty list")
avg_result = divide_safe(total, count)
return avg_result
# Usage
numbers = ["1", "2", "3", "4", "5"]
result = calculate_average(numbers)
if ResultType.is_ok(result):
print(f"Average: {ResultType.unwrap(result)}")
else:
print(f"Error: {result.error}")
State Machine with Type Safety
from enum import Enum
from typing import Dict, Type, Any
from abc import ABC, abstractmethod
class OrderStatus(Enum):
"""Order status enumeration."""
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class OrderState(ABC):
"""Abstract base state for order state machine."""
@abstractmethod
def confirm(self) -> 'OrderState':
"""Confirm the order."""
pass
@abstractmethod
def ship(self) -> 'OrderState':
"""Ship the order."""
pass
@abstractmethod
def deliver(self) -> 'OrderState':
"""Deliver the order."""
pass
@abstractmethod
def cancel(self) -> 'OrderState':
"""Cancel the order."""
pass
@property
@abstractmethod
def status(self) -> OrderStatus:
"""Get current status."""
pass
class PendingState(OrderState):
"""Order is pending confirmation."""
def confirm(self) -> 'ConfirmedState':
return ConfirmedState()
def ship(self) -> 'OrderState':
raise ValueError("Cannot ship pending order")
def deliver(self) -> 'OrderState':
raise ValueError("Cannot deliver pending order")
def cancel(self) -> 'CancelledState':
return CancelledState()
@property
def status(self) -> OrderStatus:
return OrderStatus.PENDING
class ConfirmedState(OrderState):
"""Order is confirmed and ready to ship."""
def confirm(self) -> 'ConfirmedState':
return self # Already confirmed
def ship(self) -> 'ShippedState':
return ShippedState()
def deliver(self) -> 'OrderState':
raise ValueError("Cannot deliver unshipped order")
def cancel(self) -> 'CancelledState':
return CancelledState()
@property
def status(self) -> OrderStatus:
return OrderStatus.CONFIRMED
class ShippedState(OrderState):
"""Order is shipped."""
def confirm(self) -> 'ShippedState':
return self
def ship(self) -> 'ShippedState':
return self # Already shipped
def deliver(self) -> 'DeliveredState':
return DeliveredState()
def cancel(self) -> 'OrderState':
raise ValueError("Cannot cancel shipped order")
@property
def status(self) -> OrderStatus:
return OrderStatus.SHIPPED
class DeliveredState(OrderState):
"""Order is delivered (final state)."""
def confirm(self) -> 'DeliveredState':
return self
def ship(self) -> 'DeliveredState':
return self
def deliver(self) -> 'DeliveredState':
return self # Already delivered
def cancel(self) -> 'OrderState':
raise ValueError("Cannot cancel delivered order")
@property
def status(self) -> OrderStatus:
return OrderStatus.DELIVERED
class CancelledState(OrderState):
"""Order is cancelled (final state)."""
def confirm(self) -> 'OrderState':
raise ValueError("Cannot confirm cancelled order")
def ship(self) -> 'OrderState':
raise ValueError("Cannot ship cancelled order")
def deliver(self) -> 'OrderState':
raise ValueError("Cannot deliver cancelled order")
def cancel(self) -> 'CancelledState':
return self # Already cancelled
@property
def status(self) -> OrderStatus:
return OrderStatus.CANCELLED
@dataclass
class Order:
"""Type-safe order with state machine."""
id: int
_state: OrderState = field(default_factory=PendingState)
@property
def status(self) -> OrderStatus:
"""Get current order status."""
return self._state.status
def confirm(self) -> None:
"""Confirm the order."""
self._state = self._state.confirm()
def ship(self) -> None:
"""Ship the order."""
self._state = self._state.ship()
def deliver(self) -> None:
"""Deliver the order."""
self._state = self._state.deliver()
def cancel(self) -> None:
"""Cancel the order."""
self._state = self._state.cancel()
# Usage example
order = Order(id=12345)
print(f"Initial status: {order.status}") # PENDING
order.confirm()
print(f"After confirm: {order.status}") # CONFIRMED
order.ship()
print(f"After ship: {order.status}") # SHIPPED
order.deliver()
print(f"After deliver: {order.status}") # DELIVERED
# This would raise an error:
# order.cancel() # ValueError: Cannot cancel delivered order
Key Takeaways and Best Practices
- Use generics to write reusable, type-safe code that works with multiple types
- Leverage protocols to combine duck typing flexibility with static type safety
- Implement proper error handling with Result types for robust applications
- Optimize performance by understanding the zero-runtime cost of static typing
- Build type-safe frameworks using advanced type patterns and custom validation
- Use static analysis tools like mypy with proper configuration for maximum benefit
Series Conclusion
You’ve now mastered Python’s type system from basic type() functions to advanced generic types and protocols. You understand when to use duck typing versus static typing, and you can build production-ready type-safe systems that scale.
The techniques in this series will make your code more maintainable, help you catch bugs early, and enable better collaboration with team members. Most importantly, you now have the knowledge to make informed decisions about when and how to apply different typing strategies in your Python projects.
What’s your next typing challenge? Share your experiences applying these advanced patterns in your projects – I’d love to hear how you’re using generics, protocols, and type-safe architecture in real-world applications!
External Links
- Python Documentation: typing Module
- mypy Documentation: Generics
- PEP 544 — Protocols: Structural subtyping
- Python Documentation: Generics
Tags: Python, Advanced Typing, Generics, Protocols, Type Variables, Performance Optimisation, Static Analysis, mypy, Type Safety, Software Architecture, Python Programming

