1"""
2Type Hints and Annotations in Python
3
4Demonstrates:
5- Basic type annotations
6- Generic types (List, Dict, Tuple)
7- Union and Optional
8- TypeVar and Generic classes
9- Protocol (structural subtyping)
10- Runtime type checking
11"""
12
13from typing import (
14 List, Dict, Tuple, Union, Optional, TypeVar, Generic, Protocol, Callable
15)
16from typing import get_type_hints, get_args, get_origin
17
18
19def section(title: str) -> None:
20 """Print a section header."""
21 print("\n" + "=" * 60)
22 print(f" {title}")
23 print("=" * 60)
24
25
26# =============================================================================
27# Basic Type Annotations
28# =============================================================================
29
30section("Basic Type Annotations")
31
32
33def greet(name: str, age: int) -> str:
34 """Function with basic type hints."""
35 return f"Hello {name}, you are {age} years old"
36
37
38result = greet("Alice", 30)
39print(f"greet('Alice', 30) -> {result}")
40print(f"Function annotations: {greet.__annotations__}")
41
42
43# =============================================================================
44# Collection Types
45# =============================================================================
46
47section("Collection Types")
48
49
50def process_scores(scores: List[int]) -> Dict[str, float]:
51 """Process a list of scores and return statistics."""
52 return {
53 "mean": sum(scores) / len(scores) if scores else 0,
54 "max": max(scores) if scores else 0,
55 "min": min(scores) if scores else 0
56 }
57
58
59scores = [85, 92, 78, 95, 88]
60stats = process_scores(scores)
61print(f"Scores: {scores}")
62print(f"Statistics: {stats}")
63
64
65def coordinates() -> Tuple[float, float, float]:
66 """Return 3D coordinates."""
67 return (1.5, 2.7, 3.9)
68
69
70coords = coordinates()
71print(f"Coordinates: {coords}")
72
73
74# =============================================================================
75# Union and Optional
76# =============================================================================
77
78section("Union and Optional")
79
80
81def process_id(user_id: Union[int, str]) -> str:
82 """Accept either int or str ID."""
83 return f"Processing ID: {user_id} (type: {type(user_id).__name__})"
84
85
86print(process_id(12345))
87print(process_id("USER_789"))
88
89
90def find_user(user_id: int) -> Optional[Dict[str, str]]:
91 """Return user dict or None if not found."""
92 users = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
93 return users.get(user_id)
94
95
96user = find_user(1)
97print(f"find_user(1): {user}")
98user = find_user(99)
99print(f"find_user(99): {user}")
100
101
102# =============================================================================
103# TypeVar and Generics
104# =============================================================================
105
106section("TypeVar and Generics")
107
108T = TypeVar('T')
109
110
111def first_element(items: List[T]) -> Optional[T]:
112 """Return first element of list, preserving type."""
113 return items[0] if items else None
114
115
116int_list = [1, 2, 3]
117str_list = ["a", "b", "c"]
118print(f"first_element({int_list}) -> {first_element(int_list)}")
119print(f"first_element({str_list}) -> {first_element(str_list)}")
120
121
122class Stack(Generic[T]):
123 """Generic stack implementation."""
124
125 def __init__(self) -> None:
126 self._items: List[T] = []
127
128 def push(self, item: T) -> None:
129 self._items.append(item)
130
131 def pop(self) -> Optional[T]:
132 return self._items.pop() if self._items else None
133
134 def __repr__(self) -> str:
135 return f"Stack({self._items})"
136
137
138int_stack: Stack[int] = Stack()
139int_stack.push(10)
140int_stack.push(20)
141print(f"int_stack: {int_stack}")
142print(f"Popped: {int_stack.pop()}")
143
144str_stack: Stack[str] = Stack()
145str_stack.push("hello")
146str_stack.push("world")
147print(f"str_stack: {str_stack}")
148
149
150# =============================================================================
151# Protocol (Structural Subtyping)
152# =============================================================================
153
154section("Protocol (Structural Subtyping)")
155
156
157class Drawable(Protocol):
158 """Protocol for drawable objects."""
159
160 def draw(self) -> str:
161 ...
162
163
164class Circle:
165 """Circle - implements Drawable protocol without inheritance."""
166
167 def __init__(self, radius: float):
168 self.radius = radius
169
170 def draw(self) -> str:
171 return f"Drawing circle with radius {self.radius}"
172
173
174class Square:
175 """Square - also implements Drawable protocol."""
176
177 def __init__(self, side: float):
178 self.side = side
179
180 def draw(self) -> str:
181 return f"Drawing square with side {self.side}"
182
183
184def render(obj: Drawable) -> None:
185 """Render any drawable object."""
186 print(obj.draw())
187
188
189circle = Circle(5.0)
190square = Square(10.0)
191render(circle)
192render(square)
193
194
195# =============================================================================
196# Callable Types
197# =============================================================================
198
199section("Callable Types")
200
201
202def apply_operation(x: int, y: int, operation: Callable[[int, int], int]) -> int:
203 """Apply a binary operation to two integers."""
204 return operation(x, y)
205
206
207def add(a: int, b: int) -> int:
208 return a + b
209
210
211def multiply(a: int, b: int) -> int:
212 return a * b
213
214
215result1 = apply_operation(5, 3, add)
216result2 = apply_operation(5, 3, multiply)
217print(f"apply_operation(5, 3, add) -> {result1}")
218print(f"apply_operation(5, 3, multiply) -> {result2}")
219
220
221# =============================================================================
222# Runtime Type Checking
223# =============================================================================
224
225section("Runtime Type Checking with isinstance")
226
227
228def process_value(value: Union[int, str, List[int]]) -> str:
229 """Process different types at runtime."""
230 if isinstance(value, int):
231 return f"Integer: {value * 2}"
232 elif isinstance(value, str):
233 return f"String: {value.upper()}"
234 elif isinstance(value, list):
235 return f"List sum: {sum(value)}"
236 else:
237 return "Unknown type"
238
239
240print(process_value(42))
241print(process_value("hello"))
242print(process_value([1, 2, 3, 4, 5]))
243
244
245# =============================================================================
246# Type Introspection
247# =============================================================================
248
249section("Type Introspection")
250
251
252def example_function(x: int, y: str = "default") -> bool:
253 """Example function for introspection."""
254 return True
255
256
257hints = get_type_hints(example_function)
258print(f"Type hints for example_function: {hints}")
259
260# Introspect Union type
261union_type = Union[int, str]
262print(f"\nUnion[int, str]:")
263print(f" Origin: {get_origin(union_type)}")
264print(f" Args: {get_args(union_type)}")
265
266# Introspect List type
267list_type = List[int]
268print(f"\nList[int]:")
269print(f" Origin: {get_origin(list_type)}")
270print(f" Args: {get_args(list_type)}")
271
272
273# =============================================================================
274# Summary
275# =============================================================================
276
277section("Summary")
278
279print("""
280Type hints provide several benefits:
2811. Better IDE support (autocomplete, refactoring)
2822. Early error detection with static type checkers (mypy, pyright)
2833. Self-documenting code
2844. Runtime type checking with isinstance()
2855. Generic programming with TypeVar and Generic
286
287Note: Type hints are optional and not enforced at runtime
288unless you explicitly check them.
289""")