Skip to main content

Python ParamSpec and Concatenate Practice Problems & Exercises

Practice: ParamSpec and Concatenate

11 problems3 Easy4 Medium4 Hard70–90 min
← Back to lesson

Easy

#1Type-Safe Timing Decorator with ParamSpecEasy
ParamSpecdecoratorTypeVartiming

Write a timed decorator using ParamSpec so that the wrapped function retains its exact parameter signature in type checkers.

Python
import time
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


def timed(func: Callable[P, T]) -> Callable[P, T]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}({', '.join(str(a) for a in args)}) took {elapsed:.3f}s")
        return result
    return wrapper


@timed
def slow_add(a: int, b: int) -> int:
    time.sleep(0.001)
    return a + b


result = slow_add(2, 3)
print(f"Result: {result}")
Expected Output
slow_add(2, 3) took 0.XXXs
Result: 5
Hints

Hint 1: Declare P = ParamSpec("P") and T = TypeVar("T"). The wrapper signature is (*args: P.args, **kwargs: P.kwargs).

Hint 2: The return type of the decorator is Callable[P, T] -> Callable[P, T]. This preserves the original function signature.


#2Retry Decorator with Preserved SignatureEasy
ParamSpecretrydecoratorTypeVar

Write a retry(max_attempts) parametrized decorator using ParamSpec that retries a function on exception, preserving the original type signature.

Python
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


def retry(max_attempts: int) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            last_exc: Exception = RuntimeError("no attempts")
            for attempt in range(1, max_attempts + 1):
                try:
                    result = func(*args, **kwargs)
                    print(f"Attempt {attempt} succeeded: {result}")
                    return result
                except Exception as e:
                    last_exc = e
                    print(f"Attempt {attempt} failed: {e}")
            raise last_exc
        return wrapper
    return decorator


call_count = 0

@retry(max_attempts=3)
def flaky_operation() -> int:
    global call_count
    call_count += 1
    if call_count < 3:
        raise RuntimeError("temporary error")
    return 42


flaky_operation()
Expected Output
Attempt 1 failed: temporary error
Attempt 2 failed: temporary error
Attempt 3 succeeded: 42
Hints

Hint 1: Use P = ParamSpec("P") and T = TypeVar("T") to build a retry wrapper that preserves the wrapped function signature.

Hint 2: The retry loop calls func(*args, **kwargs) inside a try/except. After max_attempts, re-raise the last exception.


#3Logging Decorator with ParamSpecEasy
ParamSpecloggingdecorator

Create a logged decorator with ParamSpec that logs function name, arguments, return value, and any exception raised.

Python
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


def logged(func: Callable[P, T]) -> Callable[P, T]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"[LOG] {func.__name__} called with args={args}, kwargs={kwargs}")
        try:
            result = func(*args, **kwargs)
            print(f"[LOG] {func.__name__} returned {result}")
            return result
        except Exception as e:
            print(f"[LOG] {func.__name__} raised {type(e).__name__}: {e}")
            raise
    return wrapper


@logged
def divide(a: float, b: float) -> float:
    return a / b


result = divide(10, 2)
print(f"Result: {result}")

try:
    divide(10, 0)
except ZeroDivisionError:
    pass
Expected Output
[LOG] divide called with args=(10, 2), kwargs={}
[LOG] divide returned 5.0
Result: 5.0
[LOG] divide called with args=(10, 0), kwargs={}
[LOG] divide raised ZeroDivisionError: division by zero
Hints

Hint 1: Wrap the function call in try/except. Log both successful returns and exceptions.

Hint 2: Use P.args and P.kwargs in the wrapper signature to preserve the parameter spec.


Medium

#4Inject Extra Argument with ConcatenateMedium
ConcatenateParamSpecinjectdecorator

Use Concatenate to write an authenticated decorator that injects a user string as the first argument to the wrapped function, prepending it to the original parameters.

Python
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec, Concatenate
except ImportError:
    from typing_extensions import ParamSpec, Concatenate

P = ParamSpec("P")
T = TypeVar("T")


class Request:
    def __init__(self, user: str, is_auth: bool) -> None:
        self.user = user
        self.is_auth = is_auth


