cpython icon indicating copy to clipboard operation
cpython copied to clipboard

gh-142750: Normalize ParamSpec __bound__ when bound is None

Open hyongtao-code opened this issue 1 month ago • 3 comments

When creating a ParamSpec with bound=None, the runtime bound attribute is set to <class 'NoneType'> instead of None.

This change mirrors TypeVar.new by explicitly converting Py_None to NULL before type checking, ensuring consistency between TypeVar and ParamSpec.

Steps to reproduce

from typing import ParamSpec, Callable

P1 = ParamSpec("P")
P2 = ParamSpec("P", bound=None)
P3 = ParamSpec("P", bound=Callable[[int, str], float])

def check_bound(label, p, expected):
    got = p.__bound__
    ok = (got is expected)
    exp_s = repr(expected)
    got_s = repr(got)
    print(f"{label}.__bound__ should be {exp_s}, got {got_s}  ->  {'OK' if ok else 'FAIL'}")

check_bound("P1", P1, None)
check_bound("P2", P2, None)
check_bound("P3", P3, Callable[[int, str], float])

Result without the patch

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_bound.py
P1.__bound__ should be None, got <class 'NoneType'>  ->  FAIL
P2.__bound__ should be None, got <class 'NoneType'>  ->  FAIL
P3.__bound__ should be typing.Callable[[int, str], float], got typing.Callable[[int, str], float]  ->  OK

Result with the patch

d:\MyCode\cpython\PCbuild\amd64>python_d.exe py_bound.py
P1.__bound__ should be None, got None  ->  OK
P2.__bound__ should be None, got None  ->  OK
P3.__bound__ should be typing.Callable[[int, str], float], got typing.Callable[[int, str], float]  ->  OK
  • Issue: gh-142750

hyongtao-code avatar Dec 15 '25 15:12 hyongtao-code

Thanks for the review! 🙂 I've added a NEWS entry and a test case to cover the ParamSpec(..., bound=None) behavior.

Test case passed locally.

D:\MyCode\cpython>PCbuild\amd64\python_d.exe -m test test_typing.py
Using random seed: 3794058828
0:00:00 Run 1 test sequentially in a single process
0:00:00 [1/1] test_typing
0:00:02 [1/1] test_typing passed

== Tests result: SUCCESS ==

1 test OK.

Total duration: 2.0 sec
Total tests: run=715 skipped=1
Total test files: run=1/1
Result: SUCCESS

hyongtao-code avatar Dec 16 '25 01:12 hyongtao-code

Thinking about this more, did this case ever come up with TypeVar?

This seems sort of wrong:

>>> repr(TypeVar("T").__bound__)
'None'
>>> repr(TypeVar("T", bound=None).__bound__)
'None'

Having no bound and having a bound of None mean very different things to a type checker, yet they have the same runtime representation. I feel like this came up before and maybe we just decided not to care, since a bound of None is not very useful at type checking time?

JelleZijlstra avatar Dec 16 '25 02:12 JelleZijlstra

Yes, we decided to not care, because bound=None just means None :)

sobolevn avatar Dec 16 '25 08:12 sobolevn