Repository: python/mypy
Pull request: Use `tuple[object, ...]` and `dict[str, object]` as upper bounds for `ParamSpec.args` and `ParamSpec.kwargs`
Fixes #12386

### Description

Mypy currently thinks that a variable annotated with `P.args` is not iterable, and thinks that a variable annotated with `P.kwargs` does not have a `.pop()` method:

```python
from typing import Callable
from typing_extensions import ParamSpec

P = ParamSpec('P')

def func(callback: Callable[P, str]) -> Callable[P, str]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> str:
        for a in args:  # E: "P.args" has no attribute "__iter__" (not iterable)
            pass
        b = 'foo' in args    # E: "P.args" has no attribute "__iter__" (not iterable)
        print(args.count(42))  # E: "P.args" has no attribute "count"
        print(len(args))  # E: Argument 1 to "len" has incompatible type "P.args"; expected "Sized"
        for c, d in kwargs.items():  # E: "P.kwargs" has no attribute "items"
            reveal_type(c)  # N: Revealed type is "Any"
        kwargs.pop('bar')  # E: "P.kwargs" has no attribute "pop"
        return 'baz'
    return inner
```

If we use `tuple[Any, ...]` as the upper bound of `ParamSpec.args`, and `dict[str, Any]` as the upper bound of `ParamSpec.kwargs`, all these false-positive errors go away.

## Test Plan

Existing tests have been updated, and a test case has been added.

To avoid repetition among the `ParamSpec` test cases, I've added a new `builtins` fixture, `paramspec.pyi`, which basically combines `tuple.pyi` and `dict.pyi`.

Problem statement:
Paramspec cannot be used as tuple/dict
**Bug Report**

I face issues, when using ParamSpec.args as a tuple and ParamSpec.kwargs as a dict

**To Reproduce**

Execute mypy on the following code

```python
from typing_extensions import ParamSpec
from typing import Callable

P = ParamSpec("P")

def foo(func: Callable[P, None]):
    def _inner(*args: P.args, **kwargs: P.kwargs):
        x = kwargs.pop("x")
```

**Expected Behavior**

It works

**Actual Behavior**

Error: `test.py:8:13: error: "P.kwargs" has no attribute "pop"  [attr-defined]`

**Your Environment**

- Mypy version used: 0.941
- Mypy command-line flags: none
- Mypy configuration options from `mypy.ini` (and other config files): none
- Python version used: 3.7.1
- Operating system and version: Linux

Hints from issue discussion:
I just closed #12660 as a duplicate. Here's the repro from that issue, by @rbroderi:

```python
from typing import TypeVar, Callable, ParamSpec
from functools import wraps

P = ParamSpec("P")
R = TypeVar("R")

def dec_test(func: Callable[P, R]) -> Callable[P, R]:
    @wraps(func)
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        for a in args:  # error: "P.args" has no attribute "__iter__" (not iterable)
            print(a)
        for arg_k, arg_v in kwargs.items():  # error: "P.kwargs" has no attribute "items"
            print(f"{arg_k},{arg_v}")
        return func(*args, **kwargs)
  
    return inner
```

Playground link: https://mypy-play.net/?mypy=latest&python=3.10&gist=52d9dbf3999e25e8a72c3bb20c1cb097

Commit message: Fix