def authenticated(
    func: Callable[Concatenate[str, P], T]
) -> Callable[Concatenate[Request, P], T]:
    @functools.wraps(func)
    def wrapper(request: Request, *args: P.args, **kwargs: P.kwargs) -> T:
        if not request.is_auth:
            raise PermissionError("Unauthenticated access blocked")
        return func(request.user, *args, **kwargs)
    return wrapper


@authenticated
def greet(user: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {user}!"


req_ok = Request("admin", is_auth=True)
req_bad = Request("guest", is_auth=False)

print(f"authenticated: {req_ok.is_auth}")
print(f"user: {req_ok.user}")
result = greet(req_ok, greeting="Hello")
print(f"greet result: {result}")

try:
    greet(req_bad)
except PermissionError as e:
    print(e)
Expected Output
authenticated: True
user: admin
greet result: Hello, admin!
Unauthenticated access blocked
Hints

Hint 1: Concatenate[ExtraType, P] prepends an extra positional argument to the parameter spec P.

Hint 2: The outer function signature is Callable[Concatenate[RequestType, P], T] and the wrapper adds the request argument before forwarding.


#5Memoize with ParamSpecMedium
ParamSpecmemoizecachedecorator

Write a memoize decorator using ParamSpec that caches return values. Track cache hits to prove the cache is being used.

Python
import functools
from typing import TypeVar, Callable, Dict, Tuple, Any
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


def memoize(func: Callable[P, T]) -> Callable[P, T]:
    cache: Dict[Tuple, T] = {}
    hits = [0]
    misses = [0]

    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        key = args + tuple(sorted(kwargs.items()))
        if key in cache:
            hits[0] += 1
            return cache[key]
        misses[0] += 1
        result = func(*args, **kwargs)
        cache[key] = result
        return result

    wrapper.cache_hits = hits      # type: ignore[attr-defined]
    wrapper.cache_misses = misses  # type: ignore[attr-defined]
    return wrapper


@memoize
def fib(n: int) -> int:
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)


r1 = fib(10)
print(f"fib(10) = {r1}")
r2 = fib(10)
print(f"fib(10) = {r2} (cached)")
print(f"Cache hits: {fib.cache_hits[0]}")      # type: ignore[attr-defined]
print(f"Unique calls: {fib.cache_misses[0]}")   # type: ignore[attr-defined]
Expected Output
fib(10) = 55
fib(10) = 55 (cached)
Cache hits: 1
Unique calls: 11
Hints

Hint 1: The memoize decorator uses a dict keyed by (args, frozenset(kwargs.items())). P preserves the original signature.

Hint 2: Track cache hits and misses separately so you can verify caching is working.


#6Rate-Limiter Decorator via ParamSpecMedium
ParamSpecrate-limiterdecoratortiming

Build a rate_limit(calls, period) decorator using ParamSpec. It should raise RateLimitError if the function is called more than calls times in the given period seconds.

Python
import time
import functools
from collections import deque
from typing import TypeVar, Callable
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


class RateLimitError(Exception):
    pass


def rate_limit(calls: int, period: float) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        timestamps: deque = deque()

        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            now = time.monotonic()
            # Remove timestamps outside the window
            while timestamps and now - timestamps[0] > period:
                timestamps.popleft()
            if len(timestamps) >= calls:
                raise RateLimitError(
                    f"max {calls} calls per {period}s window"
                )
            timestamps.append(now)
            return func(*args, **kwargs)

        return wrapper
    return decorator


successful = 0

@rate_limit(calls=3, period=1.0)
def api_call(endpoint: str) -> str:
    return f"ok"


for i in range(1, 5):
    try:
        result = api_call("/data")
        successful += 1
        print(f"Call {i}: {result}")
    except RateLimitError as e:
        print(f"Call {i}: RateLimitError: {e}")

print(f"Total successful: {successful}")
Expected Output
Call 1: ok
Call 2: ok
Call 3: ok
Call 4: RateLimitError: max 3 calls per 1.0s window
Total successful: 3
Hints

Hint 1: Store the call timestamps in a deque. Before each call, purge entries older than the window. If len(deque) >= limit, raise RateLimitError.

Hint 2: Use P and T from ParamSpec and TypeVar to preserve the wrapped function signature.


#7Decorator Factory with Configurable Behavior via ParamSpecMedium
ParamSpecdecorator-factoryconfigurableTypeVar

Create a configurable debug_log(log_args, log_result) decorator factory using ParamSpec that optionally logs call arguments and the return value.

