Python Concatenate: Change Parameters

Published on: August 28, 2026
Reading time: 4 minutes
Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.

ParamSpec preserves a callable’s complete signature, but some decorators intentionally change the visible parameters. They may inject a request context, hide a database connection, add a lock, or adapt a handler for a framework. typing.Concatenate describes a transformation at the beginning of a callable’s parameter list.

This guide shows how to combine Concatenate with ParamSpec and TypeVar, build dependency-injection, authentication, and synchronization decorators, work with async wrappers and callbacks, and understand the feature’s limitations.

The context-injection problem

from collections.abc import Callable

class Context:
    user: str

def inject_context(function: Callable):
    def wrapper(*args, **kwargs):
        context = Context()
        context.user = "system"
        return function(context, *args, **kwargs)
    return wrapper

The original function requires Context first, while the decorated function should hide that argument. Without precise typing, autocomplete and argument validation are lost.

Concatenate with ParamSpec

from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def inject_context(
    function: Callable[Concatenate[Context, P], R],
) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        context = Context()
        context.user = "system"
        return function(context, *args, **kwargs)
    return wrapper

Concatenate[Context, P] means that the original callable receives Context followed by every parameter captured in P. The public wrapper exposes only P.

Why Concatenate appears inside Callable

Concatenate is designed for callable parameter lists. It normally appears as the first argument of Callable and ends with a ParamSpec.

Callable[Concatenate[Context, P], R]

Explicit types come before P. Concatenate does not insert parameters in the middle or at the end of a captured signature.

Injecting a connection

class Connection:
    def execute(self, sql: str) -> list[tuple[object, ...]]:
        ...

def with_connection(
    function: Callable[Concatenate[Connection, P], R],
) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        connection = open_connection()
        try:
            return function(connection, *args, **kwargs)
        finally:
            connection.close()
    return wrapper

The caller does not supply the connection. The decorator owns its creation and cleanup, while the business function receives an explicitly typed dependency.

Preserving runtime metadata

from functools import wraps

@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
    ...

wraps preserves name, documentation, and the reference to the original function at runtime. Concatenate, ParamSpec, and TypeVar preserve the static relationship. Use both.

Injecting a lock

from threading import Lock


def with_lock(
    function: Callable[Concatenate[Lock, P], R],
) -> Callable[P, R]:
    lock = Lock()

    @wraps(function)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        with lock:
            return function(lock, *args, **kwargs)
    return wrapper

The lock is shared across calls to the decorated function. Document details such as granularity and reentrancy because the type annotation cannot express them.

Injecting the authenticated user

class User:
    id: int
    name: str

def require_user(
    function: Callable[Concatenate[User, P], R],
) -> Callable[P, R]:
    @wraps(function)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        user = current_user()
        if user is None:
            raise PermissionError("authentication required")
        return function(user, *args, **kwargs)
    return wrapper

The business function explicitly declares that it needs a User, while the public entry point obtains that dependency from the environment.

Injecting multiple parameters

def inject_services(
    function: Callable[
        Concatenate[User, Connection, P],
        R,
    ],
) -> Callable[P, R]:
    @wraps(function)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        user = get_user()
        connection = open_connection()
        try:
            return function(user, connection, *args, **kwargs)
        finally:
            connection.close()
    return wrapper

Several explicit types may appear before P. Their order must exactly match the actual call.

The inverse transformation

A wrapper can also add a parameter for callers while hiding it from the inner function. The received callable uses P, and the returned callable uses Concatenate.