Patch to review:
diff --git a/mypy/checkmember.py b/mypy/checkmember.py
index 16bd0e074c3f..0480ed2a5468 100644
--- a/mypy/checkmember.py
+++ b/mypy/checkmember.py
@@ -164,7 +164,7 @@ def _analyze_member_access(name: str,
         return analyze_typeddict_access(name, typ, mx, override_info)
     elif isinstance(typ, NoneType):
         return analyze_none_member_access(name, typ, mx)
-    elif isinstance(typ, TypeVarType):
+    elif isinstance(typ, TypeVarLikeType):
         return _analyze_member_access(name, typ.upper_bound, mx, override_info)
     elif isinstance(typ, DeletedType):
         mx.msg.deleted_as_rvalue(typ, mx.context)
diff --git a/mypy/semanal.py b/mypy/semanal.py
index 985d5217cc08..43116ad0074d 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1369,7 +1369,7 @@ class Foo(Bar, Generic[T]): ...
             del base_type_exprs[i]
         tvar_defs: List[TypeVarLikeType] = []
         for name, tvar_expr in declared_tvars:
-            tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
+            tvar_def = self.tvar_scope.bind_new(name, tvar_expr, named_type_func=self.named_type)
             tvar_defs.append(tvar_def)
         return base_type_exprs, tvar_defs, is_protocol
 
diff --git a/mypy/tvar_scope.py b/mypy/tvar_scope.py
index c1fe1cd6be35..c794435fbaf2 100644
--- a/mypy/tvar_scope.py
+++ b/mypy/tvar_scope.py
@@ -1,6 +1,7 @@
-from typing import Optional, Dict, Union
+from typing import Optional, Dict, Union, Callable
 from mypy.types import (
     TypeVarLikeType, TypeVarType, ParamSpecType, ParamSpecFlavor, TypeVarId, TypeVarTupleType,
+    Instance
 )
 from mypy.nodes import (
     ParamSpecExpr, TypeVarExpr, TypeVarLikeExpr, SymbolTableNode, TypeVarTupleExpr,
@@ -61,7 +62,9 @@ def class_frame(self, namespace: str) -> 'TypeVarLikeScope':
         """A new scope frame for binding a class. Prohibits *this* class's tvars"""
         return TypeVarLikeScope(self.get_function_scope(), True, self, namespace=namespace)
 
-    def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
+    def bind_new(
+        self, name: str, tvar_expr: TypeVarLikeExpr, *, named_type_func: Callable[..., Instance]
+    ) -> TypeVarLikeType:
         if self.is_class_scope:
             self.class_id += 1
             i = self.class_id
@@ -88,7 +91,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
                 tvar_expr.fullname,
                 i,
                 flavor=ParamSpecFlavor.BARE,
-                upper_bound=tvar_expr.upper_bound,
+                named_type_func=named_type_func,
                 line=tvar_expr.line,
                 column=tvar_expr.column
             )
diff --git a/mypy/typeanal.py b/mypy/typeanal.py
index eee8a43c25f3..c2b0913dae3a 100644
--- a/mypy/typeanal.py
+++ b/mypy/typeanal.py
@@ -222,7 +222,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
                 # Change the line number
                 return ParamSpecType(
                     tvar_def.name, tvar_def.fullname, tvar_def.id, tvar_def.flavor,
-                    tvar_def.upper_bound, line=t.line, column=t.column,
+                    named_type_func=self.named_type, line=t.line, column=t.column,
                 )
             if isinstance(sym.node, TypeVarExpr) and tvar_def is not None and self.defining_alias:
                 self.fail('Can\'t use bound type variable "{}"'
@@ -717,7 +717,7 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
                     else:
                         assert False, kind
                     return ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, flavor,
-                                         upper_bound=self.named_type('builtins.object'),
+                                         named_type_func=self.named_type,
                                          line=t.line, column=t.column)
         return self.anal_type(t, nested=nested)
 
@@ -855,13 +855,11 @@ def analyze_callable_args_for_paramspec(
         if not isinstance(tvar_def, ParamSpecType):
             return None
 
-        # TODO: Use tuple[...] or Mapping[..] instead?
-        obj = self.named_type('builtins.object')
         return CallableType(
             [ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.ARGS,
-                           upper_bound=obj),
+                           named_type_func=self.named_type),
              ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.KWARGS,
-                           upper_bound=obj)],
+                           named_type_func=self.named_type)],
             [nodes.ARG_STAR, nodes.ARG_STAR2],
             [None, None],
             ret_type=ret_type,
@@ -891,8 +889,6 @@ def analyze_callable_args_for_concatenate(
         if not isinstance(tvar_def, ParamSpecType):
             return None
 
-        # TODO: Use tuple[...] or Mapping[..] instead?
-        obj = self.named_type('builtins.object')
         # ick, CallableType should take ParamSpecType
         prefix = tvar_def.prefix
         # we don't set the prefix here as generic arguments will get updated at some point
@@ -900,9 +896,9 @@ def analyze_callable_args_for_concatenate(
         return CallableType(
             [*prefix.arg_types,
              ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.ARGS,
-                           upper_bound=obj),
+                           named_type_func=self.named_type),
              ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.KWARGS,
-                           upper_bound=obj)],
+                           named_type_func=self.named_type)],
             [*prefix.arg_kinds, nodes.ARG_STAR, nodes.ARG_STAR2],
             [*prefix.arg_names, None, None],
             ret_type=ret_type,