Python
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


def debug_log(
    log_args: bool = True,
    log_result: bool = True,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            print(f"[DEBUG] {func.__name__} called")
            if log_args:
                print(f"[DEBUG] {func.__name__} args: {args} kwargs: {kwargs}")
            result = func(*args, **kwargs)
            if log_result:
                print(f"[DEBUG] {func.__name__} returned: {result}")
            return result
        return wrapper
    return decorator


@debug_log(log_args=True, log_result=True)
def process(data: str) -> str:
    return f"PROCESSED: {data.upper()}"


result = process("data")
print(f"Result: {result}")
Expected Output
[DEBUG] process called
[DEBUG] process args: ('data',) kwargs: {}
[DEBUG] process returned: PROCESSED: DATA
Result: PROCESSED: DATA
Hints

Hint 1: The outer factory returns a decorator. The decorator uses P and T to preserve types. Configuration (log_args, log_result) is captured by the factory closure.

Hint 2: Conditionally print args and return value based on the flags.


Hard

#8Middleware Pipeline with ParamSpecHard
ParamSpecmiddlewarepipelineConcatenatechain

Build a middleware pipeline where each middleware is a decorator using ParamSpec. Chain them to process a "request" string through auth, logging, and validation layers.

Python
import functools
from typing import TypeVar, Callable, List
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")

MiddlewareType = Callable[[Callable[P, T]], Callable[P, T]]


def auth_middleware(token: str) -> MiddlewareType:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            print(f"[auth] checking token: {token}")
            return func(*args, **kwargs)
        return wrapper
    return decorator


def logging_middleware(func: Callable[P, T]) -> Callable[P, T]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"[log] calling {func.__name__} with {args}")
        return func(*args, **kwargs)
    return wrapper


def validation_middleware(min_len: int) -> MiddlewareType:
    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            # Validate first positional argument if it's a string
            if args and isinstance(args[0], str):
                if len(args[0]) < min_len:
                    raise ValueError(f"Input too short: min {min_len} chars")
                print(f"[validate] input length OK: {len(args[0])}")
            return func(*args, **kwargs)
        return wrapper
    return decorator


def handler(request: str) -> str:
    return f"PROCESSED: {request.upper()}"


# Chain middleware: validation -> logging -> auth -> handler
pipeline = (
    auth_middleware("secret")(
        logging_middleware(
            validation_middleware(5)(
                handler
            )
        )
    )
)

result = pipeline("request data")
print(f"Final: {result}")
Expected Output
[auth] checking token: secret
[log] calling handler with ('request data',)
[validate] input length OK: 12
Handler result: PROCESSED: REQUEST DATA
Final: PROCESSED: REQUEST DATA
Hints

Hint 1: Each middleware wraps a handler of type Callable[P, T]. It intercepts the call, does its work, and calls the inner handler.

Hint 2: Use functools.wraps to preserve names. Chain middleware by nesting wrappers: apply(m3, apply(m2, apply(m1, handler))).


#9Type-Safe Partial Application with ConcatenateHard
ParamSpecConcatenatepartialcurrying

Implement a type-safe partial_apply using Concatenate that binds the first argument of a function and returns a new callable with the remaining parameters.

Python
import functools
from typing import TypeVar, Callable
try:
    from typing import ParamSpec, Concatenate
except ImportError:
    from typing_extensions import ParamSpec, Concatenate

P = ParamSpec("P")
T = TypeVar("T")
A = TypeVar("A")


def partial_apply(
    func: Callable[Concatenate[A, P], T],
    first_arg: A,
) -> Callable[P, T]:
    @functools.wraps(func)
    def partial(*args: P.args, **kwargs: P.kwargs) -> T:
        return func(first_arg, *args, **kwargs)
    return partial


def add(a: int, b: int) -> int:
    return a + b


def greet(greeting: str, name: str) -> str:
    return f"{greeting}, {name}!"


add_5 = partial_apply(add, 5)
greet_hello = partial_apply(greet, "Hello")

print(f"add_5(10) = {add_5(10)}")
print(f"add_5(20) = {add_5(20)}")
print(f"greet_hello('Alice') = {greet_hello('Alice')}")
Expected Output
add_5(10) = 15
add_5(20) = 25
greet_hello('Alice') = Hello, Alice!
Hints

