python
exercises
exercises.py🐍python
"""
Type Hints - Exercises
Complete these exercises to practice type hints.
Run with: pytest 25_type_hints/exercises.py -v
"""
from typing import Any, Callable, TypeVar, Generic, Protocol
from dataclasses import dataclass
# =============================================================================
# EXERCISE 1: Add Type Hints to Functions
# =============================================================================
def calculate_average(numbers):
"""
Add type hints to this function.
Parameters:
- numbers: list of floats
Returns:
- float: the average of the numbers
- None if the list is empty
Example:
calculate_average([1.0, 2.0, 3.0]) # Returns 2.0
calculate_average([]) # Returns None
"""
# YOUR CODE HERE - add type hints to the function signature
if not numbers:
return None
return sum(numbers) / len(numbers)
def merge_dicts(dict1, dict2):
"""
Add type hints to this function.
Parameters:
- dict1: dictionary with string keys and any values
- dict2: dictionary with string keys and any values
Returns:
- merged dictionary
Example:
merge_dicts({"a": 1}, {"b": 2}) # Returns {"a": 1, "b": 2}
"""
# YOUR CODE HERE - add type hints to the function signature
return {**dict1, **dict2}
def filter_by_length(strings, min_length):
"""
Add type hints to this function.
Parameters:
- strings: list of strings
- min_length: minimum length (integer)
Returns:
- list of strings that meet the minimum length
Example:
filter_by_length(["a", "abc", "ab"], 2) # Returns ["abc", "ab"]
"""
# YOUR CODE HERE - add type hints to the function signature
return [s for s in strings if len(s) >= min_length]
def test_basic_type_hints():
"""Test basic type hints work correctly."""
# Test calculate_average
assert calculate_average([1.0, 2.0, 3.0]) == 2.0
assert calculate_average([]) is None
# Test merge_dicts
result = merge_dicts({"a": 1}, {"b": 2})
assert result == {"a": 1, "b": 2}
# Test filter_by_length
result = filter_by_length(["a", "abc", "ab"], 2)
assert result == ["abc", "ab"]
# =============================================================================
# EXERCISE 2: Create Typed Dataclass
# =============================================================================
# YOUR CODE HERE - Create a dataclass called Product with:
# - id: int
# - name: str
# - price: float
# - tags: list of strings (default empty list)
# - in_stock: bool (default True)
#
# Add a method:
# - apply_discount(percent: float) -> float
# Returns the discounted price
@dataclass
class Product:
# YOUR CODE HERE
pass
def test_product_dataclass():
"""Test the Product dataclass."""
# Create product
product = Product(
id=1,
name="Laptop",
price=999.99,
tags=["electronics", "computers"]
)
assert product.id == 1
assert product.name == "Laptop"
assert product.price == 999.99
assert product.tags == ["electronics", "computers"]
assert product.in_stock == True
# Test discount
discounted = product.apply_discount(10)
assert abs(discounted - 899.99) < 0.01
# =============================================================================
# EXERCISE 3: Generic Container Class
# =============================================================================
T = TypeVar('T')
class Container(Generic[T]):
"""
Implement a generic container that stores a single value.
Methods:
- __init__(value: T): Initialize with a value
- get() -> T: Return the stored value
- set(value: T) -> None: Update the value
- map(func: Callable[[T], T]) -> Container[T]: Apply function and return new container
- is_empty() -> bool: Return True if value is None
Example:
container = Container(5)
assert container.get() == 5
doubled = container.map(lambda x: x * 2)
assert doubled.get() == 10
"""
def __init__(self, value: T) -> None:
# YOUR CODE HERE
pass
def get(self) -> T:
# YOUR CODE HERE
pass
def set(self, value: T) -> None:
# YOUR CODE HERE
pass
def map(self, func: Callable[[T], T]) -> 'Container[T]':
# YOUR CODE HERE
pass
def is_empty(self) -> bool:
# YOUR CODE HERE
pass
def test_generic_container():
"""Test the generic Container class."""
# Integer container
int_container: Container[int] = Container(5)
assert int_container.get() == 5
assert int_container.is_empty() == False
doubled = int_container.map(lambda x: x * 2)
assert doubled.get() == 10
# String container
str_container: Container[str] = Container("hello")
assert str_container.get() == "hello"
upper = str_container.map(str.upper)
assert upper.get() == "HELLO"
# Update value
str_container.set("world")
assert str_container.get() == "world"
# =============================================================================
# EXERCISE 4: Protocol for Comparable Objects
# =============================================================================
class Comparable(Protocol):
"""
Define a Protocol for comparable objects.
Requirements:
- __lt__(self, other: 'Comparable') -> bool: Less than comparison
- __eq__(self, other: object) -> bool: Equality comparison
"""
# YOUR CODE HERE
pass
def find_min(items: list[Comparable]) -> Comparable | None:
"""
Find the minimum item in a list of comparable objects.
Returns None if the list is empty.
Example:
find_min([3, 1, 4, 1, 5]) # Returns 1
find_min(["banana", "apple", "cherry"]) # Returns "apple"
"""
# YOUR CODE HERE
pass
def find_max(items: list[Comparable]) -> Comparable | None:
"""
Find the maximum item in a list of comparable objects.
Returns None if the list is empty.
"""
# YOUR CODE HERE
pass
def test_comparable_protocol():
"""Test the Comparable protocol implementation."""
# Works with integers
assert find_min([3, 1, 4, 1, 5]) == 1
assert find_max([3, 1, 4, 1, 5]) == 5
# Works with strings
assert find_min(["banana", "apple", "cherry"]) == "apple"
assert find_max(["banana", "apple", "cherry"]) == "cherry"
# Empty list
assert find_min([]) is None
assert find_max([]) is None
# =============================================================================
# EXERCISE 5: TypedDict for API Response
# =============================================================================
from typing import TypedDict, NotRequired
# YOUR CODE HERE - Create the following TypedDict classes:
# 1. ApiError TypedDict with:
# - code: int (required)
# - message: str (required)
# - details: str (optional, using NotRequired)
# 2. ApiResponse TypedDict with:
# - success: bool (required)
# - data: dict with string keys and any values (optional)
# - error: ApiError (optional)
# - timestamp: str (required)
class ApiError(TypedDict):
# YOUR CODE HERE
pass
class ApiResponse(TypedDict):
# YOUR CODE HERE
pass
def create_success_response(data: dict[str, Any]) -> ApiResponse:
"""
Create a successful API response.
Example:
create_success_response({"user": "Alice"})
# Returns: {"success": True, "data": {"user": "Alice"}, "timestamp": "..."}
"""
# YOUR CODE HERE
pass
def create_error_response(code: int, message: str) -> ApiResponse:
"""
Create an error API response.
Example:
create_error_response(404, "Not found")
# Returns: {"success": False, "error": {"code": 404, "message": "Not found"}, "timestamp": "..."}
"""
# YOUR CODE HERE
pass
def test_typed_dict():
"""Test the TypedDict implementations."""
from datetime import datetime
# Success response
response = create_success_response({"items": [1, 2, 3]})
assert response["success"] == True
assert response["data"] == {"items": [1, 2, 3]}
assert "timestamp" in response
# Error response
error_response = create_error_response(404, "Not found")
assert error_response["success"] == False
assert error_response["error"]["code"] == 404
assert error_response["error"]["message"] == "Not found"
# =============================================================================
# EXERCISE 6: Higher-Order Function Types
# =============================================================================
# Type aliases for clarity
Predicate = Callable[[T], bool]
Transformer = Callable[[T], T]
def compose(f: Callable[[T], T], g: Callable[[T], T]) -> Callable[[T], T]:
"""
Compose two functions: compose(f, g)(x) = f(g(x))
Example:
add_one = lambda x: x + 1
double = lambda x: x * 2
add_then_double = compose(double, add_one)
assert add_then_double(3) == 8 # (3 + 1) * 2 = 8
"""
# YOUR CODE HERE
pass
def pipe(*funcs: Callable[[T], T]) -> Callable[[T], T]:
"""
Pipe multiple functions: pipe(f, g, h)(x) = h(g(f(x)))
Example:
add_one = lambda x: x + 1
double = lambda x: x * 2
square = lambda x: x * x
pipeline = pipe(add_one, double, square)
assert pipeline(3) == 64 # ((3 + 1) * 2)^2 = 64
"""
# YOUR CODE HERE
pass
def retry(times: int, default: T) -> Callable[[Callable[[], T]], Callable[[], T]]:
"""
Decorator factory that retries a function up to 'times' times.
Returns 'default' if all attempts fail.
Example:
counter = {"attempts": 0}
@retry(times=3, default=-1)
def flaky_function():
counter["attempts"] += 1
if counter["attempts"] < 3:
raise ValueError("Not yet!")
return 42
result = flaky_function()
assert result == 42
assert counter["attempts"] == 3
"""
# YOUR CODE HERE
pass
def test_higher_order_functions():
"""Test higher-order function implementations."""
# Test compose
add_one = lambda x: x + 1
double = lambda x: x * 2
add_then_double = compose(double, add_one)
assert add_then_double(3) == 8
# Test pipe
square = lambda x: x * x
pipeline = pipe(add_one, double, square)
assert pipeline(3) == 64
# Test retry
counter = {"attempts": 0}
@retry(times=3, default=-1)
def flaky_function():
counter["attempts"] += 1
if counter["attempts"] < 3:
raise ValueError("Not yet!")
return 42
result = flaky_function()
assert result == 42
assert counter["attempts"] == 3
# =============================================================================
# EXERCISE 7: Generic Cache with Type Safety
# =============================================================================
K = TypeVar('K')
V = TypeVar('V')
class Cache(Generic[K, V]):
"""
Implement a generic cache with type-safe get and set operations.
Methods:
- __init__(max_size: int = 100): Initialize with max capacity
- get(key: K) -> V | None: Get value by key
- set(key: K, value: V) -> None: Set value for key
- has(key: K) -> bool: Check if key exists
- delete(key: K) -> bool: Delete key, return True if existed
- clear() -> None: Clear all entries
- size() -> int: Return current number of entries
- keys() -> list[K]: Return all keys
- values() -> list[V]: Return all values
When cache exceeds max_size, remove oldest entry (FIFO).
Example:
cache: Cache[str, int] = Cache(max_size=2)
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3) # "a" is evicted
assert cache.get("a") is None
assert cache.get("b") == 2
"""
def __init__(self, max_size: int = 100) -> None:
# YOUR CODE HERE
pass
def get(self, key: K) -> V | None:
# YOUR CODE HERE
pass
def set(self, key: K, value: V) -> None:
# YOUR CODE HERE
pass
def has(self, key: K) -> bool:
# YOUR CODE HERE
pass
def delete(self, key: K) -> bool:
# YOUR CODE HERE
pass
def clear(self) -> None:
# YOUR CODE HERE
pass
def size(self) -> int:
# YOUR CODE HERE
pass
def keys(self) -> list[K]:
# YOUR CODE HERE
pass
def values(self) -> list[V]:
# YOUR CODE HERE
pass
def test_generic_cache():
"""Test the generic Cache implementation."""
cache: Cache[str, int] = Cache(max_size=3)
# Basic operations
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
assert cache.get("a") == 1
assert cache.get("b") == 2
assert cache.size() == 3
# FIFO eviction
cache.set("d", 4) # "a" should be evicted
assert cache.get("a") is None
assert cache.get("d") == 4
assert cache.size() == 3
# has and delete
assert cache.has("b") == True
assert cache.delete("b") == True
assert cache.has("b") == False
assert cache.delete("nonexistent") == False
# keys and values
assert set(cache.keys()) == {"c", "d"}
assert set(cache.values()) == {3, 4}
# clear
cache.clear()
assert cache.size() == 0
# =============================================================================
# RUN TESTS
# =============================================================================
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])