@@ -1134,7 +1130,7 @@ def bind_function_type_variables(
                 assert var_node, "Binding for function type variable not found within function"
                 var_expr = var_node.node
                 assert isinstance(var_expr, TypeVarLikeExpr)
-                self.tvar_scope.bind_new(var.name, var_expr)
+                self.tvar_scope.bind_new(var.name, var_expr, named_type_func=self.named_type)
             return fun_type.variables
         typevars = self.infer_type_variables(fun_type)
         # Do not define a new type variable if already defined in scope.
@@ -1144,7 +1140,7 @@ def bind_function_type_variables(
         for name, tvar in typevars:
             if not self.tvar_scope.allow_binding(tvar.fullname):
                 self.fail('Type variable "{}" is bound by an outer class'.format(name), defn)
-            self.tvar_scope.bind_new(name, tvar)
+            self.tvar_scope.bind_new(name, tvar, named_type_func=self.named_type)
             binding = self.tvar_scope.get_binding(tvar.fullname)
             assert binding is not None
             defs.append(binding)
diff --git a/mypy/types.py b/mypy/types.py
index e43b73f093b8..9a4836a996ea 100644
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -6,7 +6,7 @@
 
 from typing import (
     Any, TypeVar, Dict, List, Tuple, cast, Set, Optional, Union, Iterable, NamedTuple,
-    Sequence
+    Sequence, Callable
 )
 from typing_extensions import ClassVar, Final, TYPE_CHECKING, overload, TypeAlias as _TypeAlias
 
@@ -583,19 +583,39 @@ class ParamSpecType(TypeVarLikeType):
     prefix: 'Parameters'
 
     def __init__(
-         self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int,
-         upper_bound: Type, *, line: int = -1, column: int = -1,
-         prefix: Optional['Parameters'] = None
+         self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int, *,
+         upper_bound: Optional[Type] = None,
+         named_type_func: Optional[Callable[..., 'Instance']] = None, line: int = -1,
+         column: int = -1, prefix: Optional['Parameters'] = None
     ) -> None:
+        if upper_bound is None:
+            assert named_type_func is not None, (
+                "Either 'upper_bound' or 'named_type_func' must be specified"
+            )
+            upper_bound = self.get_fallback(flavor, named_type_func)
         super().__init__(name, fullname, id, upper_bound, line=line, column=column)
         self.flavor = flavor
         self.prefix = prefix or Parameters([], [], [])
 
+    @staticmethod
+    def get_fallback(flavor: int, named_type_func: Callable[..., 'Instance']) -> 'Instance':
+        builtins_object = named_type_func('builtins.object')
+        if flavor == ParamSpecFlavor.BARE:
+            return builtins_object
+        elif flavor == ParamSpecFlavor.ARGS:
+            return named_type_func('builtins.tuple', [builtins_object])
+        else:
+            return named_type_func(
+                'builtins.dict',
+                [named_type_func('builtins.str'), builtins_object]
+            )
+
     @staticmethod
     def new_unification_variable(old: 'ParamSpecType') -> 'ParamSpecType':
         new_id = TypeVarId.new(meta_level=1)
-        return ParamSpecType(old.name, old.fullname, new_id, old.flavor, old.upper_bound,
-                             line=old.line, column=old.column, prefix=old.prefix)
+        return ParamSpecType(old.name, old.fullname, new_id, old.flavor,
+                             upper_bound=old.upper_bound, line=old.line, column=old.column,
+                             prefix=old.prefix)
 
     def with_flavor(self, flavor: int) -> 'ParamSpecType':
         return ParamSpecType(self.name, self.fullname, self.id, flavor,
@@ -610,7 +630,7 @@ def copy_modified(self, *,
             self.fullname,
             id if id is not _dummy else self.id,
             flavor if flavor is not _dummy else self.flavor,
-            self.upper_bound,
+            upper_bound=self.upper_bound,
             line=self.line,
             column=self.column,
             prefix=prefix if prefix is not _dummy else self.prefix,
@@ -656,7 +676,7 @@ def deserialize(cls, data: JsonDict) -> 'ParamSpecType':
             data['fullname'],
             data['id'],
             data['flavor'],
-            deserialize_type(data['upper_bound']),
+            upper_bound=deserialize_type(data['upper_bound']),
             prefix=Parameters.deserialize(data['prefix'])
         )
 
@@ -1739,7 +1759,7 @@ def param_spec(self) -> Optional[ParamSpecType]:
             # TODO: confirm that all arg kinds are positional
             prefix = Parameters(self.arg_types[:-2], self.arg_kinds[:-2], self.arg_names[:-2])
         return ParamSpecType(arg_type.name, arg_type.fullname, arg_type.id, ParamSpecFlavor.BARE,
-                             arg_type.upper_bound, prefix=prefix)
+                             upper_bound=arg_type.upper_bound, prefix=prefix)
 
     def expand_param_spec(self,
                           c: Union['CallableType', Parameters],
diff --git a/mypy/typevars.py b/mypy/typevars.py
index b49194f342e0..cacd8db4a67a 100644
--- a/mypy/typevars.py
+++ b/mypy/typevars.py
@@ -23,7 +23,7 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]:
             )
         else:
             assert isinstance(tv, ParamSpecType)
-            tv = ParamSpecType(tv.name, tv.fullname, tv.id, tv.flavor, tv.upper_bound,
+            tv = ParamSpecType(tv.name, tv.fullname, tv.id, tv.flavor, upper_bound=tv.upper_bound,
                                line=-1, column=-1)
         tvs.append(tv)
     inst = Instance(typ, tvs)
diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test
index 2242b79d4b64..28b08aa7122f 100644
--- a/test-data/unit/check-parameter-specification.test
+++ b/test-data/unit/check-parameter-specification.test
@@ -11,7 +11,7 @@ P2 = ParamSpec("P2", contravariant=True)  # E: Only the first argument to ParamS
 P3 = ParamSpec("P3", bound=int)  # E: Only the first argument to ParamSpec has defined semantics
 P4 = ParamSpec("P4", int, str)  # E: Only the first argument to ParamSpec has defined semantics
 P5 = ParamSpec("P5", covariant=True, bound=int)  # E: Only the first argument to ParamSpec has defined semantics
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecLocations]
 from typing import Callable, List
@@ -35,7 +35,7 @@ def foo5(x: Callable[[int, str], P]) -> None: ...  # E: Invalid location for Par
 
 def foo6(x: Callable[[P], int]) -> None: ...  # E: Invalid location for ParamSpec "P" \
                                               # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]'
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecContextManagerLike]
 from typing import Callable, List, Iterator, TypeVar
