ParamSpec no Python: preserve assinaturas

Publicado em: 28/08/2026
Tempo de leitura: 5 minutos
A close-up view of a person's hand signing a business contract on a desk with a pen.

Decoradores, callbacks e funções de ordem superior costumam receber uma função e devolver outra. O desafio da tipagem estática é preservar todos os parâmetros da função original sem recorrer a Callable[..., T], que aceita qualquer assinatura e perde informações importantes. typing.ParamSpec representa a lista completa de parâmetros de uma callable, incluindo posicionais, nomeados, variádicos e defaults.

Neste guia, você aprenderá a usar ParamSpec em decoradores, wrappers síncronos e assíncronos, factories, Protocol, métodos e funções genéricas. Também verá como combinar P.args, P.kwargs, TypeVar e Concatenate, além dos erros mais comuns.

O problema de Callable com reticências

from collections.abc import Callable
from typing import TypeVar

R = TypeVar("R")

def registrar(funcao: Callable[..., R]) -> Callable[..., R]:
    def wrapper(*args, **kwargs):
        print("chamada")
        return funcao(*args, **kwargs)
    return wrapper

O tipo de retorno preserva apenas o resultado R. O analisador não sabe quais argumentos o wrapper aceita, portanto chamadas incorretas podem passar sem aviso.

Criando um ParamSpec

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

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

