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
Type variable with NamedTuple as upper bound not working #3915
I'm trying to type annotate a method that takes a NamedTuple subclass as an argument and returns an instance of that class. Here is a complete example:
fromtypingimportDictfromtypingimportNamedTuplefromtypingimportTypefromtypingimportTypeVarStruct=TypeVar('Struct', bound=NamedTuple)
defnamedtuple_from_dict(nt_class: Type[Struct], dict_values: Dict) ->Struct:
"""Create a namedtuple from a dict, using the namedtuple attribute names to look up values in the dict."""returnnt_class._make(dict_values.get(k) forkinnt_class._fields) # type: ignoreclassPoint(NamedTuple):
x: inty: intnamedtuple_from_dict(Point, {'x': 5, 'y': 3})
# error: Type argument 1 of "namedtuple_from_dict" has incompatible value "Point"
I might be doing something wrong, but I think according to the documentation this should work. I've seen a couple of bugs being open related to NamedTuples, but not one that seemed to fit this case.
The type: ignore for the return statement is needed because otherwise mypy says the function returns a NamedTuple instead of a Struct instance - which seems to be another bug...?
The point is that NamedTuple is not a real class, it is rather a special form. The same question appeared on typing tracker, see python/typing#431, a possible way proposed there is to define your own protocol that mimics collections.namedtuple interface, then you don't even need the # type: ignore:
I am a bit surprised that several people asked about something like:
x: NamedTuple=Point(1, 2)
But there is actually a simple solution to this. NamedTuple can be declared as a protocol in typeshed, then everything will work. We can list the same protocol members that are added by mypy when creating a synthetic TypeInfo plus stuff from tuple, so that the full definition will be:
NT=TypeVar('NT', bound='NamedTuple')
classNamedTuple(tuple, Protocol): # type: ignore # since 'tuple' is not a protocol_fileds: Tuple[str, ...] # this should be fixed, but this is easy_field_types: Dict[str, Any]
_field_defaults: Dict[str, Any]
_source: strdef_replace(self: NT) ->NT: ...
def_asdict(self) ->Dict[str, Any]:
@classmethoddef_make(cls: Type[NT], iterable: Iterable[Any], *, new: Any= ..., len: Any= ...) ->NT: ...
To be honest that looks kind of ugly, since protocols aren't supposed to derive from concrete classes. Also, it isn't type safe since it's deriving from Tuple[Any, ...].
I think that the root of the problem is that NamedTuple isn't a real type, it's a type constructor. A better workaround would be to disallow using NamedTuple as a type -- it's surprising that mypy doesn't complain about it already. It's still possible to create a user-defined protocol with the NamedTuple members that are relevant.
it's surprising that mypy doesn't complain about it already
It is defined as a class in typeshed, therefore it is valid in annotations, but yes we can just easily prohibit this. Maybe it makes sense to mention this in common issues?
In our case, we are defining an interface for others to use. So we would like them to define a type they will pass to us, because we would like to have (name, type, value). In concrete example, we have an API for machine learning primitives. They have internally parameters they learned (weights on deep learning layers, etc.). The usefulness of NamedTuple seems to be that they can define their own set of parameters for their primitive, name each value, put a type on it. So we have something like:
One extra thing which we are looking into and seems also not easily possible is to define a second type, which would be based on the Params. So in machine learning these days everyone wants to compute gradients on their params. So what would be great is to define a type:
In this case, the return type should be of same structure as Params, have the same fields, same field names, only that all types of those fields would be float.
The issue is that for example type Sequence[Tuple[str, Type, Any]] is not good for Params because we would like that it is required to specify all of parameters one defined. Also, different fields have different types.
I wanted to typehint that the parameter should be a subclass of NamedTuple but I hit the error error: Value of type variable "T" of "from_dict" cannot be "MyType". Maybe the issue is obvious for some people (it still isn't for me), but I still think it would be nice to add something to the docs about this, it would have saved me quite some time. :)
As a stopgap solution we could at least mention this issue in the documentation, as suggested by @sephii. Using a user-defined protocol as the bound seems like a potential workaround.
Not sure if this should be a separate bug, or considered part of this one, but the issue is not specific to TypeVar bounds. Annotating an argument directly as type NamedTuple also does not work:
from typing import NamedTuple
class Person(NamedTuple):
name: str
def foo(x: NamedTuple) -> None:
pass
p = Person(name="Name")
foo(p)
Fails with: :11: error: Argument 1 to "foo" has incompatible type "Person"; expected "NamedTuple"
The general issue seems to be what Ivan mentioned, "NamedTuple is not a class, it's a special form"
Treating NamedTuple as a class would be problematic for a few technical reasons:
Named tuples don't have a specific common base class like NamedTuple in their MRO (other than tuple), and if mypy treated it as such a common base class, it would be inconsistent and potentially confusing (and even unsafe).
namedtuple and NamedTuple can't be used meaningfully as the second argument in isinstance checks, unlike classes. NamedTuple doesn't generate an error when used as the type in isinstance, but that seems like a bug since it doesn't actually do the right thing, as far as I can tell -- it just returns False for named tuple instances.
I'm actually starting to wonder if supporting NamedTuple as a base class was a great idea, since it's actually not a real base class. Maybe we should have used a class decorator instead. It's probably too late to change that, however.
If we want to support typing.NamedTuple as a type, it looks like it would have to be a new special type like Any, Tuple[...] and Callable instead of a regular class. The use case however doesn't seem important enough to add a totally new kind of type, since the required implementation complexity is considerable.
One potential compromise would be to have a predefined a protocol/ABC that can be used instead of NamedTuple for a similar effect. It would have to have a different name, but if a user tried to use NamedTuple as a type, mypy could suggest using the corresponding protocol/ABC instead. Not sure what to call this, though. We could add it to mypy_extensions as a first step if we agree that this would be a good thing to have.
Here are some random ideas for the name of the hypothetical protocol/ABC:
AbstractNamedTuple (like AbstractSet)
NamedTupleProtocol
The protocol would include all relevant tuple and named tuple methods. If we'd make it an ABC, it could subclass tuple.
I think the main use case for me at least is a simple and quick factory for a class which should hold only few predefined and required attributes + some custom methods. I moved our code to use that instead of NamedTuples and it solved the problem.
I think NamedTuples could be made into something similar to Enums. Where are literally a custom class and are in MRO.
That sounds like you're answering the question "what's the use case for NamedTuple". What does your code look like, what does it do, that it needs a TypeVar with plain NamedTuple in the upper bound? I'd expect an answer of the form "if I don't put NamedTuple in the upper bound of the TypeVar, I wouldn't get a useful error message for broken code of the following form: ".
FWIW there is a difference between NamedTuple and Enum, in that the untyped stdlib uses namedtuple (all lowercase) and that doesn't show up in the MRO -- changing that would likely break code so is not an option. And we prefer NamedTuple to be as similar as possible to namedtuple so it's a safe substitution to make.
Maybe output a tailored error message: Did you mean "Tuple" instead of "NamedTuple"?
It would be nice if typing.NamedTuple had a verbose=True option (like collections.namedtuple), to show what's generated. And even better if it could generate a proper Tuple annotation to avoid the problem in the first place ...
[Of course, namedtuple should be replaced by something better, but that's a separate issue.]
As suggested by @sephii and @JukkaL, it would be useful to add something to the docs to clarify the relevant details of NamedTuple discussed in this and other related issues. I bumped into a problem similar to the one described by @sephii. In my case, @overload provided a nice workaround.
It seems as though mypy will now happily acceptNamedTuple as the upper bound of a TypeVar, and as an annotation. These are the two major pain points discussed in this thread, so I'm closing this issue.
Activity
gvanrossum commentedon Sep 4, 2017
With master the error is slightly different:
As a work-around you could remove the
bound=NamedTuplefrom theTypeVar()call, but I agree it's surprising this isn't accepted.ilevkivskyi commentedon Sep 4, 2017
The point is that
NamedTupleis not a real class, it is rather a special form. The same question appeared ontypingtracker, see python/typing#431, a possible way proposed there is to define your own protocol that mimicscollections.namedtupleinterface, then you don't even need the# type: ignore:This is however only works on master (release is planned in few weeks, also for
typing_extensions).sjaensch commentedon Sep 6, 2017
Thanks @gvanrossum, I'll use that workaround for now.
Good to know @ilevkivskyi, I'll be looking for the typing_extensions release and will try out your solution!
mitar commentedon Sep 17, 2017
I have another strange example of this.
Error:
Python 3.5.2
mypy 0.521
ilevkivskyi commentedon Sep 17, 2017
I am a bit surprised that several people asked about something like:
But there is actually a simple solution to this.
NamedTuplecan be declared as a protocol in typeshed, then everything will work. We can list the same protocol members that are added by mypy when creating a syntheticTypeInfoplus stuff fromtuple, so that the full definition will be:@JukkaL what do you think?
JukkaL commentedon Sep 17, 2017
To be honest that looks kind of ugly, since protocols aren't supposed to derive from concrete classes. Also, it isn't type safe since it's deriving from
Tuple[Any, ...].I think that the root of the problem is that
NamedTupleisn't a real type, it's a type constructor. A better workaround would be to disallow usingNamedTupleas a type -- it's surprising that mypy doesn't complain about it already. It's still possible to create a user-defined protocol with theNamedTuplemembers that are relevant.ilevkivskyi commentedon Sep 17, 2017
It is defined as a class in typeshed, therefore it is valid in annotations, but yes we can just easily prohibit this. Maybe it makes sense to mention this in common issues?
JukkaL commentedon Sep 17, 2017
Just rejecting this is good enough for now. If people still continue to ask about this in the future we can add it to common issues.
gvanrossum commentedon Sep 17, 2017
Why do people ask for
x: NamedTuple = Point(1, 2)? What do they think they would gain overx: tuple = Point(1, 2)?ilevkivskyi commentedon Sep 17, 2017
My understanding is that they want
_fields,_make, and_asdict(judging from the original example in this issue and elsewhere).mitar commentedon Sep 17, 2017
In our case, we are defining an interface for others to use. So we would like them to define a type they will pass to us, because we would like to have (name, type, value). In concrete example, we have an API for machine learning primitives. They have internally parameters they learned (weights on deep learning layers, etc.). The usefulness of NamedTuple seems to be that they can define their own set of parameters for their primitive, name each value, put a type on it. So we have something like:
One extra thing which we are looking into and seems also not easily possible is to define a second type, which would be based on the
Params. So in machine learning these days everyone wants to compute gradients on their params. So what would be great is to define a type:In this case, the return type should be of same structure as
Params, have the same fields, same field names, only that all types of those fields would befloat.The issue is that for example type
Sequence[Tuple[str, Type, Any]]is not good forParamsbecause we would like that it is required to specify all of parameters one defined. Also, different fields have different types.sephii commentedon Feb 27, 2018
@ilevkivskyi I just found this issue while trying to create a function that creates an instance of a named tuple based on its fields and a given dict:
I wanted to typehint that the parameter should be a subclass of
NamedTuplebut I hit the errorerror: Value of type variable "T" of "from_dict" cannot be "MyType". Maybe the issue is obvious for some people (it still isn't for me), but I still think it would be nice to add something to the docs about this, it would have saved me quite some time. :)gvanrossum commentedon Feb 27, 2018
This keeps coming up. I think we should just allow NamedTuple (or classes derived from it) as an upper bound for type variables.
JukkaL commentedon Feb 27, 2018
As a stopgap solution we could at least mention this issue in the documentation, as suggested by @sephii. Using a user-defined protocol as the bound seems like a potential workaround.
carljm commentedon Mar 10, 2018
Not sure if this should be a separate bug, or considered part of this one, but the issue is not specific to TypeVar bounds. Annotating an argument directly as type
NamedTuplealso does not work:Fails with:
:11: error: Argument 1 to "foo" has incompatible type "Person"; expected "NamedTuple"The general issue seems to be what Ivan mentioned, "NamedTuple is not a class, it's a special form"
JukkaL commentedon Mar 12, 2018
Treating
NamedTupleas a class would be problematic for a few technical reasons:NamedTuplein their MRO (other thantuple), and if mypy treated it as such a common base class, it would be inconsistent and potentially confusing (and even unsafe).namedtupleandNamedTuplecan't be used meaningfully as the second argument inisinstancechecks, unlike classes.NamedTupledoesn't generate an error when used as the type inisinstance, but that seems like a bug since it doesn't actually do the right thing, as far as I can tell -- it just returnsFalsefor named tuple instances.I'm actually starting to wonder if supporting
NamedTupleas a base class was a great idea, since it's actually not a real base class. Maybe we should have used a class decorator instead. It's probably too late to change that, however.If we want to support
typing.NamedTupleas a type, it looks like it would have to be a new special type likeAny,Tuple[...]andCallableinstead of a regular class. The use case however doesn't seem important enough to add a totally new kind of type, since the required implementation complexity is considerable.One potential compromise would be to have a predefined a protocol/ABC that can be used instead of
NamedTuplefor a similar effect. It would have to have a different name, but if a user tried to useNamedTupleas a type, mypy could suggest using the corresponding protocol/ABC instead. Not sure what to call this, though. We could add it tomypy_extensionsas a first step if we agree that this would be a good thing to have.Here are some random ideas for the name of the hypothetical protocol/ABC:
AbstractNamedTuple(likeAbstractSet)NamedTupleProtocolThe protocol would include all relevant tuple and named tuple methods. If we'd make it an
ABC, it could subclasstuple.gvanrossum commentedon Mar 12, 2018
mitar commentedon Mar 12, 2018
I think the main use case for me at least is a simple and quick factory for a class which should hold only few predefined and required attributes + some custom methods. I moved our code to use that instead of NamedTuples and it solved the problem.
I think NamedTuples could be made into something similar to Enums. Where are literally a custom class and are in MRO.
gvanrossum commentedon Mar 12, 2018
That sounds like you're answering the question "what's the use case for NamedTuple". What does your code look like, what does it do, that it needs a TypeVar with plain NamedTuple in the upper bound? I'd expect an answer of the form "if I don't put NamedTuple in the upper bound of the TypeVar, I wouldn't get a useful error message for broken code of the following form: ".
FWIW there is a difference between NamedTuple and Enum, in that the untyped stdlib uses namedtuple (all lowercase) and that doesn't show up in the MRO -- changing that would likely break code so is not an option. And we prefer NamedTuple to be as similar as possible to namedtuple so it's a safe substitution to make.
kamahen commentedon Mar 12, 2018
Maybe output a tailored error message: Did you mean "Tuple" instead of "NamedTuple"?
It would be nice if
typing.NamedTuplehad averbose=Trueoption (likecollections.namedtuple), to show what's generated. And even better if it could generate a properTupleannotation to avoid the problem in the first place ...[Of course, namedtuple should be replaced by something better, but that's a separate issue.]
khyox commentedon Jul 1, 2018
As suggested by @sephii and @JukkaL, it would be useful to add something to the docs to clarify the relevant details of NamedTuple discussed in this and other related issues. I bumped into a problem similar to the one described by @sephii. In my case,
@overloadprovided a nice workaround.AlexWaygood commentedon Mar 27, 2022
It seems as though mypy will now happily accept
NamedTupleas the upper bound of aTypeVar, and as an annotation. These are the two major pain points discussed in this thread, so I'm closing this issue.