@@ -51,7 +51,7 @@ def whatever(x: int) -> Iterator[int]:
 
 reveal_type(whatever)  # N: Revealed type is "def (x: builtins.int) -> builtins.list[builtins.int]"
 reveal_type(whatever(217))  # N: Revealed type is "builtins.list[builtins.int]"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testInvalidParamSpecType]
 # flags: --python-version 3.10
@@ -70,7 +70,7 @@ P = ParamSpec('P')
 
 def f(x: Callable[P, int]) -> None: ...
 reveal_type(f)  # N: Revealed type is "def [P] (x: def (*P.args, **P.kwargs) -> builtins.int)"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecSimpleFunction]
 from typing import Callable, TypeVar
@@ -83,7 +83,7 @@ def changes_return_type_to_str(x: Callable[P, int]) -> Callable[P, str]: ...
 def returns_int(a: str, b: bool) -> int: ...
 
 reveal_type(changes_return_type_to_str(returns_int))  # N: Revealed type is "def (a: builtins.str, b: builtins.bool) -> builtins.str"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecSimpleClass]
 from typing import Callable, TypeVar, Generic
@@ -199,7 +199,7 @@ g: Any
 reveal_type(f(g))  # N: Revealed type is "def (*Any, **Any) -> builtins.str"
 
 f(g)(1, 3, x=1, y=2)
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecDecoratorImplementation]
 from typing import Callable, Any, TypeVar, List
@@ -556,7 +556,7 @@ a: Callable[[int, bytes], str]
 b: Callable[[str, bytes], str]
 
 reveal_type(f(a, b))  # N: Revealed type is "def (builtins.bytes) -> builtins.str"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecConcatenateInReturn]
 from typing_extensions import ParamSpec, Concatenate
