You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
singledispatchmethod raises an error when relying on a forward declaration #86153
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2020-10-09.20:11:43.528>labels= ['type-bug', 'library', '3.9', '3.10']
title='singledispatchmethod raises an error when relying on a forward declaration'updated_at=<Date2022-01-11.09:46:19.220>user='https://github.com/glyph'
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/typing.py", line 518, in _evaluate
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
NameError: name 'Comparable' is not defined
It seems like perhaps singledispatchmethod should defer its type evaluation to its first invocation?
AFAIK the normal way of registering types (dispatcher.register(<type>)) also requires that registered type be defined at the execution type. I guess, if we are going to support such a thing, we might end up with supporting passing strings into the .register() as the initial argument of matching type to be consistent. Anyways, this would be a feature request for 3.10+, so changing the version info.
This behavior (only relevant with from __future__ import annotations) has been around since @singledispatchmethod was introduced in 3.8, so I agree we should treat it as a feature request.
In the meantime, maybe a workaround is to move the register call out of the class? It looks a little bit ugly but probably works.
(Disclaimer: I'm not familiar with singledispatchmethod.)
glyph commented on Oct 10, 2020
glyphmannequin
MannequinAuthor
More actions
The behavior is the same with a traditional quoted forward declaration, so it’s not specific to the __future__ import; I just phrased the example that way to show how it’s going to look in the future and to illustrate how it might crop up in a way which is maximally confusing to users less familiar with the internals of type annotations.
ryansobol commented on Oct 29, 2020
ryansobolmannequin
Mannequin
More actions
It's worth pointing out that a similar error is produced for a forward-referenced return type of a registered method, but only for python3.9. For example:
However, this code throws a NameError in python3.9.
$ python3.9 --version
Python 3.9.0
$ python3.9 integer.py
Traceback (most recent call last):
File "/Users/ryansobol/Downloads/integer.py", line 5, in <module>classInteger:
File "/Users/ryansobol/Downloads/integer.py", line 17, in Integerdef_(self, other: int) -> "Integer":
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/functools.py", line 909, in registerreturnself.dispatcher.register(cls, func=method)
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/functools.py", line 860, in register
argname, cls=next(iter(get_type_hints(func).items()))
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 1386, in get_type_hints
value = _eval_type(value, globalns, localns)
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 254, in _eval_typereturn t._evaluate(globalns, localns, recursive_guard)
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 497, in _evaluateself.__forward_value__ = _eval_type(
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 254, in _eval_typereturn t._evaluate(globalns, localns, recursive_guard)
File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 493, in _evaluateeval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>NameError: name 'Integer' is not defined
I know that some may see this issue as a feature request for 3.10+. However, for me, it is a bug preventing my code from migrating to 3.9.
Thanks for the bisection. It's not surprising that that's the culprit, and in other situations that's the right thing to do. I'm not sure how to address this without breaking other stuff -- maybe leave the ForwardRef if evaluating it doesn't work? But that's likely to have other subtle side effects -- we still want simple typos (or other reasons why a reference is legitimately broken) to go unchecked. Maybe singledispatch can catch the error and fall back on looking at bare __annotations__?
This can probably be closed as it's not an issue in Py 3.10+
I can still reproduce the issue, so I can't confirm this. What exactly changed in Python 3.10?
First, regarding the problem discussed in #86153 (comment) and further on: this remained unfixed in 3.9, 3.10, 3.11, 3.12 and 3.13. It was fixed by PEP 749, precisely by 7b7b90d (GH-119891):
t.py:
from __future__ importannotationsfromfunctoolsimportsingledispatchmethodclassInteger:
@singledispatchmethoddefadd(self, other: object) ->Integer:
raiseNotImplementedError(f"Unsupported type {type(other)}")
@add.registerdef_(self, other: int) ->"Integer":
...
print("ok!")
❯ uvx every-python run 7b7b90d~ python t.py |& tail -n 1
NameError: name 'Integer' is not defined
❯ uvx every-python run 7b7b90d python t.py |& tail -n 1
ok!
Therefore, this is no longer a bug to fix.
The original error reported by the OP still reproduces on 3.15:
As @isidentical noted, the reason why this didn't work in the first place is that @register evaluates the annotation immediately, and at the time of evaluation the class body is still executing to complete the class namespace; the final class object is not available in any way yet. There is no way to get to the class object, because it simply doesn't exist at this time.
One simple solution that comes to my mind is delaying registrations entirely by moving them to __set_name__ of singledispatchmethod, when the class object is ready. singledispatchmethod.register() would simply queue registrations and __set_name__ would flush it to the singledispatch registry in the context of the finalized class.
Doing this to all registrations could break .dispatch calls inside the class body, but that seems like poor programming to me.
If that is a concern though, we could only queue unresolvable registrations, but that gives us a complicated flow to reason about + implementation complexity.
Activity
glyph commentedon Oct 9, 2020
This example:
print(Comparable().compare(Comparable()))
Produces this result:
File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/typing.py", line 518, in _evaluate
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
NameError: name 'Comparable' is not defined
It seems like perhaps singledispatchmethod should defer its type evaluation to its first invocation?
isidentical commentedon Oct 9, 2020
AFAIK the normal way of registering types (dispatcher.register(<type>)) also requires that registered type be defined at the execution type. I guess, if we are going to support such a thing, we might end up with supporting passing strings into the .register() as the initial argument of matching type to be consistent. Anyways, this would be a feature request for 3.10+, so changing the version info.
gvanrossum commentedon Oct 9, 2020
This behavior (only relevant with
from __future__ import annotations) has been around since @singledispatchmethod was introduced in 3.8, so I agree we should treat it as a feature request.In the meantime, maybe a workaround is to move the register call out of the class? It looks a little bit ugly but probably works.
(Disclaimer: I'm not familiar with singledispatchmethod.)
glyph commentedon Oct 10, 2020
The behavior is the same with a traditional quoted forward declaration, so it’s not specific to the __future__ import; I just phrased the example that way to show how it’s going to look in the future and to illustrate how it might crop up in a way which is maximally confusing to users less familiar with the internals of type annotations.
ryansobol commentedon Oct 29, 2020
It's worth pointing out that a similar error is produced for a forward-referenced return type of a registered method, but only for python3.9. For example:
print(Integer(2).add(40))
This code runs without error in python3.8, and I am using this technique in code running in a production environment.
However, this code throws a NameError in python3.9.
I know that some may see this issue as a feature request for 3.10+. However, for me, it is a bug preventing my code from migrating to 3.9.
11 remaining items
joernheissler commentedon Jan 3, 2022
Hello!
git-bisect points at
https://bugs.python.org/issue41341
#21553
It breaks both the examples from
https://bugs.python.org/issue41987#msg379896 and
https://bugs.python.org/issue41987#msg380803
gvanrossum commentedon Jan 11, 2022
Thanks for the bisection. It's not surprising that that's the culprit, and in other situations that's the right thing to do. I'm not sure how to address this without breaking other stuff -- maybe leave the ForwardRef if evaluating it doesn't work? But that's likely to have other subtle side effects -- we still want simple typos (or other reasons why a reference is legitimately broken) to go unchecked. Maybe singledispatch can catch the error and fall back on looking at bare __annotations__?
pythonGH-86153: get_type_hints now parses forward reference properly
pythonGH-86153: Created a test for get_type_hints based on GVR's exam…
ashb commentedon Jan 13, 2025
This can probably be closed as it's not an issue in Py 3.10+
bintime: Work around python/cpython#86153
Add bintime submodule (#73)
johnslavik commentedon Jan 6, 2026
I can still reproduce the issue, so I can't confirm this. What exactly changed in Python 3.10?
First, regarding the problem discussed in #86153 (comment) and further on: this remained unfixed in 3.9, 3.10, 3.11, 3.12 and 3.13. It was fixed by PEP 749, precisely by 7b7b90d (GH-119891):
t.py:Therefore, this is no longer a bug to fix.
The original error reported by the OP still reproduces on 3.15:
with a lovely error message:
(Without
from __future__ import annotationson top we also get nearly the same error).It's not a bug report, but a feature request, as previously noted by @isidentical in #86153 (comment) and by @gvanrossum in #86153 (comment). I'm relabeling this issue accordingly.
As @isidentical noted, the reason why this didn't work in the first place is that
@registerevaluates the annotation immediately, and at the time of evaluation the class body is still executing to complete the class namespace; the final class object is not available in any way yet. There is no way to get to the class object, because it simply doesn't exist at this time.One simple solution that comes to my mind is delaying registrations entirely by moving them to
__set_name__ofsingledispatchmethod, when the class object is ready.singledispatchmethod.register()would simply queue registrations and__set_name__would flush it to the singledispatch registry in the context of the finalized class.Doing this to all registrations could break
.dispatchcalls inside the class body, but that seems like poor programming to me.If that is a concern though, we could only queue unresolvable registrations, but that gives us a complicated flow to reason about + implementation complexity.
johnslavik commentedon Jan 20, 2026
Revisiting this, I think it's not worth it.
This is easy and works:
The cost of changing the assumptions that singledispatch was built on is IMO too high to support this one edge case.
kyleaoman commentedon May 20, 2026
@johnslavik the suggested workaround:
does work at runtime, but results in a
mypyerror:error: "Callable[..., Any]" has no attribute "register" [attr-defined]