python

exercises

exercises.py🐍
"""
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"])
Exercises - Python Tutorial | DeepML