python
examples
examples.py🐍python
"""
Type Hints and Static Typing in Python - Examples
Type hints add optional static typing to Python.
They improve readability, enable better tooling, and catch bugs early.
"""
from typing import (
Optional, Union, Any,
Callable, TypeVar, Generic,
Literal, TypedDict, Protocol,
overload, Final, ClassVar
)
from collections.abc import Iterable, Iterator, Sequence
from dataclasses import dataclass
import abc
# =============================================================================
# BASIC TYPE HINTS
# =============================================================================
# Variables
name: str = "Alice"
age: int = 30
height: float = 5.8
is_active: bool = True
# None type
nothing: None = None
# Multiple types using Union (Python 3.10+ use |)
user_id: int | str = "user123"
maybe_value: int | None = None # Same as Optional[int]
# Functions with type hints
def greet(name: str) -> str:
"""Function with type hints."""
return f"Hello, {name}!"
def add(a: int, b: int) -> int:
"""Simple typed function."""
return a + b
def process_data(data: str, count: int = 1) -> list[str]:
"""Function with default parameter."""
return [data] * count
# Functions returning None
def log_message(message: str) -> None:
"""Functions that don't return a value."""
print(message)
# =============================================================================
# COLLECTION TYPES
# =============================================================================
# Lists
numbers: list[int] = [1, 2, 3, 4, 5]
names: list[str] = ["Alice", "Bob", "Charlie"]
# Tuples - fixed length and types
point: tuple[int, int] = (10, 20)
record: tuple[str, int, float] = ("Alice", 30, 5.8)
variable_tuple: tuple[int, ...] = (1, 2, 3, 4, 5) # Variable length
# Dictionaries
scores: dict[str, int] = {"Alice": 100, "Bob": 85}
config: dict[str, str | int | bool] = {
"debug": True,
"port": 8080,
"host": "localhost"
}
# Sets
unique_ids: set[int] = {1, 2, 3}
tags: frozenset[str] = frozenset(["python", "typing"])
# Nested collections
matrix: list[list[int]] = [[1, 2], [3, 4], [5, 6]]
users_data: dict[str, dict[str, Any]] = {
"alice": {"age": 30, "email": "alice@example.com"}
}
# =============================================================================
# OPTIONAL AND NONE HANDLING
# =============================================================================
def find_user(user_id: int) -> str | None:
"""Return user name or None if not found."""
users = {1: "Alice", 2: "Bob"}
return users.get(user_id)
def process_user(user_id: int) -> str:
"""Example of handling Optional return values."""
user = find_user(user_id)
if user is None:
return "Unknown"
# After the check, type checker knows user is str
return user.upper()
def greet_optional(name: str | None = None) -> str:
"""Optional parameter with None default."""
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"
# =============================================================================
# CALLABLE TYPES
# =============================================================================
# Function types
Processor = Callable[[str], str]
Validator = Callable[[int], bool]
NoArgsFunc = Callable[[], None]
AnyFunc = Callable[..., Any] # Any arguments
def apply_processor(text: str, processor: Processor) -> str:
"""Function taking another function as parameter."""
return processor(text)
def create_multiplier(factor: int) -> Callable[[int], int]:
"""Function returning a function."""
def multiplier(n: int) -> int:
return n * factor
return multiplier
# Example usage
double = create_multiplier(2)
result = apply_processor("hello", str.upper)
# =============================================================================
# TYPE VARIABLES (GENERICS)
# =============================================================================
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')
def first_element(items: list[T]) -> T | None:
"""Generic function that works with any list type."""
return items[0] if items else None
def reverse_dict(d: dict[K, V]) -> dict[V, K]:
"""Generic function with multiple type variables."""
return {v: k for k, v in d.items()}
# Bounded type variables
Number = TypeVar('Number', int, float)
def add_numbers(a: Number, b: Number) -> Number:
"""Type variable bounded to numeric types."""
return a + b
# =============================================================================
# GENERIC CLASSES
# =============================================================================
class Stack(Generic[T]):
"""A generic stack implementation."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T | None:
return self._items[-1] if self._items else 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")
class Pair(Generic[K, V]):
"""Generic class with two type parameters."""
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
def swap(self) -> 'Pair[V, K]':
return Pair(self.value, self.key)
# =============================================================================
# LITERAL TYPES
# =============================================================================
Direction = Literal["north", "south", "east", "west"]
HttpMethod = Literal["GET", "POST", "PUT", "DELETE"]
def move(direction: Direction) -> str:
"""Only accepts specific literal values."""
return f"Moving {direction}"
def make_request(method: HttpMethod, url: str) -> dict:
"""Only accepts valid HTTP methods."""
return {"method": method, "url": url}
# Usage
move("north") # OK
# move("up") # Type error!
# =============================================================================
# TYPED DICTIONARIES
# =============================================================================
class UserDict(TypedDict):
"""Typed dictionary with specific keys and types."""
name: str
age: int
email: str
class PartialUserDict(TypedDict, total=False):
"""All fields are optional."""
name: str
age: int
email: str
class MixedUserDict(TypedDict):
"""Mix of required and optional fields."""
name: str # Required
age: int # Required
email: str | None # Can be None
def process_user_data(user: UserDict) -> str:
"""Function accepting typed dictionary."""
return f"{user['name']} ({user['age']})"
# Usage
user: UserDict = {"name": "Alice", "age": 30, "email": "alice@example.com"}
# =============================================================================
# PROTOCOLS (STRUCTURAL SUBTYPING)
# =============================================================================
class Drawable(Protocol):
"""Protocol defining drawable interface."""
def draw(self) -> str:
...
class Resizable(Protocol):
"""Protocol for resizable objects."""
def resize(self, factor: float) -> None:
...
class Circle:
"""Class that implements Drawable without explicit inheritance."""
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> str:
return f"Circle with radius {self.radius}"
class Square:
"""Another drawable class."""
def __init__(self, side: float):
self.side = side
def draw(self) -> str:
return f"Square with side {self.side}"
def resize(self, factor: float) -> None:
self.side *= factor
def render(shape: Drawable) -> None:
"""Works with any class that has draw() method."""
print(shape.draw())
# Works because Circle has draw() method
render(Circle(5))
render(Square(10))
# =============================================================================
# OVERLOADED FUNCTIONS
# =============================================================================
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
@overload
def process(value: list[int]) -> list[int]: ...
def process(value: int | str | list[int]) -> int | str | list[int]:
"""
Overloaded function with different signatures.
Type checker knows the return type based on input type.
"""
if isinstance(value, int):
return value * 2
elif isinstance(value, str):
return value.upper()
else:
return [x * 2 for x in value]
# Type checker knows these types:
int_result: int = process(5) # Returns int
str_result: str = process("hello") # Returns str
list_result: list[int] = process([1, 2, 3]) # Returns list[int]
# =============================================================================
# FINAL AND CLASS VARIABLES
# =============================================================================
# Constants that shouldn't be reassigned
MAX_SIZE: Final[int] = 100
API_URL: Final = "https://api.example.com"
class Config:
"""Class with class variables and instance variables."""
# Class variable (shared by all instances)
version: ClassVar[str] = "1.0.0"
# Instance variable
name: str
def __init__(self, name: str) -> None:
self.name = name
# =============================================================================
# TYPE ALIASES
# =============================================================================
# Simple type aliases
UserId = int
Email = str
JsonDict = dict[str, Any]
# Complex type aliases
Handler = Callable[[str, int], dict[str, Any]]
Matrix = list[list[float]]
Tree = dict[str, 'Tree | int'] # Recursive type alias
def send_email(to: Email, subject: str) -> bool:
"""Using type alias for clarity."""
return True
def get_user(user_id: UserId) -> dict[str, Any]:
"""Type alias makes code more readable."""
return {"id": user_id, "name": "User"}
# =============================================================================
# DATACLASSES WITH TYPE HINTS
# =============================================================================
@dataclass
class Point:
"""Dataclass with full type hints."""
x: float
y: float
label: str = ""
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
@dataclass
class Rectangle:
"""Another typed dataclass."""
width: float
height: float
def area(self) -> float:
return self.width * self.height
# =============================================================================
# ABSTRACT BASE CLASSES WITH TYPE HINTS
# =============================================================================
class Repository(abc.ABC, Generic[T]):
"""Abstract generic repository."""
@abc.abstractmethod
def get(self, id: int) -> T | None:
"""Get item by ID."""
pass
@abc.abstractmethod
def save(self, item: T) -> int:
"""Save item and return ID."""
pass
@abc.abstractmethod
def delete(self, id: int) -> bool:
"""Delete item by ID."""
pass
@dataclass
class User:
id: int
name: str
email: str
class UserRepository(Repository[User]):
"""Concrete implementation of generic repository."""
def __init__(self) -> None:
self._users: dict[int, User] = {}
self._next_id = 1
def get(self, id: int) -> User | None:
return self._users.get(id)
def save(self, item: User) -> int:
if item.id == 0:
item.id = self._next_id
self._next_id += 1
self._users[item.id] = item
return item.id
def delete(self, id: int) -> bool:
if id in self._users:
del self._users[id]
return True
return False
# =============================================================================
# TYPE GUARDS
# =============================================================================
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
"""Type guard to narrow list type."""
return all(isinstance(x, str) for x in val)
def process_items(items: list[object]) -> None:
"""Example using type guard."""
if is_string_list(items):
# Type checker knows items is list[str] here
for item in items:
print(item.upper())
else:
print("Not a string list")
# =============================================================================
# SELF TYPE (Python 3.11+)
# =============================================================================
from typing import Self
class Builder:
"""Class using Self type for method chaining."""
def __init__(self) -> None:
self._value: int = 0
def add(self, n: int) -> Self:
self._value += n
return self
def multiply(self, n: int) -> Self:
self._value *= n
return self
def result(self) -> int:
return self._value
class ExtendedBuilder(Builder):
"""Subclass inherits correct Self type."""
def subtract(self, n: int) -> Self:
self._value -= n
return self
# Method chaining works correctly with types
result = Builder().add(5).multiply(2).result()
extended_result = ExtendedBuilder().add(10).subtract(3).multiply(2).result()
# =============================================================================
# DEMONSTRATIONS
# =============================================================================
if __name__ == "__main__":
print("=" * 60)
print("BASIC TYPE HINTS")
print("=" * 60)
print(f"greet('World'): {greet('World')}")
print(f"add(5, 3): {add(5, 3)}")
print("\n" + "=" * 60)
print("GENERIC STACK")
print("=" * 60)
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(f"Stack peek: {stack.peek()}")
print(f"Stack pop: {stack.pop()}")
print("\n" + "=" * 60)
print("TYPED DICTIONARY")
print("=" * 60)
user: UserDict = {"name": "Alice", "age": 30, "email": "alice@example.com"}
print(f"Process user: {process_user_data(user)}")
print("\n" + "=" * 60)
print("PROTOCOLS (DUCK TYPING)")
print("=" * 60)
shapes: list[Drawable] = [Circle(5), Square(10)]
for shape in shapes:
render(shape)
print("\n" + "=" * 60)
print("OVERLOADED FUNCTIONS")
print("=" * 60)
print(f"process(5): {process(5)}")
print(f"process('hello'): {process('hello')}")
print(f"process([1,2,3]): {process([1, 2, 3])}")
print("\n" + "=" * 60)
print("GENERIC REPOSITORY")
print("=" * 60)
repo = UserRepository()
user_obj = User(0, "Bob", "bob@example.com")
id = repo.save(user_obj)
print(f"Saved user with ID: {id}")
print(f"Retrieved user: {repo.get(id)}")