@@ -569,7 +569,7 @@ def f(i: Callable[Concatenate[int, P], str]) -> Callable[Concatenate[int, P], st
 n: Callable[[int, bytes], str]
 
 reveal_type(f(n))  # N: Revealed type is "def (builtins.int, builtins.bytes) -> builtins.str"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecConcatenateNamedArgs]
 # flags: --strict-concatenate
@@ -592,7 +592,7 @@ def f2(c: Callable[P, R]) -> Callable[Concatenate[int, P], R]:
 
 # reason for rejection:
 f2(lambda x: 42)(42, x=42)
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 [out]
 main:10: error: invalid syntax
 [out version>=3.8]
@@ -619,7 +619,7 @@ def f2(c: Callable[P, R]) -> Callable[Concatenate[int, P], R]:
 
 # reason for rejection:
 f2(lambda x: 42)(42, x=42)
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 [out]
 main:9: error: invalid syntax
 [out version>=3.8]
@@ -640,7 +640,7 @@ n = f(a)
 
 reveal_type(n)  # N: Revealed type is "def (builtins.int)"
 reveal_type(n(42))  # N: Revealed type is "None"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testCallablesAsParameters]
 # credits to https://github.com/microsoft/pyright/issues/2705
@@ -658,7 +658,7 @@ def test(a: int, /, b: str) -> str: ...
 abc = Foo(test)
 reveal_type(abc)
 bar(abc)
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 [out]
 main:11: error: invalid syntax
 [out version>=3.8]
@@ -677,7 +677,7 @@ n: Foo[[int]]
 def f(x: int) -> None: ...
 
 n.foo(f)
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecLiteralsTypeApplication]
 from typing_extensions import ParamSpec
@@ -709,7 +709,7 @@ Z[bytes, str](lambda one: None)  # E: Cannot infer type of lambda \
                                  # E: Argument 1 to "Z" has incompatible type "Callable[[Any], None]"; expected "Callable[[bytes, str], None]"
 Z[bytes, str](f2)  # E: Argument 1 to "Z" has incompatible type "Callable[[bytes, int], None]"; expected "Callable[[bytes, str], None]"
 
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecLiteralEllipsis]
 from typing_extensions import ParamSpec
@@ -740,7 +740,7 @@ n = Z(f1)
 n = Z(f2)
 n = Z(f3)
 
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecApplyConcatenateTwice]
 from typing_extensions import ParamSpec, Concatenate
@@ -770,7 +770,7 @@ def f(c: C[P]) -> None:
     reveal_type(p1)  # N: Revealed type is "__main__.C[[builtins.str, **P`-1]]"
     p2 = p1.add_str()
     reveal_type(p2)  # N: Revealed type is "__main__.C[[builtins.int, builtins.str, **P`-1]]"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecLiteralJoin]
 from typing import Generic, Callable, Union
@@ -788,7 +788,7 @@ def func(
 ) -> None:
     job = action if isinstance(action, Job) else Job(action)
     reveal_type(job)  # N: Revealed type is "__main__.Job[[builtins.int]]"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testApplyParamSpecToParamSpecLiterals]
 from typing import TypeVar, Generic, Callable
@@ -818,7 +818,7 @@ def func2(job: Job[..., None]) -> None:
     run_job(job, "Hello", 42)
     run_job(job, 42, msg="Hello")
     run_job(job, x=42, msg="Hello")
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testExpandNonBareParamSpecAgainstCallable]
 from typing import Callable, TypeVar, Any
@@ -850,7 +850,7 @@ reveal_type(A().func(f, 42))  # N: Revealed type is "builtins.int"
 
 # TODO: this should reveal `int`
 reveal_type(A().func(lambda x: x + x, 42))  # N: Revealed type is "Any"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecConstraintOnOtherParamSpec]
 from typing import Callable, TypeVar, Any, Generic
@@ -880,7 +880,7 @@ reveal_type(A().func(Job(lambda x: x)))  # N: Revealed type is "__main__.Job[[x:
 
 def f(x: int, y: int) -> None: ...
 reveal_type(A().func(Job(f)))  # N: Revealed type is "__main__.Job[[x: builtins.int, y: builtins.int], None]"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testConstraintBetweenParamSpecFunctions1]
 from typing import Callable, TypeVar, Any, Generic
