# How Pyrefly Handles ParamSpec and TypeVarTuple for Callable Generics

> Learn how Pyrefly processes ParamSpec and TypeVarTuple for callable generics. Discover its Typed Syntax Process for handling variadic generic signatures.

- Repository: [Meta/pyrefly](https://github.com/facebook/pyrefly)
- Tags: internals
- Published: 2026-05-21

---

**Pyrefly converts `ParamSpec` and `TypeVarTuple` into specialized `TypeVarType` nodes within its Typed Syntax Process (TSP) representation, storing them as parameter packs in `CallableType` structures to support variadic generic callable signatures.**

Pyrefly, Facebook's open-source Python type checker, implements PEP 612's callable generic constructs through a multi-stage pipeline that transforms Python AST nodes into a solver-friendly intermediate graph. When analyzing `Callable` types containing `ParamSpec` or `TypeVarTuple`, the type-checker flags these variables in [`pyrefly/lib/tsp/type_conversion.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/type_conversion.rs) and later records them as bindings inside `CallableType` structures defined in [`pyrefly/lib/binding/callable.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/binding/callable.rs).

## Converting ParamSpec to TSP

Pyrefly distinguishes between a `ParamSpec` variable itself and its placeholder value inside a `Callable` signature.

### ParamSpec Variable Conversion

In [`pyrefly/lib/tsp/type_conversion.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/type_conversion.rs), the compiler matches the `PyreflyType::ParamSpec` variant and constructs a `DeclaredType::TypeVarType` with the `is_param_spec` flag enabled:

```rust
// pyrefly/lib/tsp/type_conversion.rs (≈ line 283)
match pyrefly_type {
    PyreflyType::ParamSpec(ps) => {
        TypeVarType::new(ps.name.clone(), /* is_param_spec = true */)
    }
    // ...
}

```

This flag tells the solver that this type variable represents a parameter pack rather than a single type.

### ParamSpecValue Placeholder

When `ParamSpec` appears as the parameter list of a `Callable` (forming a `ParamSpecValue`), the converter maps it to a builtin type placeholder:

```rust
// pyrefly/lib/tsp/type_conversion.rs (≈ line 344)
PyreflyType::ParamSpecValue(_) => builtin("ParamSpec"),

```

The solver recognizes this builtin name and expands it when resolving concrete call sites.

## TypeVarTuple Implementation

`TypeVarTuple` follows an analogous conversion path but uses a variadic-specific flag. The same conversion function in [`pyrefly/lib/tsp/type_conversion.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/type_conversion.rs) handles this variant:

```rust
// pyrefly/lib/tsp/type_conversion.rs (≈ line 345)
PyreflyType::TypeVarTuple(tv) => {
    TypeVarType::new(tv.name.clone(), /* is_type_var_tuple = true */)
}

```

The `is_type_var_tuple` marker enables the solver to treat the variable as a variadic tuple that can be unpacked with the `*` or `Unpack` operator.

## Building Generic Callable Signatures

Once converted, these type variables participate in `CallableType` construction within [`pyrefly/lib/binding/callable.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/binding/callable.rs).

### CallableType Construction

The callable builder walks the parameter list and, upon encountering a `ParamSpec` or `TypeVarTuple`, records a `ParamSpecBinding` or `TypeVarTupleBinding` inside the `CallableType` structure:

- **ParamSpecBinding**: Captures the remaining arguments of a callable after any `Concatenate` prefix.
- **TypeVarTupleBinding**: Represents a variadic argument pack that can be substituted with a concrete tuple of types.

### Parameter Pack Substitution

During type solving (located in `pyrefly/lib/solver/`), the engine substitutes these bindings when it encounters a concrete call. For a `ParamSpec`, the solver unpacks the captured argument types into the callable's parameter list. For a `TypeVarTuple`, it expands the variadic tuple into individual positional arguments.

## Error Handling and Validation

Pyrefly emits precise diagnostics when these constructs appear in invalid contexts. If a `ParamSpec` or `TypeVarTuple` is used where a concrete type is required (for example, as a generic argument to a non-callable), the type-checker raises an error such as `E: ParamSpec is not allowed in this context`.

Test coverage in [`pyrefly/lib/test/paramspec.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/test/paramspec.rs) and [`pyrefly/lib/test/type_var_tuple.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/test/type_var_tuple.rs) asserts these validation rules, ensuring that variadic type variables cannot be instantiated with incompatible types or used outside callable signatures.

## Practical Code Examples

The following patterns demonstrate how these internal mechanisms surface in user code:

```python

# Example 1: ParamSpec with Concatenate

from typing import Callable, ParamSpec, Concatenate

P = ParamSpec('P')

def wrapper(f: Callable[Concatenate[int, P], int]) -> Callable[P, int]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> int:
        return f(42, *args, **kwargs)
    return inner

# Pyrefly records:

# - A CallableType with a leading fixed `int` argument

# - A ParamSpecBinding capturing the remaining `P` parameters

```

```python

# Example 2: TypeVarTuple with Unpack

from typing import Callable, TypeVarTuple, Unpack

Ts = TypeVarTuple('Ts')

def apply_args(f: Callable[[Unpack[Ts]], int], *args: Unpack[Ts]) -> int:
    return f(*args)

# Pyrefly treats `Ts` as a variadic TypeVarType;

# the Callable becomes a generic that accepts any tuple of argument types.

```

## Summary

- **ParamSpec** is converted to a `TypeVarType` with the `is_param_spec` flag in [`pyrefly/lib/tsp/type_conversion.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/tsp/type_conversion.rs).
- **TypeVarTuple** receives the `is_type_var_tuple` flag during the same conversion phase.
- **ParamSpecValue** maps to a builtin `"ParamSpec"` placeholder that the solver expands at call sites.
- **CallableType** structures in [`pyrefly/lib/binding/callable.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/binding/callable.rs) store these as `ParamSpecBinding` or `TypeVarTupleBinding` objects.
- The **solver** substitutes concrete argument types for these bindings when resolving generic callable calls.
- **Validation errors** are enforced and tested in [`pyrefly/lib/test/paramspec.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/test/paramspec.rs) and [`pyrefly/lib/test/type_var_tuple.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/test/type_var_tuple.rs).

## Frequently Asked Questions

### What is the difference between ParamSpec and TypeVarTuple in Pyrefly's implementation?

**ParamSpec** captures the *shape* of a function's parameter list (both positional and keyword arguments), while **TypeVarTuple** captures a variadic sequence of individual types. Internally, `ParamSpec` triggers the `is_param_spec` flag and creates a `ParamSpecBinding`, whereas `TypeVarTuple` sets `is_type_var_tuple` and creates a `TypeVarTupleBinding`. The former is used to preserve argument names and kinds; the latter treats arguments as a homogeneous or heterogeneous tuple.

### How does Pyrefly prevent misuse of ParamSpec in non-callable contexts?

According to [`pyrefly/lib/test/paramspec.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/test/paramspec.rs), the type-checker emits specific diagnostics when a `ParamSpec` appears where a concrete type is expected, such as in a generic class parameter list or as a variable annotation outside a `Callable`. The validation logic checks the `is_param_spec` flag during type resolution and raises an error if the context does not support parameter packs.

### Where does the actual parameter substitution happen for generic callables?

Substitution occurs in the solver modules under `pyrefly/lib/solver/`. When the type-checker encounters a call to a generic callable containing a `ParamSpec` or `TypeVarTuple`, it matches the provided arguments against the `CallableType`'s bindings. The solver then creates a substitution map that replaces the variadic type variables with the concrete argument types or tuples inferred from the call site.

### Does Pyrefly support Concatenate and Unpack operators?

Yes. According to the callable binding logic in [`pyrefly/lib/binding/callable.rs`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/binding/callable.rs), Pyrefly supports `typing.Concatenate` for prepending fixed arguments to a `ParamSpec`, and `typing.Unpack` (or the `*` syntax) for expanding `TypeVarTuple` instances. The parser recognizes these operators during AST construction, and the type converter preserves them as modifiers on the `CallableType` before the solver performs final expansion.