def registrar(funcao: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("chamada")
        return funcao(*args, **kwargs)
    return wrapper

Callable[P, R] significa uma função com parâmetros descritos por P e retorno R. O wrapper recebe exatamente os mesmos parâmetros e devolve o mesmo resultado.

O que P.args e P.kwargs representam

P.args anota o pacote de argumentos posicionais recebido pelo wrapper. P.kwargs representa os argumentos nomeados. Eles só devem aparecer juntos em funções que capturam *args e **kwargs relacionados ao mesmo ParamSpec.

def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
    return funcao(*args, **kwargs)

Esses atributos não são listas de tipos para manipulação arbitrária; são marcadores especiais usados pelo analisador.

Decorador com tempo de execução

from functools import wraps
from time import perf_counter

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

def medir(funcao: Callable[P, R]) -> Callable[P, R]:
    @wraps(funcao)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        inicio = perf_counter()
        try:
            return funcao(*args, **kwargs)
        finally:
            duracao = perf_counter() - inicio
            print(f"{funcao.__name__}: {duracao:.6f}s")
    return wrapper

functools.wraps preserva metadados em runtime, enquanto ParamSpec preserva a assinatura para o analisador. As duas ferramentas resolvem problemas diferentes e complementares.

Decorador com argumentos

def repetir(vezes: int):
    def decorar(funcao: Callable[P, R]) -> Callable[P, R]:
        @wraps(funcao)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            resultado: R
            for _ in range(vezes):
                resultado = funcao(*args, **kwargs)
            return resultado
        return wrapper
    return decorar

A factory externa recebe a configuração. A função interna continua genérica em relação à assinatura decorada.

Wrappers assíncronos

from collections.abc import Awaitable

async def executar_com_log(
    funcao: Callable[P, Awaitable[R]],
    *args: P.args,
    **kwargs: P.kwargs,
) -> R:
    print("iniciando")
    resultado = await funcao(*args, **kwargs)
    print("concluído")
    return resultado

O ParamSpec preserva os parâmetros da coroutine function, enquanto Awaitable[R] descreve o valor aguardável retornado.

Decorador que transforma sync em async

import asyncio


def em_thread(
    funcao: Callable[P, R],
) -> Callable[P, Awaitable[R]]:
    @wraps(funcao)
    async def wrapper(
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        return await asyncio.to_thread(funcao, *args, **kwargs)
    return wrapper

A assinatura dos parâmetros é preservada, mas o retorno muda de R para Awaitable[R].

Callbacks configuráveis

class Executor:
    def executar(
        self,
        callback: Callable[P, R],
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        return callback(*args, **kwargs)

Esse método aceita qualquer callback, mas exige argumentos compatíveis com a callable fornecida. Uma chamada com parâmetros faltando ou extras pode ser detectada estaticamente.

ParamSpec em Protocol

from typing import Protocol

class Middleware(Protocol[P, R]):
    def __call__(
        self,
        proximo: Callable[P, R],
    ) -> Callable[P, R]: ...

Esse contrato descreve objetos que recebem uma função e devolvem outra com a mesma assinatura. Protocol é útil quando o decorador possui estado ou é implementado por uma classe.

Classes decoradoras

class Contador:
    def __init__(self, funcao: Callable[P, R]) -> None:
        self.funcao = funcao
        self.chamadas = 0

    def __call__(
        self,
        *args: P.args,
        **kwargs: P.kwargs,
    ) -> R:
        self.chamadas += 1
        return self.funcao(*args, **kwargs)

A inferência de classes decoradoras pode variar entre analisadores e versões. Em bibliotecas públicas, execute testes de tipagem específicos para garantir que a assinatura exposta permaneça correta.

Métodos e self

Quando um decorador é aplicado a métodos, o primeiro parâmetro faz parte da assinatura capturada. Normalmente, o wrapper não precisa tratar self separadamente.

def auditar(funcao: Callable[P, R]) -> Callable[P, R]:
    @wraps(funcao)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(funcao.__qualname__)
        return funcao(*args, **kwargs)
    return wrapper

Se o decorador adiciona ou remove explicitamente um parâmetro, use Concatenate.

Adicionando um parâmetro com Concatenate

from typing import Concatenate

class Contexto:
    usuario: str

def injetar_contexto(
    funcao: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R]:
    @wraps(funcao)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        contexto = Contexto()
        contexto.usuario = "sistema"
        return funcao(contexto, *args, **kwargs)
    return wrapper

A função original exige Contexto como primeiro argumento, mas a função decorada não o expõe ao chamador.

ParamSpec versus TypeVar

TypeVar representa um único tipo. ParamSpec representa uma lista completa de parâmetros. Um TypeVar não consegue capturar, por exemplo, dois argumentos posicionais, um keyword-only e **kwargs como uma unidade.

T = TypeVar("T")
# T pode ser int, str, Usuario...

P = ParamSpec("P")
# P pode representar (id: int, *, ativo: bool)

ParamSpec versus Callable[…, R]

Callable[..., R] é apropriado quando os parâmetros realmente não importam ou quando a API aceita qualquer chamada de forma dinâmica. Para decoradores que devem preservar a interface original, ParamSpec é muito mais seguro.

Parâmetros keyword-only

def enviar(destino: str, *, urgente: bool = False) -> int:
    ...

funcao_tipica = registrar(enviar)
funcao_tipica("fila", urgente=True)

O ParamSpec captura a natureza keyword-only de urgente. O analisador pode rejeitar uma chamada posicional incompatível.

Overloads e ParamSpec

Um decorador tipado com ParamSpec normalmente preserva a assinatura inferida de cada overload, desde que o analisador consiga aplicar o decorador às declarações. Bibliotecas com APIs muito complexas podem precisar expor overloads explícitos no próprio decorador.

Retorno covariante e restrições

O TypeVar de retorno pode usar limites ou covariância conforme o contrato. Não altere a relação sem necessidade. Para um decorador que apenas observa a chamada, Callable[P, R] -> Callable[P, R] é a forma mais transparente.

Decoradores que podem retornar None

def ignorar_erros(
    funcao: Callable[P, R],
) -> Callable[P, R | None]:
    @wraps(funcao)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
        try:
            return funcao(*args, **kwargs)
        except Exception:
            return None
    return wrapper

Os parâmetros continuam iguais, mas o tipo de retorno muda. O chamador agora precisa tratar None.

Preservando exceções e comportamento

ParamSpec não descreve exceções, efeitos colaterais, desempenho ou política de retries. A assinatura estática é apenas parte do contrato. Documente mudanças comportamentais introduzidas pelo wrapper.

Compatibilidade de versões

ParamSpec está disponível no módulo typing de versões modernas. Para versões anteriores, use typing_extensions.ParamSpec. A sintaxe genérica mais recente pode variar, mas a forma tradicional com ParamSpec("P") possui ampla compatibilidade.

Erros comuns

  • Usar Callable[…, R]: perde a validação dos argumentos.
  • Esquecer P.args ou P.kwargs: o wrapper deixa de encaminhar corretamente a assinatura.
  • Usar ParamSpec fora de Callable: ele possui contextos específicos.
  • Alterar parâmetros sem Concatenate: a anotação deixa de refletir a função real.
  • Confundir wraps com tipagem: wraps preserva metadados, não resolve tudo estaticamente.
  • Ocultar mudança de retorno: o tipo do wrapper precisa mostrar a transformação.

Exemplo completo: retry tipado

from functools import wraps
from time import sleep

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

def retry(
    tentativas: int,
    espera: float = 0.0,
):
    if tentativas < 1:
        raise ValueError("tentativas deve ser positiva")

    def decorar(funcao: Callable[P, R]) -> Callable[P, R]:
        @wraps(funcao)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            ultimo_erro: Exception | None = None
            for indice in range(tentativas):
                try:
                    return funcao(*args, **kwargs)
                except Exception as erro:
                    ultimo_erro = erro
                    if indice + 1 < tentativas and espera:
                        sleep(espera)
            assert ultimo_erro is not None
            raise ultimo_erro
        return wrapper
    return decorar

O decorador adiciona retries sem perder os parâmetros ou o retorno da função original. Em produção, limite as exceções capturadas e considere idempotência.

Testando tipagem

Além de testes de runtime, crie arquivos de teste com chamadas válidas e inválidas. Ferramentas como mypy e pyright podem ser executadas no CI. Recursos como reveal_type() ajudam a verificar se a função decorada mantém a assinatura esperada.

Conclusão

typing.ParamSpec permite escrever decoradores e funções de ordem superior que preservam assinaturas completas. Ele elimina grande parte do uso inseguro de Callable[..., R] e melhora autocomplete, documentação e detecção de erros.

A documentação oficial de ParamSpec no Python detalha seus contextos. Combine ParamSpec com TypeVar para retornos, functools.wraps para metadados e Concatenate apenas quando o wrapper realmente adiciona ou remove parâmetros.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A detailed image of a reticulated python showcasing its patterned scales and intricate skin texture.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs no Python: refine os dois ramos

    Aprenda TypeIs no Python para refinar tipos nos ramos verdadeiro e falso, comparar com TypeGuard e criar predicados seguros.

    Ler mais

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

    TypeGuard no Python: refine tipos com segurança

    Aprenda TypeGuard no Python para refinar tipos, validar coleções, TypedDict e Protocol com segurança estática e checagem real.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    typing.Self no Python: retornos fluentes

    Aprenda typing.Self no Python para métodos fluentes, classmethods, builders, clones, Protocol, context managers e retornos que preservam subclasses.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Yellow block letters spelling 'error' on a vibrant pink background, capturing a playful message.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup no Python: múltiplos erros

    Aprenda ExceptionGroup no Python para múltiplos erros, except*, grupos aninhados, TaskGroup, filtros, logging e validação em lote.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A tranquil wooden pathway winds through a vibrant autumn forest, covered in fallen leaves.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk no Python: percorra diretórios

    Aprenda Path.walk no Python para percorrer diretórios, podar pastas, tratar erros, links simbólicos, tamanhos, remoção segura e compatibilidade.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Minimalist hourglass filled with sand symbolizing time and patience, against a soft background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout no Python: controle prazos

    Aprenda asyncio.timeout no Python para deadlines, timeout_at, reagendamento, TaskGroup, cleanup, retries e cancelamento assíncrono seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026