@@ -898,7 +898,7 @@ def func(__action: Job[_P]) -> Callable[_P, None]:
     ...
 
 reveal_type(func)  # N: Revealed type is "def [_P] (__main__.Job[_P`-1]) -> def (*_P.args, **_P.kwargs)"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testConstraintBetweenParamSpecFunctions2]
 from typing import Callable, TypeVar, Any, Generic
@@ -916,7 +916,7 @@ def func(__action: Job[_P]) -> Callable[_P, None]:
     ...
 
 reveal_type(func)  # N: Revealed type is "def [_P] (__main__.Job[_P`-1]) -> def (*_P.args, **_P.kwargs)"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testConstraintsBetweenConcatenatePrefixes]
 from typing import Any, Callable, Generic, TypeVar
@@ -937,7 +937,7 @@ def adds_await() -> Callable[
         ...
 
     return decorator  # we want `_T` and `_P` to refer to the same things.
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testParamSpecVariance]
 from typing import Callable, Generic
@@ -995,7 +995,7 @@ a3: Callable[[int], None]
 a3 = f3  # E: Incompatible types in assignment (expression has type "Callable[[bool], None]", variable has type "Callable[[int], None]")
 a3 = f2
 a3 = f1
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testDecoratingClassesThatUseParamSpec]
 from typing import Generic, TypeVar, Callable, Any
@@ -1039,7 +1039,7 @@ reveal_type(j)  # N: Revealed type is "__main__.Job[[x: _T`-1]]"
 jf = j.into_callable()
 reveal_type(jf)  # N: Revealed type is "def [_T] (x: _T`-1)"
 reveal_type(jf(1))  # N: Revealed type is "None"
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testStackedConcatenateIsIllegal]
 from typing_extensions import Concatenate, ParamSpec
@@ -1048,7 +1048,7 @@ from typing import Callable
 P = ParamSpec("P")
 
 def x(f: Callable[Concatenate[int, Concatenate[int, P]], None]) -> None: ...  # E: Nested Concatenates are invalid
-[builtins fixtures/tuple.pyi]
+[builtins fixtures/paramspec.pyi]
 
 [case testPropagatedAnyConstraintsAreOK]
 from typing import Any, Callable, Generic, TypeVar
@@ -1063,3 +1063,26 @@ class Job(Generic[P]): ...
 @callback
 def run_job(job: Job[...]) -> T: ...
 [builtins fixtures/tuple.pyi]