Hint 1: partial_apply(func, first_arg) returns a Callable[P, T] where P is the original parameters minus the first argument.

Hint 2: Use Concatenate[FirstArg, P] to describe the original function signature and P for the returned partial.


#10Transaction Decorator with Context InjectionHard
ParamSpecConcatenatetransactioncontext-injection

Use Concatenate to build a transactional decorator that injects a Transaction object as the first argument to the decorated function, handling commit/rollback automatically.

Python
import functools
import uuid
from typing import TypeVar, Callable
try:
    from typing import ParamSpec, Concatenate
except ImportError:
    from typing_extensions import ParamSpec, Concatenate

P = ParamSpec("P")
T = TypeVar("T")


class Transaction:
    def __init__(self) -> None:
        self.id = f"tx-{uuid.uuid4().hex[:3]}"

    def begin(self) -> None:
        print(f"[TXN] begin transaction {self.id}")

    def commit(self) -> None:
        print(f"[TXN] commit transaction {self.id}")

    def rollback(self, reason: str) -> None:
        print(f"[TXN] rollback transaction {self.id}: {reason}")


def transactional(
    func: Callable[Concatenate[Transaction, P], T]
) -> Callable[P, T]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        txn = Transaction()
        txn.begin()
        try:
            result = func(txn, *args, **kwargs)
            txn.commit()
            return result
        except Exception as e:
            txn.rollback(str(e))
            raise
    return wrapper


@transactional
def transfer(txn: Transaction, amount: float, from_acc: str, to_acc: str) -> str:
    if amount > 50 and from_acc == "Bob":
        raise ValueError("Insufficient funds")
    return f"Transfer of {amount} from {from_acc} to {to_acc} completed"


result = transfer(100, "Alice", "Bob")
print(f"transfer result: {result}")

try:
    transfer(100, "Bob", "Alice")
except ValueError:
    pass
Expected Output
[TXN] begin transaction tx-001
transfer result: Transfer of 100 from Alice to Bob completed
[TXN] commit transaction tx-001
[TXN] begin transaction tx-002
[TXN] rollback transaction tx-002: Insufficient funds
Hints

Hint 1: Concatenate[Transaction, P] means the wrapped function receives a Transaction as its first argument (injected by the decorator).

Hint 2: The decorator creates a Transaction, calls begin(), invokes the function, and calls commit() on success or rollback() on exception.


#11Type-Safe Event System with ParamSpecHard
ParamSpecevent-systemcallbacktype-safety

Build a typed event bus where handlers for a specific event all share the same ParamSpec. Emitting the event calls all handlers with the correct arguments.

Python
import functools
from typing import TypeVar, Callable, List, Any
try:
    from typing import ParamSpec
except ImportError:
    from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")


class EventBus:
    def __init__(self) -> None:
        self._handlers: List[Callable[..., None]] = []
        self._event_count = 0

    def on(self, func: Callable[P, None]) -> Callable[P, None]:
        self._handlers.append(func)
        return func

    def emit(self, *args: Any, **kwargs: Any) -> None:
        self._event_count += 1
        for handler in self._handlers:
            handler(*args, **kwargs)

    @property
    def event_count(self) -> int:
        return self._event_count


login_bus = EventBus()


@login_bus.on
def audit_handler(user: str, ip: str) -> None:
    print(f"Handler 1: user_login event, user={user}, ip={ip}")


@login_bus.on
def metrics_handler(user: str, ip: str) -> None:
    print(f"Handler 2: user_login event, user={user}")


login_bus.emit(user="alice", ip="192.168.1.1")
login_bus.emit(user="alice", ip="192.168.1.1")
login_bus.emit(user="bob", ip="10.0.0.1")

print(f"Total events processed: {login_bus.event_count}")
Expected Output
Handler 1: user_login event, user=alice, ip=192.168.1.1
Handler 2: user_login event, user=alice
Handler 1: user_login event, user=bob, ip=10.0.0.1
Total events processed: 3
Hints

Hint 1: Use P = ParamSpec("P") to define the handler signature. EventBus[P] stores handlers typed as Callable[P, None].

Hint 2: emit(**kwargs) calls each handler with the kwargs. Type safety ensures all handlers accept the same parameter spec.

© 2026 EngineersOfAI. All rights reserved.