def require_token(
    function: Callable[P, R],
) -> Callable[Concatenate[str, P], R]:
    @wraps(function)
    def wrapper(
        token: str,
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        validate_token(token)
        return function(*args, **kwargs)
    return wrapper

The caller now supplies a token before the original parameters.

Asynchronous wrappers

from collections.abc import Awaitable

class AsyncContext:
    trace_id: str

def with_trace(
    function: Callable[
        Concatenate[AsyncContext, P],
        Awaitable[R],
    ],
) -> Callable[P, Awaitable[R]]:
    @wraps(function)
    async def wrapper(
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        context = AsyncContext()
        context.trace_id = create_trace_id()
        return await function(context, *args, **kwargs)
    return wrapper

The parameter relationship matches the synchronous version, while the result remains awaitable.

Concatenate in callback APIs

def register_handler(
    handler: Callable[Concatenate[Event, P], None],
    *args: P.args,
    **kwargs: P.kwargs,
) -> None:
    event = wait_for_event()
    handler(event, *args, **kwargs)

The registration function owns the Event, and consumers provide the remaining arguments compatible with P.

Methods and self

For methods, self or cls already belongs to the captured signature. A decorator that only forwards arguments normally needs ParamSpec alone. Concatenate becomes relevant when the beginning of the visible signature really changes.

Method descriptors bind the receiver automatically, so dependency injection after self can be awkward to describe with a generic prefix. A method-specific Protocol or explicit overload may be clearer. Always test the binding behavior with the checker used by the project.

Limitation: prefixes only

Concatenate adds types before P. It cannot insert a parameter after the first captured argument or express a new keyword-only parameter at the end. More complex transformations may require explicit overloads, dedicated Protocols, or checker plugins.

Named parameters

The explicit types added by Concatenate behave as leading positional parameters in the callable relationship. Names, defaults, and keyword-only details may not be represented as desired. If public names matter, define a Protocol with an explicit __call__ signature.

Concatenate versus overload

Concatenate models a generic signature transformation. Overload models a finite set of alternative call shapes. Use Concatenate when any P is preserved after adding or removing a prefix. Use overload when a small number of concrete signatures have different results.

Concatenate versus Protocol

Protocol is more detailed for callables with specific parameter names, extra attributes, or methods. Concatenate is compact for generic decorators. The two features can also work together.

Concatenate versus functools.partial

functools.partial binds arguments at runtime. Concatenate describes the static relationship between the input and output callables. A typed helper that builds partials can use ParamSpec and Concatenate, though complex keyword binding may vary between checkers.

Changing the return type

def with_optional_context(
    function: Callable[Concatenate[Context, P], R],
) -> Callable[P, R | None]:
    @wraps(function)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
        context = try_context()
        if context is None:
            return None
        return function(context, *args, **kwargs)
    return wrapper

Concatenate handles the parameters; the return annotation must expose any result transformation.

Common mistakes

  • Using Concatenate without a final ParamSpec: the construction is incomplete.
  • Trying to insert in the middle: Concatenate represents prefixes.
  • Changing the order: explicit types must match the real call.
  • Forgetting P.args and P.kwargs: remaining parameters are not preserved.
  • Ignoring method binding: descriptors can change runtime behavior.
  • Assuming typing validates runtime objects: the wrapper must still construct correct dependencies.

Complete unit-of-work example

from collections.abc import Callable
from functools import wraps
from typing import Concatenate, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

class UnitOfWork:
    def __enter__(self):
        return self

    def commit(self) -> None:
        ...

    def __exit__(self, exc_type, exc, tb) -> None:
        if exc is not None:
            self.rollback()

    def rollback(self) -> None:
        ...

def transactional(
    function: Callable[Concatenate[UnitOfWork, P], R],
) -> Callable[P, R]:
    @wraps(function)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        with UnitOfWork() as uow:
            result = function(uow, *args, **kwargs)
            uow.commit()
            return result
    return wrapper

@transactional
def create_order(
    uow: UnitOfWork,
    customer_id: int,
    item_ids: list[int],
) -> int:
    ...

Callers see only customer_id and item_ids. The inner function still declares its dependency explicitly and remains easy to test.

Testing the signature

Use reveal_type(), mypy or pyright fixtures, and intentionally invalid calls. Runtime tests should also verify argument order, method binding, and dependency lifecycle.

Conclusion

typing.Concatenate complements ParamSpec by modeling callables that add or remove parameters at the beginning of a signature. It is useful for context injection, authentication, locks, connections, and adapters.

The official Python Concatenate documentation defines the construction. Also read the guide to Python ParamSpec, and choose Protocol or overloads when a transformation cannot be expressed as a generic prefix.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A close-up view of a person's hand signing a business contract on a desk with a pen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ParamSpec: Preserve Signatures

    Learn Python ParamSpec to preserve complete function signatures in decorators, callbacks, async wrappers, and higher-order utilities.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeIs: Narrow Both Branches

    Learn Python TypeIs to narrow true and false branches, compare it with TypeGuard, and build sound reusable type predicates.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Hands typing code on a laptop in a workspace. Indoor setting focused on software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeGuard: Refine Types Safely

    Learn Python TypeGuard to narrow types and validate collections, TypedDict, Protocol, and external data with runtime checks and static safety.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python typing.Self: Fluent Return Types

    Learn Python typing.Self for fluent methods, classmethods, builders, clones, Protocol, context managers, generics, and subclass-preserving returns.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A conceptual image showing error code projected on binary data with smoke.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExceptionGroup: Multiple Errors

    Learn Python ExceptionGroup for multiple errors, except*, nested groups, TaskGroup, filtering, logging, API compatibility, and batch validation.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Path.walk: Traverse Directories

    Learn Python Path.walk to traverse directories, prune folders, handle errors and symlinks, calculate sizes, delete safely, and support older versions.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026