+
+[case testTupleAndDictOperationsOnParamSpecArgsAndKwargs]
+from typing import Callable
+from typing_extensions import ParamSpec
+
+P = ParamSpec('P')
+
+def func(callback: Callable[P, str]) -> Callable[P, str]:
+    def inner(*args: P.args, **kwargs: P.kwargs) -> str:
+        reveal_type(args[5])  # N: Revealed type is "builtins.object"
+        for a in args:
+            reveal_type(a)  # N: Revealed type is "builtins.object"
+        b = 'foo' in args
+        reveal_type(b)  # N: Revealed type is "builtins.bool"
+        reveal_type(args.count(42))  # N: Revealed type is "builtins.int"
+        reveal_type(len(args))  # N: Revealed type is "builtins.int"
+        for c, d in kwargs.items():
+            reveal_type(c)  # N: Revealed type is "builtins.str"
+            reveal_type(d)  # N: Revealed type is "builtins.object"
+        kwargs.pop('bar')
+        return 'baz'
+    return inner
+[builtins fixtures/paramspec.pyi]
diff --git a/test-data/unit/fixtures/paramspec.pyi b/test-data/unit/fixtures/paramspec.pyi
new file mode 100644
index 000000000000..0686924aad6f
--- /dev/null
+++ b/test-data/unit/fixtures/paramspec.pyi
@@ -0,0 +1,76 @@
+# builtins stub for paramspec-related test cases
+
+from typing import (
+    Sequence, Generic, TypeVar, Iterable, Iterator, Tuple, Mapping, Optional, Union, Type, overload,
+    Protocol
+)
+
+T = TypeVar("T")
+T_co = TypeVar('T_co', covariant=True)
+KT = TypeVar("KT")
+VT = TypeVar("VT")
+
+class object:
+    def __init__(self) -> None: ...
+
+class function: ...
+class ellipsis: ...
+
+class type:
+    def __init__(self, *a: object) -> None: ...
+    def __call__(self, *a: object) -> object: ...
+
+class list(Sequence[T], Generic[T]):
+    @overload
+    def __getitem__(self, i: int) -> T: ...
+    @overload
+    def __getitem__(self, s: slice) -> list[T]: ...
+    def __contains__(self, item: object) -> bool: ...
+    def __iter__(self) -> Iterator[T]: ...
+
+class int:
+    def __neg__(self) -> 'int': ...
+
+class bool(int): ...
+class float: ...
+class slice: ...
+class str: ...
+class bytes: ...
+
+class tuple(Sequence[T_co], Generic[T_co]):
+    def __new__(cls: Type[T], iterable: Iterable[T_co] = ...) -> T: ...
+    def __iter__(self) -> Iterator[T_co]: ...
+    def __contains__(self, item: object) -> bool: ...
+    def __getitem__(self, x: int) -> T_co: ...
+    def __mul__(self, n: int) -> Tuple[T_co, ...]: ...
+    def __rmul__(self, n: int) -> Tuple[T_co, ...]: ...
+    def __add__(self, x: Tuple[T_co, ...]) -> Tuple[T_co, ...]: ...
+    def __len__(self) -> int: ...
+    def count(self, obj: object) -> int: ...
+
+class _ItemsView(Iterable[Tuple[KT, VT]]): ...
+
+class dict(Mapping[KT, VT]):
+    @overload
+    def __init__(self, **kwargs: VT) -> None: ...
+    @overload
+    def __init__(self, arg: Iterable[Tuple[KT, VT]], **kwargs: VT) -> None: ...
+    def __getitem__(self, key: KT) -> VT: ...
+    def __setitem__(self, k: KT, v: VT) -> None: ...
+    def __iter__(self) -> Iterator[KT]: ...
+    def __contains__(self, item: object) -> int: ...
+    def update(self, a: Mapping[KT, VT]) -> None: ...
+    @overload
+    def get(self, k: KT) -> Optional[VT]: ...
+    @overload
+    def get(self, k: KT, default: Union[KT, T]) -> Union[VT, T]: ...
+    def __len__(self) -> int: ...
+    def pop(self, k: KT) -> VT: ...
+    def items(self) -> _ItemsView[KT, VT]: ...
+
+def isinstance(x: object, t: type) -> bool: ...
+
+class _Sized(Protocol):
+    def __len__(self) -> int: ...
+
+def len(x: _Sized) -> int: ...


Code review comments:
diff_hunk: @@ -583,19 +583,39 @@ class ParamSpecType(TypeVarLikeType):
     prefix: 'Parameters'
 
     def __init__(
-         self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int,
-         upper_bound: Type, *, line: int = -1, column: int = -1,
-         prefix: Optional['Parameters'] = None
+         self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int, *,
+         upper_bound: Optional[Type] = None,
+         named_type_func: Optional[Callable[..., 'Instance']] = None, line: int = -1,
path: mypy/types.py
text: @user1:
Currently we at least mostly avoid calling back to the type checker in `mypy.types` or `mypy.nodes` (even through a callback), and I'd prefer to continue to maintain this principle, as a style issue. Also, it's better if constructors of essentially data objects such as types don't perform any non-trivial logic, such as calling `get_fallback`.

What about defining a helper function in `mypy.semanal_shared` that takes some of these arguments (`named_type_func` etc.) and calculates the fallback? `get_fallback` would be removed from here and would part of this helper function. You could name it `calculate_param_spec_fallback`, for example, similar to `calculate_tuple_fallback` that is already defined there.

Also, can you add argument types for the callback?

@author:
I moved the code into `semanal_shared` -- although it could actually possibly be moved into `typeanal`, since it seems paramspec args and paramspec kwargs are only ever constructed in `typeanal` at the moment. (But perhaps that might change in the future -- what do you think?)

Moving the code into `typeanal` would mean that we wouldn't have to use a callback protocol at all, the functions could just call `self.named_type()` directly. But the functions might become more tightly coupled with the logic in `typeanal.py`.