# What is PEP 612?


[PEP 612: Parameter Specification Variables](https://peps.python.org/pep-0612/) (Python 3.10) adds `ParamSpec` and `Concatenate` to the typing system. They let a decorator declare "this returns a function with the same parameters as the one it wrapped," which ordinary type variables from [PEP 484](https://pydevtools.com/handbook/explanation/what-is-pep-484.md) cannot express.

## Why did decorators erase signatures?

Before PEP 612, the best available type for a generic decorator was `Callable[..., R]`. The `...` throws away everything about the parameters, so a type checker accepts any arguments to the decorated function:

```python
from collections.abc import Callable
from typing import TypeVar

R = TypeVar("R")

def logged(func: Callable[..., R]) -> Callable[..., R]: ...

@logged
def greet(name: str, excited: bool = False) -> str: ...

greet(42)  # no error: the checker lost the signature
```

Every decorated function opted out of argument checking, and the checker never said so.

## How does ParamSpec preserve the signature?

A `ParamSpec` captures the wrapped function's full parameter list, names and defaults included, and carries it to the returned function:

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

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

def logged(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@logged
def greet(name: str, excited: bool = False) -> str:
    return f"hi {name}{'!' if excited else ''}"

greet("world", excited=True)  # ok
greet(42)                     # caught
```

[mypy](https://pydevtools.com/handbook/reference/mypy.md) reports the second call as `Argument 1 to "greet" has incompatible type "int"; expected "str"`. [Pyright](https://pydevtools.com/handbook/reference/pyright.md) and [ty](https://pydevtools.com/handbook/reference/ty.md) catch it the same way.

## What does Concatenate add?

`Concatenate` types decorators that add or remove leading arguments. A decorator that opens a database connection and passes it as the first argument keeps the rest of the signature intact:

```python
from collections.abc import Callable
from typing import Concatenate
import sqlite3

def with_conn[**P, R](func: Callable[Concatenate[sqlite3.Connection, P], R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        conn = sqlite3.connect("app.db")
        try:
            return func(conn, *args, **kwargs)
        finally:
            conn.close()
    return wrapper

@with_conn
def count_rows(conn: sqlite3.Connection, table: str) -> int:
    return conn.execute(f"SELECT count(*) FROM {table}").fetchone()[0]

count_rows("users")    # ok: the decorator supplies conn
count_rows(table=123)  # caught: str expected
```

Callers never see the `conn` parameter; the checker validates everything else.

## Where do you get ParamSpec?

`ParamSpec` and `Concatenate` live in `typing` from Python 3.10, and in `typing_extensions` for older versions. On Python 3.12+, [PEP 695](https://pydevtools.com/handbook/explanation/what-is-pep-695.md) type parameter syntax declares one inline with `def logged[**P, R](...)`, as the `with_conn` example shows, with no `ParamSpec("P")` declaration at all.

## Learn More

- [PEP 612: Parameter Specification Variables](https://peps.python.org/pep-0612/)
- [ParamSpec in the typing documentation](https://docs.python.org/3/library/typing.html#typing.ParamSpec)
- [What is PEP 681?](https://pydevtools.com/handbook/explanation/what-is-pep-681.md) covers the other big "make the checker understand library magic" PEP
